File Coverage

lib/Catalyst/Plugin/PrometheusTiny.pm
Criterion Covered Total %
statement 56 59 94.9
branch 3 10 30.0
condition 4 5 80.0
subroutine 16 16 100.0
pod 1 3 33.3
total 80 93 86.0


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::PrometheusTiny;
2              
3 2     2   1892718 use strict;
  2         5  
  2         68  
4 2     2   11 use warnings;
  2         4  
  2         54  
5 2     2   24 use v5.10.1;
  2         6  
6              
7             our $VERSION = '0.004';
8              
9             $VERSION = eval $VERSION;
10              
11 2     2   10 use Carp ();
  2         3  
  2         28  
12 2     2   569 use Catalyst::Utils ();
  2         75411  
  2         61  
13 2     2   521 use Moose::Role;
  2         5249  
  2         14  
14 2     2   12812 use Prometheus::Tiny::Shared;
  2         23245  
  2         1609  
15              
16             my $defaults = {
17             metrics => {
18             http_request_duration_seconds => {
19             help => 'Request durations in seconds',
20             type => 'histogram',
21             },
22             http_request_size_bytes => {
23             help => 'Request sizes in bytes',
24             type => 'histogram',
25             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
26             },
27             http_requests_total => {
28             help => 'Total number of http requests processed',
29             type => 'counter',
30             },
31             http_response_size_bytes => {
32             help => 'Response sizes in bytes',
33             type => 'histogram',
34             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
35             }
36             },
37             };
38              
39             my ($prometheus, # instance
40             $ignore_path_regexp, # set from config
41             $metrics_endpoint, # set from config with default
42             $no_default_controller, # set from config
43             $request_path # derived from $metrics_endpoint
44             );
45              
46             # for testing
47             sub _clear_prometheus {
48 1     1   4568 undef $prometheus;
49             }
50              
51             sub prometheus {
52 11     11 1 28738 my $c = shift;
53 11   66     55 $prometheus ||= do {
54             my $config = Catalyst::Utils::merge_hashes(
55             $defaults,
56 3   100     67 $c->config->{'Plugin::PrometheusTiny'} || {}
57             );
58              
59 3         255 $metrics_endpoint = $config->{endpoint};
60 3 50       14 if ($metrics_endpoint) {
61 0 0       0 if ( $metrics_endpoint !~ m|^/| ) {
62 0         0 Carp::croak
63             "Plugin::PrometheusTiny endpoint '$metrics_endpoint' does not begin with '/'";
64             }
65             }
66             else {
67 3         7 $metrics_endpoint = '/metrics';
68             }
69              
70 3         7 $request_path = $metrics_endpoint;
71 3         17 $request_path =~ s|^/||;
72              
73 3         12 $ignore_path_regexp = $config->{ignore_path_regexp};
74 3 50       12 if ($ignore_path_regexp) {
75 0 0       0 $ignore_path_regexp = qr/$ignore_path_regexp/
76             unless 'Regexp' eq ref $ignore_path_regexp;
77             }
78              
79 3         7 $no_default_controller = $config->{no_default_controller};
80              
81 3         6 my $metrics = $config->{metrics};
82 3 50       13 Carp::croak "Plugin::PrometheusTiny metrics must be a hash reference"
83             unless 'HASH' eq ref $metrics;
84              
85             my $prom
86             = Prometheus::Tiny::Shared->new(
87             ( filename => $config->{filename} ) x defined $config->{filename}
88 3         32 );
89              
90 3         3237 for my $name ( sort keys %$metrics ) {
91             $prom->declare(
92             $name,
93 13         871 %{ $metrics->{$name} },
  13         66  
94             );
95             }
96              
97 3         58 $prom;
98             };
99 11         54 return $prometheus;
100             }
101              
102             after finalize => sub {
103             my $c = shift;
104             my $request = $c->request;
105              
106             return
107             if !$no_default_controller && $request->path eq $request_path;
108              
109             return
110             if $ignore_path_regexp
111             && $request->path =~ $ignore_path_regexp;
112              
113             my $response = $c->response;
114             my $code = $response->code;
115             my $method = $request->method;
116              
117             $prometheus->histogram_observe(
118             'http_request_size_bytes',
119             $request->content_length // 0,
120             { method => $method, code => $code }
121             );
122             $prometheus->histogram_observe(
123             'http_response_size_bytes',
124             $response->has_body ? length( $response->body ) : 0,
125             { method => $method, code => $code }
126             );
127             $prometheus->inc(
128             'http_requests_total',
129             { method => $method, code => $code }
130             );
131             $prometheus->histogram_observe(
132             'http_request_duration_seconds',
133             $c->stats->elapsed, { method => $method, code => $code }
134             );
135             };
136              
137             before setup_components => sub {
138             my $class = shift;
139              
140             # initialise prometheus instance pre-fork and setup lexicals
141             $class->prometheus;
142              
143             return
144             if $class->config->{'Plugin::PrometheusTiny'}{no_default_controller};
145              
146             # Paranoia, as we're going to eval $metrics_endpoint
147             if ( $metrics_endpoint =~ s|[^-A-Za-z0-9\._~/]||g ) {
148             $class->log->warn(
149             "Plugin::PrometheusTiny unsafe characters removed from endpoint");
150             }
151              
152             $class->log->info(
153             "Plugin::PrometheusTiny metrics endpoint installed at $metrics_endpoint"
154             );
155              
156             eval qq|
157              
158             package Catalyst::Plugin::PrometheusTiny::Controller;
159             use base 'Catalyst::Controller';
160              
161             sub begin : Private { }
162             sub end : Private { }
163              
164             sub metrics : Path($metrics_endpoint) Args(0) {
165             my ( \$self, \$c ) = \@_;
166             my \$res = \$c->res;
167             \$res->content_type("text/plain");
168             \$res->output( \$c->prometheus->format );
169             }
170             1;
171              
172 1     1 0 9 | or do {
  1     1 0 2  
  1     1   241  
  1     1   9  
  1     4   3  
  1     4   9  
  1     4   13417  
  1         3  
  1         4  
  1         1058  
  1         3  
  1         5  
  4         7961  
  4         28  
  4         181  
  4         1086  
173             Carp::croak("Plugin::PrometheusTiny controller eval failed: $@");
174             };
175              
176             $class->inject_component(
177             "Controller::Plugin::PrometheusTiny" => {
178             from_component => "Catalyst::Plugin::PrometheusTiny::Controller"
179             }
180             );
181             };
182              
183             1;
184              
185             =head1 NAME
186              
187             Catalyst::Plugin::PrometheusTiny - use Prometheus::Tiny with Catalyst
188              
189             =head1 SYNOPSIS
190              
191             Use the plugin in your application class:
192              
193             package MyApp;
194             use Catalyst 'PrometheusTiny';
195              
196             MyApp->setup;
197              
198             Add more metrics:
199              
200             MyApp->config('Plugin::PrometheusTiny' => {
201             metrics => {
202             myapp_thing_to_measure => {
203             help => 'Some thing we want to measure',
204             type => 'histogram',
205             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
206             },
207             myapp_something_else_to_measure => {
208             help => 'Some other thing we want to measure',
209             type => 'counter',
210             },
211             },
212             });
213              
214             And somewhere in your controller classes:
215              
216             $c->prometheus->observe_histogram(
217             'myapp_thing_to_measure', $value, { label1 => 'foo' }
218             );
219              
220             $c->prometheus->inc(
221             'myapp_something_else_to_measure', $value, { label2 => 'bar' }
222             );
223              
224             Once your app has served from requests you can fetch request/response metrics:
225              
226             curl http://$myappaddress/metrics
227              
228             =head1 DESCRIPTION
229              
230             This plugin integrates L<Prometheus::Tiny::Shared> with your L<Catalyst> app,
231             providing some default metrics for requests and responses, with the ability
232             to easily add further metrics to your app. A default controller is included
233             which makes the metrics available via the configured L</endpoint>, though this
234             can be disabled if you prefer to add your own controller action.
235              
236             See L<Prometheus::Tiny> for more details of the kind of metrics supported.
237              
238             The following metrics are included by default:
239              
240             http_request_duration_seconds => {
241             help => 'Request durations in seconds',
242             type => 'histogram',
243             },
244             http_request_size_bytes => {
245             help => 'Request sizes in bytes',
246             type => 'histogram',
247             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
248             },
249             http_requests_total => {
250             help => 'Total number of http requests processed',
251             type => 'counter',
252             },
253             http_response_size_bytes => {
254             help => 'Response sizes in bytes',
255             type => 'histogram',
256             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
257             }
258              
259             =head1 METHODS
260              
261             =head2 prometheus
262              
263             sub my_action {
264             my ( $self, $c ) = @_;
265              
266             $c->prometheus->inc(...);
267             }
268              
269             Returns the C<Prometheus::Tiny::Shared> instance.
270              
271             =head1 CONFIGURATION
272              
273             =head2 endpoint
274              
275             The endpoint from which metrics are served. Defaults to C</metrics>.
276              
277             =head2 filename
278              
279             It is recommended that this is set to a directory on a memory-backed
280             filesystem. See L<Prometheus::Tiny::Shared/filename> for details and default
281             value.
282              
283             =head2 ignore_path_regex
284              
285             ignore_path_regex => '^(healthcheck|foobar)'
286              
287             A regular expression against which C<< $c->request->path >> is checked, and
288             if there is a match then the request is not added to default request/response
289             metrics.
290              
291             =head2 metrics
292              
293             metrics => {
294             $metric_name => {
295             help => $metric_help_text,
296             type => $metric_type,
297             },
298             # more...
299             }
300              
301             See L<Prometheus::Tiny/declare>. Declare extra metrics to be added to those
302             included with the plugin.
303              
304             =head2 no_default_controller
305              
306             no_default_controller => 0 # default
307              
308             If set to a true value then the default L</endpoint> will not be
309             added, and you will need to add your own controller action for exporting the
310             metrics. Something like:
311              
312             package MyApp::Controller::Stats;
313              
314             sub begin : Private { }
315             sub end : Private { }
316              
317             sub index : Path Args(0) {
318             my ( $self, $c ) = @_;
319             my $res = $c->res;
320             $res->content_type("text/plain");
321             $res->output( $c->prometheus->format );
322             }
323              
324             =head1 AUTHOR
325              
326             Peter Mottram (SysPete) <peter@sysnix.com>
327              
328             =head1 CONTRIBUTORS
329              
330             Curtis "Ovid" Poe
331              
332             =head1 COPYRIGHT
333              
334             Copyright (c) 2021 the Catalyst::Plugin::PrometheusTiny L</AUTHOR>
335             and L</CONTRIBUTORS> as listed above.
336              
337             =head1 LICENSE
338              
339             This library is free software and may be distributed under the same terms
340             as perl itself.
341