File Coverage

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: