File Coverage

lib/Dancer2/Plugin/PrometheusTiny.pm
Criterion Covered Total %
statement 59 59 100.0
branch 4 4 100.0
condition 3 4 75.0
subroutine 15 15 100.0
pod 0 2 0.0
total 81 84 96.4


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