File Coverage

blib/lib/Mojolicious/Plugin/Prometheus.pm
Criterion Covered Total %
statement 112 115 97.3
branch 19 24 79.1
condition 16 21 76.1
subroutine 28 29 96.5
pod 1 1 100.0
total 176 190 92.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Prometheus;
2 12     12   13338290 use Mojo::Base 'Mojolicious::Plugin', -signatures;
  12         8838  
  12         137  
3 12     12   37039 use Mojolicious::Plugin::Prometheus::Collector::Perl;
  12         54  
  12         128  
4 12     12   7713 use IPC::ShareLite;
  12         51543  
  12         825  
5 12     12   6843 use Net::Prometheus;
  12         289931  
  12         899  
6 12     12   199 use Time::HiRes qw/gettimeofday tv_interval/;
  12         54  
  12         167  
7 12     12   1259 use Mojo::Collection;
  12         36  
  12         555  
8 12     12   7741 use Mojolicious::Plugin::Prometheus::Guard;
  12         79  
  12         169  
9 12     12   16386 use Prometheus::MetricRenderer;
  12         48  
  12         136  
10              
11             our $VERSION = '1.5.1';
12              
13             has prometheus => \&_prometheus;
14             has guard => \&_guard;
15             has share => \&_share;
16             has route => sub { undef };
17             has shm_key => sub { $$ };
18              
19             has global_collectors => sub { Mojo::Collection->new };
20              
21             # Attributes to hold the different metrics that are registered
22             has http_request_duration_seconds => sub { undef };
23             has http_request_size_bytes => sub { undef };
24             has http_response_size_bytes => sub { undef };
25             has http_requests_total => sub { undef };
26              
27             # Configuration for the default metric types
28             has config => sub {
29             {
30             http_request_duration_seconds => {
31             buckets => [.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10],
32             labels => [qw/worker method/],
33             cb => sub($c) { $$, $c->req->method, tv_interval($c->stash('prometheus.start_time')) },
34             },
35             http_request_size_bytes => {
36             buckets => [1, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000],
37             labels => [qw/worker method/],
38             cb => sub($c) { $$, $c->req->method, $c->req->content->body_size },
39             },
40             http_response_size_bytes => {
41             buckets => [5, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000],
42             labels => [qw/worker method code/],
43             cb => sub($c) { $$, $c->req->method, $c->res->code, $c->res->content->body_size },
44             },
45             http_requests_total => {
46             labels => [qw/worker method code/],
47             cb => sub($c) { $$, $c->req->method, $c->res->code },
48             },
49             perl_collector => {
50             enabled => 1,
51             labels_cb => sub { { worker => $$ } },
52             },
53             process_collector => {
54             enabled => 1,
55             labels_cb => sub { { worker => $$ } },
56             },
57             }
58             };
59              
60 11     11 1 799 sub register($self, $app, $config = {}) {
  11         25  
  11         26  
  11         24  
  11         24  
61 11 100       61 $self->shm_key($config->{shm_key}) if $config->{shm_key};
62              
63 11         64 for(keys $self->config->%*) {
64 66 100       167 next unless $config->{$_};
65 1         3 $self->config->{$_} = { $self->config->{$_}->%*, $config->{$_}->%* };
66             }
67              
68             # Present _only_ for a short while for backward compat
69 11 100       59 $self->config->{http_request_duration_seconds}{buckets} = $config->{duration_buckets} if $config->{duration_buckets};
70 11 100       82 $self->config->{http_request_size_bytes}{buckets} = $config->{request_buckets} if $config->{request_buckets};
71 11 100       113 $self->config->{http_response_size_bytes}{buckets} = $config->{response_buckets} if $config->{response_buckets};
72              
73             # Net::Prometheus instance can be overridden in its entirety
74 11 100       92 $self->prometheus($config->{prometheus}) if $config->{prometheus};
75              
76             # Only the two built-in servers are supported for now
77 11     20   198 $app->hook(before_server_start => sub { $self->_start(@_, $config) });
  20         55821  
78              
79             $self->http_request_duration_seconds(
80             $self->prometheus->new_histogram(
81             namespace => $config->{namespace} // undef,
82             subsystem => $config->{subsystem} // undef,
83             name => "http_request_duration_seconds",
84             help => "Histogram with request processing time",
85             labels => $self->config->{http_request_duration_seconds}{labels},
86             buckets => $self->config->{http_request_duration_seconds}{buckets},
87             )
88 11   100     333 );
      50        
89              
90             $self->http_request_size_bytes(
91             $self->prometheus->new_histogram(
92             namespace => $config->{namespace} // undef,
93             subsystem => $config->{subsystem} // undef,
94             name => "http_request_size_bytes",
95             help => "Histogram containing request sizes",
96             labels => $self->config->{http_request_size_bytes}{labels},
97             buckets => $self->config->{http_request_size_bytes}{buckets},
98             )
99 11   100     2681 );
      50        
100              
101             $self->http_response_size_bytes(
102             $self->prometheus->new_histogram(
103             namespace => $config->{namespace} // undef,
104             subsystem => $config->{subsystem} // undef,
105             name => "http_response_size_bytes",
106             help => "Histogram containing response sizes",
107             labels => $self->config->{http_response_size_bytes}{labels},
108             buckets => $self->config->{http_response_size_bytes}{buckets},
109             )
110 11   100     5371 );
      50        
111              
112             $self->http_requests_total(
113             $self->prometheus->new_counter(
114             namespace => $config->{namespace} // undef,
115             subsystem => $config->{subsystem} // undef,
116             name => "http_requests_total",
117             help => "How many HTTP requests processed, partitioned by status code and HTTP method.",
118             labels => $self->config->{http_requests_total}{labels},
119             )
120 11   100     1748 );
      50        
121              
122             $app->hook(
123             before_dispatch => sub {
124 234     234   2492442 my ($c) = @_;
125 234         2482 $c->stash('prometheus.start_time' => [gettimeofday]);
126 234         6370 $self->http_request_size_bytes->observe($self->config->{http_request_size_bytes}{cb}->($c));
127             }
128 11         1616 );
129              
130             $app->hook(
131             after_render => sub {
132 234     234   451055 my ($c) = @_;
133 234         1520 $self->http_request_duration_seconds->observe($self->config->{http_request_duration_seconds}{cb}->($c));
134             }
135 11         213 );
136              
137             $app->hook(
138             after_dispatch => sub {
139 234     234   170616 my ($c) = @_;
140 234         1065 $self->http_requests_total->inc($self->config->{http_requests_total}{cb}->($c));
141 234         40782 $self->http_response_size_bytes->observe($self->config->{http_response_size_bytes}{cb}->($c));
142             }
143 11         216 );
144              
145             # Create common helper methods
146 11     36   232 $app->helper('prometheus.instance' => sub { $self->prometheus });
  36         1541  
147 11         6606 $app->helper('prometheus.register' => \&_register);
148              
149             # Plugin-internal helper methods
150 11         5003 $app->helper('prometheus.collect' => \&_collect);
151 11     39   4939 $app->helper('prometheus.guard' => sub { $self->guard });
  39         1050  
152 11     14   5108 $app->helper('prometheus.global_collectors' => sub { $self->global_collectors });
  14         311  
153              
154             # Create the endpoint that should serve metrics
155 11   66     5482 my $prefix = $config->{route} // $app->routes->under('/');
156 11   100     3914 $self->route($prefix->get($config->{path} // '/metrics'));
157 11         4543 $self->route->to(cb => \&_metrics);
158             }
159              
160 13     13   47216 sub _metrics($c) {
  13         37  
  13         30  
161 13         73 $c->render(text => $c->prometheus->collect, format => 'txt');
162             }
163              
164 22     22   9902 sub _register($c, $collector, $scope = 'worker') {
  22         52  
  22         42  
  22         49  
  22         64  
165 22 100       168 return $c->prometheus->instance->register($collector) if $scope eq 'worker';
166 1         6 return push $c->prometheus->global_collectors->@*, $collector;
167             }
168              
169 13     13   368 sub _collect($c) {
  13         33  
  13         64  
170             # Update stats for current worker
171 13     13   48 $c->prometheus->guard->_change(sub { $_->{$$} = $c->prometheus->instance->render });
  13         80  
172              
173             # Fetch stats for all worker-specific collectors
174 13         105 my $worker_stats = Mojo::Collection->new(keys %{$c->prometheus->guard->_fetch})
175             ->sort
176 13     13   1166 ->map(sub { ($c->prometheus->guard->_fetch->{$_}) })
177 13         389 ->join("\n");
178              
179             # Fetch stats for global / on-demand collectors
180 13         1332 my $renderer = Prometheus::MetricRenderer->new;
181             my $global_stats = $c->prometheus->global_collectors
182 1     1   24 ->map(sub { [ $_->collect({}) ] })
183 1     1   228 ->map(sub { $renderer->render($_) })
184 13         132 ->join("\n");
185              
186 13         834 return $worker_stats."\n".$global_stats."\n";
187             }
188              
189 10     10   68 sub _share($self) {
  10         20  
  10         24  
190 10 50       49 IPC::ShareLite->new(-key => $self->shm_key, -create => 1, -destroy => 0) || die $!;
191             }
192              
193 10     10   86 sub _guard($self) {
  10         60  
  10         32  
194 10         86 Mojolicious::Plugin::Prometheus::Guard->new(share => $self->share);
195             }
196              
197             sub _start {
198 20     20   149 my ($self, $server, $app, $config) = @_;
199 20 50       210 return unless $server->isa('Mojo::Server::Daemon');
200              
201             Mojo::IOLoop->next_tick(
202             sub {
203 20     20   21267 my $labels = $self->config->{process_collector}{labels_cb}->();
204 20         280 my $collector = Net::Prometheus::ProcessCollector->new(labels => [%$labels]);
205 20         31449 $app->prometheus->register($collector);
206             }
207 20 50       141 ) if $self->config->{process_collector}{enabled};
208              
209             # Remove stopped workers
210             $server->on(
211             reap => sub {
212 0     0   0 my ($server, $pid) = @_;
213 0         0 $self->guard->_change(sub { delete $_->{$pid} });
  0         0  
214             }
215 20 50       1845 ) if $server->isa('Mojo::Server::Prefork');
216             }
217              
218 10     10   51 sub _prometheus($self) {
  10         18  
  10         18  
219 10         82 my $prometheus = Net::Prometheus->new(disable_process_collector => 1, disable_perl_collector => 1);
220              
221             # Adding the Perl-collector, if enabled
222 10 50       292 if($self->config->{perl_collector}{enabled}) {
223 10         102 my $perl_collector = Mojolicious::Plugin::Prometheus::Collector::Perl->new($self->config->{perl_collector});
224 10         261 $prometheus->register($perl_collector);
225             }
226 10         521 return $prometheus;
227             };
228              
229             1;
230              
231             __END__