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