1#  You may distribute under the terms of either the GNU General Public License
2#  or the Artistic License (the same terms as Perl itself)
3#
4#  (C) Paul Evans, 2020 -- leonerd@leonerd.org.uk
5
6package Metrics::Any::Collector 0.07;
7
8use v5.14;
9use warnings;
10
11use Carp;
12
13use Metrics::Any::Adapter;
14
15use List::Util 1.29 qw( pairkeys );
16
17=head1 NAME
18
19C<Metrics::Any::Collector> - module-side of the monitoring metrics reporting API
20
21=head1 SYNOPSIS
22
23   use Metrics::Any '$metrics',
24      strict => 0,
25      name_prefix => [ 'my_module_name' ];
26
27   sub do_thing {
28      $metrics->inc_counter( 'things_done' );
29   }
30
31=head1 DESCRIPTION
32
33Instances of this class provide an API for individual modules to declare
34metadata about metrics they will report, and to report individual values or
35observations on those metrics. An instance should be obtained for a reporting
36module by the C<use Metrics::Any> statement.
37
38The collector acts primarily as a proxy for the application's configured
39L<Metrics::Any::Adapter> instance. The proxy will lazily create an adapter
40when required to first actually report a metric value, but until then any
41metadata stored by any of the C<make_*> methods will not create one. This lazy
42deferral allows a certain amount of flexibility with module load order and
43application startup. By carefully writing module code to not report any values
44of metrics until the main activity has actually begin, it should be possible
45to allow programs to configure the metric reporting in a flexible manner
46during program startup.
47
48=head1 ENVIRONMENT
49
50=head2 METRICS_ANY_DISABLE
51
52I<Since version 0.07.>
53
54Provides a list of packages and namespaces in which to disable L<Metrics::Any>
55reporting entirely.
56
57This variable gives a comma-separated list of name patterns. Patterns may end
58with C<::*>, where they will match any package whose name starts with that
59prefix, or they may be literal package names. If any code in matching packages
60attempts to use L<Metrics::Any::Collector> to report metrics, that code will
61be given a C<Null> adapter, and no metrics will be reported from here.
62
63For example, to disable the metrics that C<Net::Async::HTTP::Server> itself
64creates when exporting Prometheus metrics:
65
66   $ METRICS_ANY_DISABLE=Net::Async::HTTP::Server ./program.pl
67
68=cut
69
70# Not public API; used by Metrics::Any::import_into
71sub new
72{
73   my $class = shift;
74   my ( $package, %args ) = @_;
75
76   return bless {
77      package => $package,
78      adapter => undef,
79      deferred => [],
80      name_prefix => $args{name_prefix},
81      metrics => {},
82      strict => $args{strict} // 1,
83   }, $class;
84}
85
86my %disable_for_package;
87my %disable_for_namespace;
88if( my $val = $ENV{METRICS_ANY_DISABLE} ) {
89   foreach my $pattern ( split m/,/, $val ) {
90      if( $pattern =~ s/\*$// ) {
91         $pattern =~ s/::$//;
92         $disable_for_namespace{$pattern} = 1;
93      }
94      else {
95         $disable_for_package{$pattern} = 1;
96      }
97   }
98
99   require Metrics::Any::Adapter::Null;
100}
101
102sub _enabled_for_package
103{
104   my ( $pkg ) = @_;
105
106   return 0 if $disable_for_package{$pkg};
107   return 1 unless %disable_for_namespace;
108
109   do {
110      return 0 if $disable_for_namespace{$pkg};
111   } while( $pkg =~ s/::[^:]+// );
112
113   return 1;
114}
115
116sub adapter
117{
118   my $self = shift;
119   return $self->{adapter} if $self->{adapter};
120
121   my $adapter = $self->{adapter} =
122      ( _enabled_for_package( $self->{package} ) ? Metrics::Any::Adapter->adapter
123                                                 : Metrics::Any::Adapter::Null->new );
124   foreach my $call ( @{ $self->{deferred} } ) {
125      my ( $method, @args ) = @$call;
126      $adapter->$method( @args );
127   }
128   undef $self->{deferred};
129   return $adapter;
130}
131
132sub _adapter_call
133{
134   my $self = shift;
135   my ( $method, @args ) = @_;
136
137   if( $self->{adapter} ) {
138      $self->{adapter}->$method( @args );
139   }
140   else {
141      push @{ $self->{deferred} }, [ $method, @args ];
142   }
143}
144
145sub _metricname
146{
147   my $self = shift;
148   my ( $suffix ) = @_;
149
150   return $suffix unless defined $self->{name_prefix};
151   return [ @{ $self->{name_prefix} }, @$suffix ];
152}
153
154sub _labelvalues
155{
156   my $self = shift;
157   my ( $type, $handle, @args ) = @_;
158
159   my $meta = $self->{$handle};
160   if( $meta ) {
161      $meta->[0] eq $type or croak "Metric '$handle' is not a $type";
162   }
163   elsif( !$self->{strict} ) {
164      my @labelnames;
165      if( !@args ) {
166         # no labels
167      }
168      elsif( ref $args[0] eq "ARRAY" ) {
169         @labelnames = pairkeys @{ $args[0] };
170      }
171      elsif( ref $args[0] eq "HASH" ) {
172         carp "Lazily creating a labelled metric with multiple labels using a HASH reference yields unreliable label order"
173            if keys %{ $args[0] } > 1;
174         @labelnames = keys %{ $args[0] };
175      }
176      else {
177         croak "Cannot lazily create a labelled metric from label values specified in a flat list";
178      }
179
180      my $make_method = "make_$type";
181      $self->$make_method( $handle, labels => \@labelnames );
182
183      $meta = $self->{$handle};
184   }
185   else {
186      croak "No such metric '$handle'";
187   }
188
189   my ( undef, @labelnames ) = @$meta;
190
191   if( !@args ) {
192      return;
193   }
194   elsif( ref $args[0] ) {
195      warn "Received additional arguments to metrics reporting function\n" if @args > 1;
196      my ( $arg ) = @args;
197      my %v = ( ref $arg eq "ARRAY" ) ? @$arg : %$arg;
198
199      my @labelvalues;
200      ( defined $v{$_} or croak "Missing value for label '$_'" ) and push @labelvalues, delete $v{$_}
201         for @labelnames;
202
203      # Warn but don't complain about extra values
204      carp "Found extra label value for '$_'" for keys %v;
205
206      return @labelvalues;
207   }
208   else {
209      return @args;
210   }
211}
212
213=head1 ARGUMENTS
214
215=head2 name_prefix
216
217I<Since version 0.05.>
218
219Optional prefix to prepend to any name provided to the C<make_*> functions.
220
221If set, this value and the registered names must be given as array references,
222not simple strings.
223
224   use Metrics::Any '$metrics', name_prefix => [qw( my_program_name )];
225
226   $metrics->make_counter( events =>
227      name => [ "events" ],
228   );
229
230   # Will create a counter named ["my_program_name", "events"] formed by the
231   # adapter.
232
233=head2 strict
234
235I<Since version 0.05.>
236
237Optional boolean which controls whether metrics must be registered by a
238C<make_> method before they can be used (when true), or whether to attempt
239lazily registering them when first encountered by a reporting method (when
240false).
241
242When strict mode is off and a reporting method (e.g. C<inc_counter>) is
243invoked on an unrecognised handle, it will be lazily registered. If the metric
244is reported with values, an attempt is made to determine what the list of
245label names is; which will depend on the form the label values are given in.
246Labels passed by array reference, or by hash reference for a single label will
247work fine. If a hash reference is passed with multiple keys, a warning is
248printed that the order may not be reliable. Finally, for (discouraged) flat
249lists of values directly it is not possible to recover label name information
250so an exception is thrown.
251
252For this reason, when operating with strict mode off, it is recommended always
253to use the array reference form of supplying labels, to ensure they are
254registered correctly.
255
256In the current version this parameter defaults true, and thus all metrics must
257be registered in advance. This may be changed in a future version for
258convenience in smaller modules, so paranoid authors should set it explicitly:
259
260   use Metrics::Any::Adapter '$metrics', strict => 1;
261
262If strict mode is switched off, it is recommended to set a name prefix to
263ensure that lazily-registered metrics will at least have a useful name.
264
265=cut
266
267=head1 BOOLEAN OVERRIDE
268
269Instances of this class override boolean truth testing. They are usually true,
270except in the case that an adapter has already been created and it is the Null
271type. This allows modules to efficiently test whether to report metrics at all
272by using code such as
273
274   if( $metrics ) {
275      $metrics->inc_counter( name => some_expensive_function() );
276   }
277
278While the Null adapter will simply ignore any of the methods invoked on it,
279without this conditional test the caller would otherwise still have to
280calculate the value that won't be used. This structure allows the calculation
281to be avoided if metrics are not in use.
282
283=cut
284
285use overload
286   'bool' => sub {
287      !$_[0]->{adapter} or ref $_[0]->{adapter} ne "Metrics::Any::Adapter::Null"
288   },
289   # stringify as itself otherwise bool takes over and it just prints as 1,
290   # leading to much developer confusion
291   '""' => sub { $_[0] },
292   fallback => 1;
293
294=head1 METHODS
295
296   $package = $metrics->package
297
298Returns the package name that created the collector; the package in which the
299
300   use Metrics::Any '$metrics';
301
302statement was invoked.
303
304=cut
305
306sub package
307{
308   my $self = shift;
309   return $self->{package};
310}
311
312=head1 METRIC TYPES
313
314Each type of metric is created by one of the C<make_*> methods. They all take
315the following common arguments:
316
317=over 4
318
319=item name => ARRAY[ STRING ] | STRING
320
321Optional. An array of string parts, or a plain string name to use for
322reporting this metric to its upstream service.
323
324Modules should preferrably use an array of string parts to specify their
325metric names, as different adapter types may have different ways to represent
326this hierarchially. Base-level parts of the name should come first, followed
327by more specific parts. It is common for related metrics to be grouped by name
328having identical prefixes but differing only in the final part.
329
330The name is optional; if unspecified then the handle will be used to form the
331name, combined with a C<name_prefix> argument if one was set for the package.
332
333=item description => STRING
334
335Optional human-readable description. May be used for debugging or other
336purposes.
337
338=item labels => ARRAY[ STRING ]
339
340Optional reference to an array of string names to use as label names.
341
342A labelled metric will expect to receive additional information in its
343reporting method to give values for these labels. This information should be
344in either an even-length array reference of name/value pairs, or a hash
345reference. E.g.
346
347   $metrics->inc_counter( handle => [ labelname => $labelvalue ] );
348   $metrics->inc_counter( handle => { labelname => $labelvalue } );
349
350A legacy form where a plain list of values is passed, each corresponding to a
351named label in the same order, is currently accepted but discouraged in favour
352of the above forms.
353
354   $metrics->inc_counter( handle => $labelvalue );
355
356Note that not all metric reporting adapters may be able to represent all of
357the labels. Each should document what its behaviour will be.
358
359=back
360
361=cut
362
363=head2 Counter
364
365The L</make_counter> method creates a new metric which counts occurances of
366some event within the application. Its value begins at zero, and can be
367incremented by L</inc_counter> whenever the event occurs.
368
369Some counters may simple count occurances of events, while others may count
370in other units, for example counts of bytes. Adapters may make use of the
371C<units> parameter of the distribution to perform some kind of
372adapter-specific behaviour. The following units are suggested:
373
374=head3 bytes
375
376Observations give sizes in bytes (perhaps memory buffer or network message
377sizes), and should be integers.
378
379=cut
380
381=head2 make_counter
382
383   $collector->make_counter( $handle, %args )
384
385Requests the creation of a new counter metric. The C<$handle> name should be
386unique within the collector instance, though does not need to be unique across
387the entire program, as it will be namespaced by the collector instance.
388
389The following extra arguments may be passed:
390
391=over 4
392
393=item units => STRING
394
395A hint to the adapter about what kind of measurements are being observed, so
396it might take specific behaviour.
397
398=back
399
400=cut
401
402sub make_counter
403{
404   my $self = shift;
405   my ( $handle, %args ) = @_;
406
407   $args{name} = $self->_metricname( $args{name} // [ $handle ] );
408
409   $self->{$handle} and croak "Already have a metric '$handle'";
410   $self->{$handle} = [ counter => @{ $args{labels} // [] } ];
411
412   $self->_adapter_call( make_counter => "$self->{package}/$handle",
413      collector => $self,
414      %args
415   );
416}
417
418=head2 inc_counter
419
420   $collector->inc_counter( $handle, $labels )
421
422Reports that the counter metric value be incremented by one. The C<$handle>
423name must match one earlier created by L</make_counter>.
424
425=cut
426
427sub inc_counter
428{
429   my $self = shift;
430   my ( $handle, @args ) = @_;
431
432   my @labelvalues = $self->_labelvalues( counter => $handle, @args );
433
434   $self->adapter->inc_counter_by( "$self->{package}/$handle", 1, @labelvalues );
435}
436
437=head2 inc_counter_by
438
439   $collector->inc_counter_by( $handle, $amount, $labels )
440
441Reports that a counter metric value be incremented by some specified value.
442
443=cut
444
445sub inc_counter_by
446{
447   my $self = shift;
448   my ( $handle, $amount, @args ) = @_;
449
450   my @labelvalues = $self->_labelvalues( counter => $handle, @args );
451
452   $self->adapter->inc_counter_by( "$self->{package}/$handle", $amount, @labelvalues );
453}
454
455=head2 Distribution
456
457The L</make_distribution> method creates a new metric which counts individual
458observations of some numerical quantity (which may or may not be integral).
459New observations can be added by the L</report_distribution> method.
460
461Some adapter types may only store an aggregated total; others may store some
462sort of statistical breakdown, either total + count, or a bucketed histogram.
463The specific adapter documentation should explain how it handles
464distributions.
465
466Adapters may make use of the C<units> parameter of the distribution to perform
467some kind of adapter-specific behaviour. The following units are suggested:
468
469=head3 bytes
470
471Observations give sizes in bytes (perhaps memory buffer or network message
472sizes), and should be integers.
473
474=head3 seconds
475
476Observations give durations in seconds.
477
478=cut
479
480=head2 make_distribution
481
482   $collector->make_distribution( $handle, %args )
483
484Requests the creation of a new distribution metric.
485
486The following extra arguments may be passed:
487
488=over 4
489
490=item units => STRING
491
492A hint to the adapter about what kind of measurements are being observed, so
493it might take specific behaviour. If unspecified, a default of C<bytes> will
494apply.
495
496=back
497
498=cut
499
500sub make_distribution
501{
502   my $self = shift;
503   my ( $handle, %args ) = @_;
504
505   $args{name} = $self->_metricname( $args{name} // [ $handle ] );
506
507   $args{units} //= "bytes";
508
509   $self->{$handle} and croak "Already have a metric '$handle'";
510   $self->{$handle} = [ distribution => @{ $args{labels} // [] } ];
511
512   $self->_adapter_call( make_distribution => "$self->{package}/$handle",
513      collector => $self,
514      %args
515   );
516}
517
518=head2 report_distribution
519
520   $collector->report_distribution( $handle, $amount, $labels )
521
522I<Since version 0.05.>
523
524Reports a new observation for the distribution metric. The C<$handle> name
525must match one earlier created by L</make_distribution>. The C<$amount> may
526be interpreted by the adapter depending on the defined C<units> type for the
527distribution.
528
529This method used to be called C<inc_distribution_by> and is currently still
530available as an alias.
531
532=cut
533
534sub report_distribution
535{
536   my $self = shift;
537   my ( $handle, $amount, @args ) = @_;
538
539   my @labelvalues = $self->_labelvalues( distribution => $handle, @args );
540
541   my $adapter = $self->adapter;
542
543   # Support new and legacy name
544   my $method = $adapter->can( "report_distribution" ) // "inc_distribution_by";
545   $adapter->$method( "$self->{package}/$handle", $amount, @labelvalues );
546}
547
548*inc_distribution_by = \&report_distribution;
549
550=head2 Gauge
551
552The L</make_gauge> method creates a new metric which reports on the
553instantaneous value of some measurable quantity. Unlike the other metric types
554this does not have to only increment forwards when certain events occur, but
555can measure a quantity that may both increase and decrease over time; such as
556the number some kind of object in memory, or the size of some data structure.
557
558As an alternative to incrementing or decrementing the value when particular
559events occur, the absolute value of the gauge can also be set directly.
560
561=cut
562
563=head2 make_gauge
564
565   $collector->make_gauge( $handle, %args )
566
567Requests the creation of a new gauge metric.
568
569=cut
570
571sub make_gauge
572{
573   my $self = shift;
574   my ( $handle, %args ) = @_;
575
576   $args{name} = $self->_metricname( $args{name} // [ $handle ] );
577
578   $self->{$handle} and croak "Already have a metric '$handle'";
579   $self->{$handle} = [ gauge => @{ $args{labels} // [] } ];
580
581   $self->_adapter_call( make_gauge => "$self->{package}/$handle",
582      collector => $self,
583      %args
584   );
585}
586
587=head2 inc_gauge
588
589   $collector->inc_gauge( $handle, $labels )
590
591=head2 dec_gauge
592
593   $collector->dec_gauge( $handle, $labels )
594
595=head2 inc_gauge_by
596
597   $collector->inc_gauge_by( $handle, $amount, $labels )
598
599=head2 dec_gauge_by
600
601   $collector->dec_gauge_by( $handle, $amount, $labels )
602
603Reports that the observed value of the gauge has increased or decreased by the
604given amount (or 1).
605
606=cut
607
608sub inc_gauge
609{
610   my $self = shift;
611   my ( $handle, @args ) = @_;
612
613   my @labelvalues = $self->_labelvalues( gauge => $handle, @args );
614
615   $self->adapter->inc_gauge_by( "$self->{package}/$handle", 1, @labelvalues );
616}
617
618sub dec_gauge
619{
620   my $self = shift;
621   my ( $handle, @args ) = @_;
622
623   my @labelvalues = $self->_labelvalues( gauge => $handle, @args );
624
625   $self->adapter->inc_gauge_by( "$self->{package}/$handle", -1, @labelvalues );
626}
627
628sub inc_gauge_by
629{
630   my $self = shift;
631   my ( $handle, $amount, @args ) = @_;
632
633   my @labelvalues = $self->_labelvalues( gauge => $handle, @args );
634
635   $self->adapter->inc_gauge_by( "$self->{package}/$handle", $amount, @labelvalues );
636}
637
638sub dec_gauge_by
639{
640   my $self = shift;
641   my ( $handle, $amount, @args ) = @_;
642
643   my @labelvalues = $self->_labelvalues( gauge => $handle, @args );
644
645   $self->adapter->inc_gauge_by( "$self->{package}/$handle", -$amount, @labelvalues );
646}
647
648=head2 set_gauge_to
649
650   $collector->set_gauge_to( $handle, $amount, $labels )
651
652Reports that the observed value of the gauge is now the given amount.
653
654The C<$handle> name must match one earlier created by L</make_gauge>.
655
656=cut
657
658sub set_gauge_to
659{
660   my $self = shift;
661   my ( $handle, $amount, @args ) = @_;
662
663   my @labelvalues = $self->_labelvalues( gauge => $handle, @args );
664
665   $self->adapter->set_gauge_to( "$self->{package}/$handle", $amount, @labelvalues );
666}
667
668=head2 Timer
669
670The L</make_timer> method creates a new metric which measures durations of
671time consumed by the application. New observations of durations can be added
672by the L</report_timer> method.
673
674Timer metrics may be handled by the adapter similarly to distribution metrics.
675Moreover, adapters may choose to implement timers as distributions with units
676of C<seconds>.
677
678=cut
679
680=head2 make_timer
681
682   $collector->make_timer( $handle, %args )
683
684Requests the creation of a new timer metric.
685
686=cut
687
688sub make_timer
689{
690   my $self = shift;
691   my ( $handle, %args ) = @_;
692
693   $args{name} = $self->_metricname( $args{name} // [ $handle ] );
694
695   $self->{$handle} and croak "Already have a metric '$handle'";
696   $self->{$handle} = [ timer => @{ $args{labels} // [] } ];
697
698   $self->_adapter_call( make_timer => "$self->{package}/$handle",
699      collector => $self,
700      %args
701   );
702}
703
704=head2 report_timer
705
706   $collector->report_timer( $handle, $duration, $labels )
707
708I<Since version 0.05.>
709
710Reports a new duration for the timer metric. The C<$handle> name must match
711one earlier created by L</make_timer>. The C<$duration> gives a time measured
712in seconds, and may be fractional.
713
714This method used to called C<inc_timer_by> and is currently still available as
715an alias.
716
717=cut
718
719sub report_timer
720{
721   my $self = shift;
722   my ( $handle, $duration, @args ) = @_;
723
724   my @labelvalues = $self->_labelvalues( timer => $handle, @args );
725
726   my $adapter = $self->adapter;
727
728   # Support new and legacy name
729   my $method = $adapter->can( "report_timer" ) // "inc_timer_by";
730   $adapter->$method( "$self->{package}/$handle", $duration, @labelvalues );
731}
732
733*inc_timer_by = \&report_timer;
734
735=head1 AUTHOR
736
737Paul Evans <leonerd@leonerd.org.uk>
738
739=cut
740
7410x55AA;
742