File Coverage

blib/lib/OpenTelemetry/Instrumentation/namespace.pm
Criterion Covered Total %
statement 133 147 90.4
branch 38 60 63.3
condition 5 12 41.6
subroutine 21 21 100.0
pod 1 6 16.6
total 198 246 80.4


line stmt bran cond sub pod time code
1             package OpenTelemetry::Instrumentation::namespace;
2             # ABSTRACT: OpenTelemetry instrumentation for a namespace
3              
4             our $VERSION = '0.033';
5              
6 2     2   477347 use v5.38;
  2         9  
7 2     2   1397 use experimental 'try';
  2         10571  
  2         16  
8              
9 2     2   1943 use Class::Method::Modifiers 'install_modifier';
  2         4770  
  2         163  
10 2     2   3741 use Devel::Peek;
  2         1517  
  2         12  
11 2     2   361 use List::Util 'pairs';
  2         4  
  2         225  
12 2     2   1298 use Module::Runtime ();
  2         5273  
  2         84  
13 2     2   1362 use OpenTelemetry::Common;
  2         106439  
  2         141  
14 2     2   1383 use OpenTelemetry;
  2         216682  
  2         32  
15 2     2   965 use Ref::Util qw( is_regexpref is_coderef is_hashref );
  2         6  
  2         162  
16 2     2   1471 use YAML::PP;
  2         322029  
  2         401  
17              
18 2     2   1408 use namespace::clean ();
  2         52370  
  2         340  
19              
20 2     2   22 use parent 'OpenTelemetry::Instrumentation';
  2         5  
  2         42  
