File Coverage

lib/Dancer2/Plugin/PrometheusTiny.pm
Criterion Covered Total %
statement 60 61 98.3
branch 5 6 83.3
condition 3 4 75.0
subroutine 15 15 100.0
pod 0 2 0.0
total 83 88 94.3


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::PrometheusTiny;
2 3     3   3164944 use strict;
  3         23  
  3         108  
3 3     3   16 use warnings;
  3         6  
  3         185  
4              
5             our $VERSION = '0.005';
6             $VERSION = eval $VERSION;
7              
8 3     3   2006 use Dancer2::Plugin;
  3         46007  
  3         34  
9 3     3   12095 use Hash::Merge::Simple ();
  3         8  
  3         68  
10 3     3   2320 use Prometheus::Tiny::Shared;
  3         36254  
  3         126  
11 3     3   2142 use Time::HiRes ();
  3         4792  
  3         124  
12 3         79 use Types::Standard qw(
13             ArrayRef
14             Bool
15             Dict
16             Enum
17             InstanceOf
18             Maybe
19             Map
20             Num
21             Optional
22             Str
23 3     3   25 );
  3         11  
24              
25             sub default_metrics {
26             return {
27 2     2 0 133 http_request_duration_seconds => {
28             help => 'Request durations in seconds',
29             type => 'histogram',
30             },
31             http_request_size_bytes => {
32             help => 'Request sizes in bytes',
33             type => 'histogram',
34             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
35             },
36             http_requests_total => {
37             help => 'Total number of http requests processed',
38             type => 'counter',
39             },
40             http_response_size_bytes => {
41             help => 'Response sizes in bytes',
42             type => 'histogram',
43             buckets => [ 1, 50, 100, 1_000, 50_000, 500_000, 1_000_000 ],
44             }
45             };
46             }
47              
48             # CONFIG
49              
50             has endpoint => (
51             is => 'ro',
52             isa => Str,
53             from_config => sub {'/metrics'},
54             );
55              
56             has filename => (
57             is => 'ro',
58             isa => Maybe [Str],
59             from_config => sub {undef},
60             );
61              
62             has include_default_metrics => (
63             is => 'ro',
64             isa => Bool,
65             from_config => sub {1},
66             );
67              
68             has metrics => (
69             is => 'ro',
70             isa => Map [
71             Str,
72             Dict [
73             help => Str,
74             type => Enum [qw/ counter gauge histogram /],
75             buckets => Optional [ ArrayRef [Num] ],
76             ]
77             ],
78             from_config => sub { {} },
79             );
80              
81             has prometheus_tiny_class => (
82             is => 'ro',
83             isa => Enum [ 'Prometheus::Tiny', 'Prometheus::Tiny::Shared' ],
84             from_config => sub {'Prometheus::Tiny::Shared'},
85             );
86              
87             # HOOKS
88              
89             plugin_hooks 'before_format';
90              
91             # KEYWORDS
92              
93             has prometheus => (
94             is => 'ro',
95             isa => InstanceOf ['Prometheus::Tiny'],
96             lazy => 1,
97             clearer => '_clear_prometheus',
98             builder => sub {
99 3     3   37 my $self = shift;
100              
101 3         58 my $class = $self->prometheus_tiny_class;
102 3         157 my $prom = $class->new(
103             ( filename => $self->filename ) x defined $self->filename );
104              
105 3 100       3789 my $metrics = $self->include_default_metrics
106             ? Hash::Merge::Simple->merge(
107             $self->default_metrics,
108             $self->metrics
109             )
110             : $self->metrics;
111              
112 3         392 for my $name ( sort keys %$metrics ) {
113             $prom->declare(
114             $name,
115 11         708 %{ $metrics->{$name} },
  11         44  
116             );
117             }
118              
119 3         107 return $prom;
120             },
121             );
122              
123             plugin_keywords 'prometheus';
124              
125             # add hooks and metrics route
126              
127             sub BUILD {
128 3     3 0 17910 my $plugin = shift;
129 3         64 my $app = $plugin->app;
130 3         72 my $prometheus = $plugin->prometheus;
131              
132             $app->add_hook(
133             Dancer2::Core::Hook->new(
134             name => 'before',
135             code => sub {
136 6     6   503992 my $app = shift;
137 6         64 $app->request->var( prometheus_plugin_request_start =>
138             [Time::HiRes::gettimeofday] );
139             }
140             )
141 3         221 );
142              
143 3 100       1832 if ( $plugin->include_default_metrics ) {
144             $app->add_hook(
145             Dancer2::Core::Hook->new(
146             name => 'after',
147             code => sub {
148 2     2   3519 my $response = shift;
149 2         12 $plugin->_add_default_metrics($response);
150             },
151             )
152 2         57 );
153             $app->add_hook(
154              
155             # thrown errors bypass after hook
156             Dancer2::Core::Hook->new(
157             name => 'after_error',
158             code => sub {
159 1     1   18413 my $response = shift;
160 1         11 $plugin->_add_default_metrics($response);
161             },
162             )
163 2         834 );
164             }
165              
166             $app->add_route(
167             method => 'get',
168             regexp => $plugin->endpoint,
169             code => sub {
170 3     3   315 my $app = shift;
171 3         69 $plugin->execute_plugin_hook(
172             'before_format', $app,
173             $prometheus
174             );
175 3         852 my $response = $app->response;
176 3         37 $response->content_type('text/plain');
177 3         641 $response->content( $plugin->prometheus->format );
178 3         8110 $response->halt;
179             }
180 3         846 );
181             }
182              
183             sub _add_default_metrics {
184 3     3   10 my ( $plugin, $response ) = @_;
185 3 50       33 if ( $response->isa('Dancer2::Core::Response::Delayed') ) {
186 0         0 $response = $response->response;
187             }
188 3         24 my $request = $plugin->app->request;
189              
190             my $elapsed
191             = Time::HiRes::tv_interval(
192 3         14 $request->vars->{prometheus_plugin_request_start} );
193              
194 3         144 my $labels = {
195             code => $response->status,
196             method => $request->method,
197             };
198              
199 3         265 my $prometheus = $plugin->prometheus;
200              
201 3   50     52 $prometheus->histogram_observe(
202             'http_request_size_bytes',
203             length( $request->content || '' ),
204             $labels
205             );
206 3   100     1645 $prometheus->histogram_observe(
207             'http_response_size_bytes',
208             length( $response->content || '' ),
209             $labels
210             );
211 3         1493 $prometheus->inc(
212             'http_requests_total',
213             $labels
214             );
215 3         109 $prometheus->histogram_observe(
216             'http_request_duration_seconds',
217             $elapsed, $labels
218             );
219             }
220              
221             1;
222              
223             =head1 NAME
224              
225             Dancer2::Plugin::PrometheusTiny - use Prometheus::Tiny with Dancer2
226              
227             =head1 SYNOPSIS
228              
229             =head1 DESCRIPTION
230              
231             This plugin integrates L<Prometheus::Tiny::Shared> with your L<Dancer2> app,
232             providing some default metrics for requests and responses, with the ability
233             to easily add further metrics to your app. A route is added which makes
234             the metrics available via the configured L</endpoint>.
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 KEYWORDS
260              
261             =head2 prometheus
262              
263             get '/some/route' => sub {
264             prometheus->inc(...);
265             }
266              
267             Returns the C<Prometheus::Tiny::Shared> instance.
268              
269             =head1 CONFIGURATION
270              
271             Example:
272              
273             plugins:
274             PrometheusTiny:
275             endpoint: /prometheus-metrics # default: /metrics
276             filename: /run/d2prometheus # default: (undef)
277             include_default_metrics: 0 # default: 1
278             metrics: # default: {}
279             http_request_count:
280             help: HTTP Request count
281             type: counter
282            
283             See below for full details of each configuration setting.
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 include_default_metrics
296              
297             Defaults to true. If set to false, then the default metrics shown in
298             L</DESCRIPTION> will not be added.
299              
300             =head2 metrics
301              
302             Declares extra metrics to be merged with those included with the plugin. See
303             See L<Prometheus::Tiny/declare> for details.
304              
305             =head2 prometheus_tiny_class
306              
307             Defaults to L<Prometheus::Tiny::Shared>.
308              
309             B<WARNING:> You shoulf only set this if you are running a single process plack
310             server such as L<Twiggy>, and you don't want to use file-based store for
311             metrics. Setting this to L<Prometheus::Tiny> will mean that metrics are instead
312             stored in memory.
313              
314             =head1 AUTHOR
315              
316             Peter Mottram (SysPete) <peter@sysnix.com>
317              
318             =head1 CONTRIBUTORS
319              
320             None yet.
321              
322             =head1 COPYRIGHT
323              
324             Copyright (c) 2021 the Catalyst::Plugin::PrometheusTiny L</AUTHOR>
325             and L</CONTRIBUTORS> as listed above.
326              
327             =head1 LICENSE
328              
329             This library is free software and may be distributed under the same terms
330             as Perl itself.