File Coverage

lib/Catalyst/Plugin/PrometheusTiny.pm
Criterion Covered Total %
statement 58 60 96.6
branch 5 10 50.0
condition 4 5 80.0
subroutine 16 16 100.0
pod 1 3 33.3
total 84 94 89.3


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