21              
22             my $LOGGER = OpenTelemetry::Common::internal_logger;
23             my %CACHE;
24              
25             sub install {
26 3     3 1 500263 my $class = shift;
27 3         17 my ( $rules, $options ) = $class->parse_options(@_);
28              
29 3 100       20 return !!1 unless @$rules;
30              
31             # Loop over loaded modules
32 2         335 for my $module ( keys %INC ) {
33 645 100       1832 if ( lc $module eq $module ) {
34             # $LOGGER->trace("Not auto-instrumenting $module because it is a pragma");
35 46         104 next;
36             }
37              
38 599 50       1470 if ( $module =~ /^[0-9]/ ) {
39             # $LOGGER->trace("Not auto-instrumenting $module because it is a version");
40 0         0 next;
41             }
42              
43 599         1258 $class->wrap_module( $module, $rules, $options )
44             }
45              
46 2         158 $class->wrap_require( $rules, $options );
47              
48 2         11 return !!1;
49             }
50              
51 2     2 0 5 sub wrap_require ($class, $rules, $options ) {
  2         5  
  2         3  
  2         4  
  2         2  
52 2         8 my $old_hook = ${^HOOK}{require__before};
53             ${^HOOK}{require__before} = sub {
54 75     75   48458 my ($name) = @_;
55              
56 75         154 my $return;
57 75 100       270 $return = $old_hook->($name) if $old_hook;
58              
59             return sub {
60 75 100 66     18933 $return->() if ref $return && is_coderef $return;
61 75 50       288 return unless $INC{$name};
62 75         395 $class->wrap_module($name, $rules, $options);
63 75         7592 };
64 2         22 };
65             }
66              
67 5     5 0 18 sub parse_options ( $class, @config ) {
  5         12  
  5         15  
  5         11  
68 5         37 my %options = (
69             -ignore_constants => 1,
70             -ignore_private => 1,
71             -ignore_import => 1,
72             -prefer_instrumentations => 1,
73             );
74              
75 5         11 my @rules;
76 5         57 for ( pairs @config ) {
77 5 100       40 unless ( $_->[0] =~ /^-/ ) {
78 4         11 push @rules, @$_;
79 4         12 next;
80             }
81              
82 1         7 $options{ $_->[0] } = $_->[1];
83             }
84              
85 5 100       38 if ( my $path = delete $options{-from_file} ) {
86 1         3 try {
87 1         10 $LOGGER->trace("Loading OpenTelemetry namespace configuration from $path");
88 1         89 my $loaded = YAML::PP::LoadFile($path);
89 1 50       4207 @rules = ( is_hashref($loaded) ? %$loaded : @$loaded, @rules );
90             }
91             catch ($e) {
92 0         0 $LOGGER->warn(
93             "Could not load configuration for OpenTelemetry namespace instrumentation: $e"
94             );
95             }
96             }
97              
98 5         31 return ( \@rules, \%options );
99             }
100              
101 674     674 0 998 sub wrap_module ( $class, $module, $rules, $options ) {
  674         1084  
  674         1011  
  674         998  
  674         970  
  674         925  
102 674         2272 my $package = $module =~ s/\//::/gr;
103 674         2180 $package =~ s/\.p[ml]$//;
104              
105 674 50       1567 if ( $package =~ /^::/ ) {
106             # $LOGGER->trace("Not auto-instrumenting $package because it is not a package");
107 0         0 return;
108             }
109              
110 674 100       1425 if ( $package =~ /^OpenTelemetry/ ) {
111             # $LOGGER->trace("Not auto-instrumenting $package because it is itself an OpenTelemetry class");
112 38         81 return;
113             }
114              
115             # TODO
116 636 100       1768 if ( $package =~ /^(?:B|Exporter|Test2|Plack|XSLoader)(?:::|$)/ ) {
117             # $LOGGER->trace("Not auto-instrumenting $package because it is not currently supported");
118 281         1730 return;
119             }
120              
121 355 50       1029 if ( my $instrumentation = $class->for_package($package) ) {
122 0 0       0 if ( $options->{-prefer_instrumentations} ) {
123             # $LOGGER->trace(
124             # "Not auto-instrumenting $package because $instrumentation is installed in this system. You can ignore this by disabling -prefer_instrumentations"
125             # );
126              
127 0         0 return;
128             }
129              
130 0         0 my $notional = Module::Runtime::module_notional_filename($instrumentation);
131 0 0       0 if ( $INC{$notional} ) {
132             # $LOGGER->trace(
133             # "Not auto-instrumenting $package because $instrumentation has already been loaded"
134             # );
135 0         0 return;
136             }
137             }
138              
139 355         8648 $class->wrap_package( $package, $rules, $options );
140             }
141              
142 355     355 0 608 sub wrap_package ( $class, $package, $rules, $options ) {
  355         543  
  355         558  
  355         541  
  355         512  
  355         574  
143             # Check the assumed package of the module against each package rule
144 355         1998 for my $pair ( pairs @$rules ) {
145 709         1857 my ( $matcher, $rules ) = @$pair;
146              
147             # If this rule does not apply to this package
148             # move to the next rule
149 709 100       3314 next if is_regexpref($matcher)
    100          
150             ? $package !~ $matcher
151             : $package ne $matcher;
152              
153             # Since this rule applies to this package,
154             # we abort if this rule is 'ignore'
155 4 100       18 return unless $rules;
156              
157 3         14 $class->wrap_subroutines( $package, $rules, $options )
158             }
159             }
160              
161 5     5 0 27 sub wrap_subroutines ( $class, $package, $rules, $options ) {
  5         12  
  5         13  
  5         12  
  5         12  
  5         10  
162 5         30 my $default_rules = [ qr/.*/ => 1 ];
163              
164             # Normalise rules
165 5 100       22 $rules = $default_rules unless ref $rules;
166 5 50       19 $rules = [ %$rules ] if is_hashref $rules;
167              
168 5         134 my $subs = namespace::clean->get_functions($package);
169              
170             # The package rule has matched
171             # Loop over the subroutines in the package
172 5         619 SUB: while ( my ( $subname, $coderef ) = each %$subs ) {
173 10         1241 my $fullname = "${package}::${subname}";
174              
175 10 0 33     36 if ( $subname =~ /^(?:un)?import$/ && $options->{-ignore_import} ) {
176             # $LOGGER->trace(
177             # "Not auto-instrumenting $fullname because -ignore_import was set",
178             # );
179 0         0 next;
180             }
181              
182 10 0 33     33 if ( uc($subname) eq $subname && $options->{-ignore_constants} ) {
183             # $LOGGER->trace(
184             # "Not auto-instrumenting $fullname because -ignore_constants was set",
185             # );
186 0         0 next;
187             }
188              
189 10 0 33     30 if ( $subname =~ /^_/ && $options->{-ignore_private} ) {
190             # $LOGGER->trace(
191             # "Not auto-instrumenting $fullname because -ignore_private was set",
192             # );
193 0         0 next;
194             }
195              
196             # Skip imported functions.
197             # See https://stackoverflow.com/a/3685262/807650
198 10 50       142 if ( my $gv = Devel::Peek::CvGV($coderef) ) {
199 10 50       55 if ( *$gv{PACKAGE} ne $package ) {
200             # $LOGGER->trace(
201             # "Not auto-instrumenting $fullname because it is imported from a different package"
202             # );
203 0         0 next;
204             }
205             }
206              
207 10 50       34 if ( defined prototype $coderef ) {
208             # $LOGGER->trace(
209             # "Not auto-instrumenting $fullname because it has a prototype"
210             # );
211 0         0 next;
212             }
213              
214 10         71 for ( pairs @$rules ) {
215 13         576 my ( $matcher, $spanner ) = @$_;
216              
217 13 100       216 next unless $subname =~ $matcher;
218 9 50       37 next SUB unless $spanner;
219              
220             # Avoid double-wrapping subs
221 9 50       44 if ( $CACHE{$package}{$subname}++ ) {
222             # $LOGGER->trace(
223             # "Not auto-instrumenting $fullname because we have already done so"
224             # );
225 0         0 next;
226             }
227              
228             $LOGGER->info(
229 9         66 "Adding OpenTelemetry auto-instrumentation for $fullname"
230             );
231              
232             $spanner = sub {
233 8     8   28 my ( $package, $subname, $orig, @args ) = @_;
234             OpenTelemetry
235             ->tracer_provider
236             ->tracer( name => $package, version => $package->VERSION )
237             ->in_span(
238 8         19540 "${package}::${subname}" => sub { $orig->(@args) },
239 8         77 );
240 9 50       490 } unless is_coderef $spanner;
241              
242             install_modifier $package => around => $subname => sub {
243 8     8   75946 local @_ = ( $package, $subname, @_ );
244 8         35 goto $spanner;
245 9         75 };
246             }
247             }
248             }
249              
250             1;