File Coverage

blib/lib/Log/Any/Adapter/OpenTelemetry.pm
Criterion Covered Total %
statement 49 54 90.7
branch 6 8 75.0
condition 4 5 80.0
subroutine 13 14 92.8
pod 0 1 0.0
total 72 82 87.8


line stmt bran cond sub pod time code
1             package Log::Any::Adapter::OpenTelemetry;
2              
3 1     1   987 use strict;
  1         3  
  1         83  
4 1     1   7 use warnings;
  1         4  
  1         78  
5 1     1   8 use experimental 'signatures';
  1         2  
  1         9  
6              
7             our $VERSION = '0.033';
8              
9 1     1   219 use Log::Any::Adapter::Util ();
  1         2  
  1         41  
10 1     1   16 use OpenTelemetry qw( otel_config otel_span_from_context otel_logger_provider );
  1         3  
  1         17  
11 1     1   144 use Ref::Util 'is_hashref';
  1         2  
  1         78  
12 1     1   8 use Time::HiRes 'time';
  1         3  
  1         9  
13              
14 1         11 use OpenTelemetry::Constants qw(
15             LOG_LEVEL_TRACE
16             LOG_LEVEL_DEBUG
17             LOG_LEVEL_INFO
18             LOG_LEVEL_WARN
19             LOG_LEVEL_ERROR
20             LOG_LEVEL_FATAL
21 1     1   101 );
  1         3  
22              
23 1     1   1593 use base 'Log::Any::Adapter::Base';
  1         3  
  1         368  
24              
25             my %LOG2OTEL = (
26             trace => LOG_LEVEL_TRACE,
27             debug => LOG_LEVEL_DEBUG,
28             info => LOG_LEVEL_INFO,
29             warn => LOG_LEVEL_WARN,
30             error => LOG_LEVEL_ERROR,
31             fatal => LOG_LEVEL_FATAL,
32             );
33              
34             my %OTEL2LOG = (
35             trace => 8,
36             debug => 7,
37             info => 6,
38             warn => 4,
39             error => 3,
40             fatal => 2,
41             );
42              
43             sub init ( $self, @ ) {
44             # FIXME: It would be good to get a logger early and cache
45             # it for eventual calls. However, this suffers from the same
46             # issue with caching tracers that is documented in the POD
47             # for OpenTelemetry::Trace::Tracer: namely, that if we get
48             # the no-op logger before we've set up a real logger provider
49             # that can generate real loggers, we'll be stuck with a no-op.
50             # It might be that we need to revisit the proxy classes removed
51             # in d9e321bd1bf65d510b12ef34fe2b5a0c51da0bf2, although the
52             # rationale for why they were removed is still sound. We'd just
53             # have to come up with a way to make sure its delegate continues
54             # to point to the right place even if the tracer provider changes
55             # $self->{logger} = otel_logger_provider->logger;
56             }
57              
58             for my $method ( Log::Any::Adapter::Util::logging_methods() ) {
59 1     1   10 no strict 'refs';
  1         3  
  1         235  
60 0     0   0 *$method = sub ( $self, @args) {
  0         0  
  0         0  
  0         0  
61 0         0 $self->structured( $method, $self->category, @args );
62             };
63             }
64              
65             for my $method ( Log::Any::Adapter::Util::detection_methods() ) {
66             my $numeric = Log::Any::Adapter::Util::numeric_level( $method =~ s/^is_//r );
67              
68 1     1   11 no strict 'refs';
  1         4  
  1         720  
69             *$method = sub {
70 67   100 67   232359 my $level = $OTEL2LOG{ lc( otel_config('LOG_LEVEL') // 'info' ) };
71 67   66     238 $numeric <= ( $level // $OTEL2LOG{info} );
72             };
73             }
74              
75 47     47 0 1110 sub structured ( $self, $method, $category, @parts ) {
  47         74  
  47         97  
  47         64  
  47         74  
  47         61  
76 47         65 my $level = $method;
77 47         112 for ($level) {
78 47         214 s/(?:emergency|alert|critical)/fatal/;
79 47         91 s/notice/info/;
80 47         107 s/warning/warn/;
81             }
82              
83             # FIXME: This is a little finicky. The aim is for the first
84             # argument to be the body (even if it is structured), and
85             # anything else gets put into the attributes. If the log
86             # comes with structured data that is not a hash, we put it
87             # under a `payload` key. Maybe this can be simplified to
88             # always put the data under a given key, but then we add
89             # data to the arguably common operation of attaching a hash.
90 47         132 my %args = ( body => shift @parts );
91              
92 47 50       115 $args{attributes} = @parts == 1
    50          
    100          
    100          
93             ? is_hashref $parts[0]
94             ? $parts[0] : { payload => $parts[0] }
95             : @parts % 2
96             ? { payload => \@parts } : { @parts }
97             if @parts;
98              
99             otel_logger_provider->logger->emit_record(
100             timestamp => time,
101             severity_text => $method,
102 47         147 severity_number => 0+$LOG2OTEL{$level},
103             %args,
104             );
105             }
106              
107             1;