blib/lib/Mojolicious/Plugin/Prometheus.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 72 | 75 | 96.0 |
branch | 7 | 10 | 70.0 |
condition | 26 | 34 | 76.4 |
subroutine | 22 | 23 | 95.6 |
pod | 1 | 1 | 100.0 |
total | 128 | 143 | 89.5 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Mojolicious::Plugin::Prometheus; 2: use Mojo::Base 'Mojolicious::Plugin'; 3: use Time::HiRes qw/gettimeofday tv_interval/; 4: use Net::Prometheus; 5: use IPC::ShareLite; 6: 7: our $VERSION = '1.4.1'; 8: 9: has prometheus => sub { Net::Prometheus->new(disable_process_collector => 1) }; 10: has route => sub {undef}; 11: has http_request_duration_seconds => sub { 12: undef; 13: }; 14: has http_request_size_bytes => sub { 15: undef; 16: }; 17: has http_response_size_bytes => sub { 18: undef; 19: }; 20: has http_requests_total => sub { 21: undef; 22: }; 23: 24: 25: sub register { 26: my ($self, $app, $config) = @_; 27: 28: $self->{key} = $config->{shm_key} || '12345'; 29: 30: $self->prometheus($config->{prometheus}) if $config->{prometheus}; 31: $app->helper(prometheus => sub { $self->prometheus }); 32: 33: # Only the two built-in servers are supported for now 34: $app->hook(before_server_start => sub { $self->_start(@_, $config) }); 35: 36: $self->http_request_duration_seconds( 37: $self->prometheus->new_histogram( 38: namespace => $config->{namespace} // undef, 39: subsystem => $config->{subsystem} // undef, 40: name => "http_request_duration_seconds", 41: help => "Histogram with request processing time", 42: labels => [qw/worker method/], 43: buckets => $config->{duration_buckets} // undef, 44: ) 45: ); 46: 47: $self->http_request_size_bytes( 48: $self->prometheus->new_histogram( 49: namespace => $config->{namespace} // undef, 50: subsystem => $config->{subsystem} // undef, 51: name => "http_request_size_bytes", 52: help => "Histogram containing request sizes", 53: labels => [qw/worker method/], 54: buckets => $config->{request_buckets} 55: // [(1, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)], 56: ) 57: ); 58: 59: $self->http_response_size_bytes( 60: $self->prometheus->new_histogram( 61: namespace => $config->{namespace} // undef, 62: subsystem => $config->{subsystem} // undef, 63: name => "http_response_size_bytes", 64: help => "Histogram containing response sizes", 65: labels => [qw/worker method code/], 66: buckets => $config->{response_buckets} 67: // [(5, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)], 68: ) 69: ); 70: 71: $self->http_requests_total( 72: $self->prometheus->new_counter( 73: namespace => $config->{namespace} // undef, 74: subsystem => $config->{subsystem} // undef, 75: name => "http_requests_total", 76: help => 77: "How many HTTP requests processed, partitioned by status code and HTTP method.", 78: labels => [qw/worker method code/] 79: ) 80: ); 81: 82: $app->hook( 83: before_dispatch => sub { 84: my ($c) = @_; 85: $c->stash('prometheus.start_time' => [gettimeofday]); 86: $self->http_request_size_bytes->observe($$, $c->req->method, 87: $c->req->content->body_size); 88: } 89: ); 90: 91: $app->hook( 92: after_render => sub { 93: my ($c) = @_; 94: $self->http_request_duration_seconds->observe($$, $c->req->method, 95: tv_interval($c->stash('prometheus.start_time'))); 96: } 97: ); 98: 99: $app->hook( 100: after_dispatch => sub { 101: my ($c) = @_; 102: $self->http_requests_total->inc($$, $c->req->method, $c->res->code); 103: $self->http_response_size_bytes->observe($$, $c->req->method, 104: $c->res->code, $c->res->content->body_size); 105: } 106: ); 107: 108: 109: my $prefix = $config->{route} // $app->routes->under('/'); 110: $self->route($prefix->get($config->{path} // '/metrics')); 111: $self->route->to( 112: cb => sub { 113: my ($c) = @_; 114: # Collect stats and render 115: $self->_guard->_change(sub { $_->{$$} = $app->prometheus->render }); 116: $c->render( 117: text => join("\n", 118: map { ($self->_guard->_fetch->{$_}) } 119: sort keys %{$self->_guard->_fetch}), 120: format => 'txt' 121: ); 122: } 123: ); 124: 125: } 126: 127: sub _guard { 128: my $self = shift; 129: 130: my $share = $self->{share} 131: ||= IPC::ShareLite->new(-key => $self->{key}, -create => 1, -destroy => 0) 132: || die $!; 133: 134: return Mojolicious::Plugin::Mojolicious::_Guard->new(share => $share); 135: } 136: 137: sub _start { 138: 139: #my ($self, $app, $config) = @_; 140: my ($self, $server, $app, $config) = @_; 141: return unless $server->isa('Mojo::Server::Daemon'); 142: 143: Mojo::IOLoop->next_tick( 144: sub { 145: my $pc = Net::Prometheus::ProcessCollector->new(labels => [worker => $$]); 146: $self->prometheus->register($pc) if $pc; 147: } 148: ); 149: 150: # Remove stopped workers 151: $server->on( 152: reap => sub { 153: my ($server, $pid) = @_; 154: $self->_guard->_change(sub { delete $_->{$pid} }); 155: } 156: ) if $server->isa('Mojo::Server::Prefork'); 157: } 158: 159: 160: package Mojolicious::Plugin::Mojolicious::_Guard; 161: use Mojo::Base -base; 162: 163: use Fcntl ':flock'; 164: use Sereal qw(get_sereal_decoder get_sereal_encoder); 165: 166: my ($DECODER, $ENCODER) = (get_sereal_decoder, get_sereal_encoder); 167: 168: sub DESTROY { shift->{share}->unlock } 169: 170: sub new { 171: my $self = shift->SUPER::new(@_); 172: $self->{share}->lock(LOCK_EX); 173: return $self; 174: } 175: 176: sub _change { 177: my ($self, $cb) = @_; 178: my $stats = $self->_fetch; 179: $cb->($_) for $stats; 180: $self->_store($stats); 181: } 182: 183: sub _fetch { 184: return {} unless my $data = shift->{share}->fetch; 185: return $DECODER->decode($data); 186: } 187: 188: sub _store { shift->{share}->store($ENCODER->encode(shift)) } 189: 190: 1; 191: __END__ 192: 193: =for stopwords prometheus 194: 195: =encoding utf8 196: 197: =head1 NAME 198: 199: Mojolicious::Plugin::Prometheus - Mojolicious Plugin 200: 201: =head1 SYNOPSIS 202: 203: # Mojolicious 204: $self->plugin('Prometheus'); 205: 206: # Mojolicious::Lite 207: plugin 'Prometheus'; 208: 209: # Mojolicious::Lite, with custom response buckets (seconds) 210: plugin 'Prometheus' => { response_buckets => [qw/4 5 6/] }; 211: 212: # You can add your own route to do access control 213: my $under = app->routes->under('/secret' =>sub { 214: my $c = shift; 215: return 1 if $c->req->url->to_abs->userinfo eq 'Bender:rocks'; 216: $c->res->headers->www_authenticate('Basic'); 217: $c->render(text => 'Authentication required!', status => 401); 218: return undef; 219: }); 220: plugin Prometheus => {route => $under}; 221: 222: =head1 DESCRIPTION 223: 224: L<Mojolicious::Plugin::Prometheus> is a L<Mojolicious> plugin that exports Prometheus metrics from Mojolicious. 225: 226: Hooks are also installed to measure requests response time and count requests based on method and HTTP return code. 227: 228: =head1 HELPERS 229: 230: =head2 prometheus 231: 232: Create further instrumentation into your application by using this helper which gives access to the L<Net::Prometheus> object. 233: See L<Net::Prometheus> for usage. 234: 235: =head1 METHODS 236: 237: L<Mojolicious::Plugin::Prometheus> inherits all methods from 238: L<Mojolicious::Plugin> and implements the following new ones. 239: 240: =head2 register 241: 242: $plugin->register($app, \%config); 243: 244: Register plugin in L<Mojolicious> application. 245: 246: C<%config> can have: 247: 248: =over 2 249: 250: =item * route 251: 252: L<Mojolicious::Routes::Route> object to attach the metrics to, defaults to generating a new one for '/'. 253: 254: Default: / 255: 256: =item * path 257: 258: The path to mount the exporter. 259: 260: Default: /metrics 261: 262: =item * prometheus 263: 264: Override the L<Net::Prometheus> object. The default is a new singleton instance of L<Net::Prometheus>. 265: 266: =item * namespace, subsystem 267: 268: These will be prefixed to the metrics exported. 269: 270: =item * request_buckets 271: 272: Override buckets for request sizes histogram. 273: 274: Default: C<(1, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)> 275: 276: =item * response_buckets 277: 278: Override buckets for response sizes histogram. 279: 280: Default: C<(5, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)> 281: 282: =item * duration_buckets 283: 284: Override buckets for request duration histogram. 285: 286: Default: C<(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10)> (actually see L<Net::Prometheus|https://metacpan.org/source/PEVANS/Net-Prometheus-0.05/lib/Net/Prometheus/Histogram.pm#L19>) 287: 288: =item * shm_key 289: 290: Key used for shared memory access between workers, see L<$key in IPc::ShareLite|https://metacpan.org/pod/IPC::ShareLite> for details. 291: 292: =back 293: 294: =head1 METRICS 295: 296: In addition to exposing the default process metrics that L<Net::Prometheus> already expose 297: this plugin will also expose 298: 299: =over 2 300: 301: =item * C<http_requests_total>, request counter partitioned over HTTP method and HTTP response code 302: 303: =item * C<http_request_duration_seconds>, request duration histogram partitioned over HTTP method 304: 305: =item * C<http_request_size_bytes>, request size histogram partitioned over HTTP method 306: 307: =item * C<http_response_size_bytes>, response size histogram partitioned over HTTP method 308: 309: =back 310: 311: =head1 AUTHOR 312: 313: Vidar Tyldum 314: 315: (the IPC::ShareLite parts of this code is shamelessly stolen from L<Mojolicious::Plugin::Status> written by Sebastian Riedel and mangled into something that works for me) 316: 317: =head1 COPYRIGHT AND LICENSE 318: 319: Copyright (C) 2018, Vidar Tyldum 320: 321: This program is free software, you can redistribute it and/or modify it under 322: the terms of the Artistic License version 2.0. 323: 324: =head1 SEE ALSO 325: 326: =over 2 327: 328: =item L<Net::Prometheus> 329: 330: =item L<Mojolicious::Plugin::Status> 331: 332: =item L<Mojolicious> 333: 334: =item L<Mojolicious::Guides> 335: 336: =item L<http://mojolicious.org> 337: 338: =back 339: 340: =cut 341: |