File Coverage

blib/lib/OpenTelemetry/Instrumentation/namespace.pm
Criterion Covered Total %
statement 136 147 92.5
branch 41 60 68.3
condition 6 12 50.0
subroutine 21 21 100.0
pod 1 6 16.6
total 205 246 83.3


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.034';
5              
6 2     2   277452 use v5.38;
  2         6  
7 2     2   890 use experimental 'try';
  2         5797  
  2         11  
8              
9 2     2   1148 use Class::Method::Modifiers 'install_modifier';
  2         2819  
  2         110  
10 2     2   2321 use Devel::Peek;
  2         1063  
  2         9  
11 2     2   172 use List::Util 'pairs';
  2         3  
  2         134  
12 2     2   882 use Module::Runtime ();
  2         2794  
  2         49  
13 2     2   811 use OpenTelemetry::Common;
  2         61823  
  2         113  
14 2     2   956 use OpenTelemetry;
  2         127861  
  2         22  
15 2     2   487 use Ref::Util qw( is_regexpref is_coderef is_hashref );
  2         3  
  2         100  
16 2     2   1045 use YAML::PP;
  2         179707  
  2         290  
17              
18 2     2   888 use namespace::clean ();
  2         29822  
  2         117  
19              
20 2     2   15 use parent 'OpenTelemetry::Instrumentation';
  2         3  
  2         32  
21              
22             my $LOGGER = OpenTelemetry::Common::internal_logger;
23             my %CACHE;
24              
25             sub install {
26 3     3 1 275915 my $class = shift;
27 3         27 my ( $rules, $options ) = $class->parse_options(@_);
28              
29 3 100       16 return !!1 unless @$rules;
30              
31             # Loop over loaded modules
32 2         236 for my $module ( keys %INC ) {
33 645 100       1324 if ( lc $module eq $module ) {
34             # $LOGGER->trace("Not auto-instrumenting $module because it is a pragma");
35 46         58 next;
36             }
37              
38 599 50       928 if ( $module =~ /^[0-9]/ ) {
39             # $LOGGER->trace("Not auto-instrumenting $module because it is a version");
40 0         0 next;
41             }
42              
43 599         875 $class->wrap_module( $module, $rules, $options )
44             }
45              
46 2         67 $class->wrap_require( $rules, $options );
47              
48 2         10 return !!1;
49             }
50              
51 2     2 0 4 sub wrap_require ($class, $rules, $options ) {
  2         4  
  2         3  
  2         3  
  2         3  
52 2         6 my $old_hook = ${^HOOK}{require__before};
53             ${^HOOK}{require__before} = sub {
54 75     75   30156 my ($name) = @_;
55              
56 75         126 my $return;
57 75 100       155 $return = $old_hook->($name) if $old_hook;
58              
59             return sub {
60 75 100 66     10487 $return->() if ref $return && is_coderef $return;
61 75 50       175 return unless $INC{$name};
62 75         234 $class->wrap_module($name, $rules, $options);
63 75         5476 };
64 2         16 };
65             }
66              
67 6     6 0 14 sub parse_options ( $class, @config ) {
  6         12  
  6         12  
  6         9  
68 6         30 my %options = (
69             -ignore_constants => 1,
70             -ignore_private => 1,
71             -ignore_import => 1,
72             -prefer_instrumentations => 1,
73             );
74              
75 6         12 my @rules;
76 6         43 for ( pairs @config ) {
77 6 100       32 unless ( $_->[0] =~ /^-/ ) {
78 5         10 push @rules, @$_;
79 5         8 next;
80             }
81              
82 1         5 $options{ $_->[0] } = $_->[1];
83             }
84              
85 6 100       30 if ( my $path = delete $options{-from_file} ) {
86 1         3 try {
87 1         7 $LOGGER->trace("Loading OpenTelemetry namespace configuration from $path");
88 1         58 my $loaded = YAML::PP::LoadFile($path);
89 1 50       2662 @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 6         23 return ( \@rules, \%options );
99             }
100              
101 674     674 0 706 sub wrap_module ( $class, $module, $rules, $options ) {
  674         755  
  674         721  
  674         708  
  674         741  
  674         693  
102 674         1246 my $package = $module =~ s/\//::/gr;
103 674         1300 $package =~ s/\.p[ml]$//;
104              
105 674 50       1096 if ( $package =~ /^::/ ) {
106             # $LOGGER->trace("Not auto-instrumenting $package because it is not a package");
107 0         0 return;
108             }
109              
110 674 100       943 if ( $package =~ /^OpenTelemetry/ ) {
111             # $LOGGER->trace("Not auto-instrumenting $package because it is itself an OpenTelemetry class");
112 38         64 return;
113             }
114              
115             # TODO
116 636 100       1069 if ( $package =~ /^(?:B|Exporter|Test2|Plack|XSLoader)(?:::|$)/ ) {
117             # $LOGGER->trace("Not auto-instrumenting $package because it is not currently supported");
118 281         1112 return;
119             }
120              
121 355 50       638 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         6768 $class->wrap_package( $package, $rules, $options );
140             }
141              
142 355     355 0 381 sub wrap_package ( $class, $package, $rules, $options ) {
  355         383  
  355         394  
  355         414  
  355         369  
  355         368  
143             # Check the assumed package of the module against each package rule
144 355         1005 for my $pair ( pairs @$rules ) {
145 709         1223 my ( $matcher, $rules ) = @$pair;
146              
147             # If this rule does not apply to this package
148             # move to the next rule
149 709 100       2178 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       11 return unless $rules;
156              
157 3         22 $class->wrap_subroutines( $package, $rules, $options )
158             }
159             }
160              
161 6     6 0 11 sub wrap_subroutines ( $class, $package, $rules, $options ) {
  6         9  
  6         10  
  6         9  
  6         9  
  6         7  
162 6         41 my $default_rules = [ qr/.*/ => 1 ];
163              
164             # Normalise rules
165 6 100       15 $rules = $default_rules unless ref $rules;
166 6 50       12 $rules = [ %$rules ] if is_hashref $rules;
167              
168 6         44 my $subs = namespace::clean->get_functions($package);
169              
170             # The package rule has matched
171             # Loop over the subroutines in the package
172 6         470 SUB: while ( my ( $subname, $coderef ) = each %$subs ) {
173 18         1382 my $fullname = "${package}::${subname}";
174              
175 18 50 66     42 if ( $subname =~ /^(?:un)?import$/ && $options->{-ignore_import} ) {
176             # $LOGGER->trace(
177             # "Not auto-instrumenting $fullname because -ignore_import was set",
178             # );
179 1         4 next;
180             }
181              
182 17 0 33     58 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 17 0 33     33 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 17 50       73 if ( my $gv = Devel::Peek::CvGV($coderef) ) {
199 17 100       51 if ( *$gv{PACKAGE} ne $package ) {
200             # $LOGGER->trace(
201             # "Not auto-instrumenting $fullname because it is imported from a different package"
202             # );
203 3         10 next;
204             }
205             }
206              
207 14 100       34 if ( defined prototype $coderef ) {
208             # $LOGGER->trace(
209             # "Not auto-instrumenting $fullname because it has a prototype"
210             # );
211 4         10 next;
212             }
213              
214 10         47 for ( pairs @$rules ) {
215 13         249 my ( $matcher, $spanner ) = @$_;
216              
217 13 100       104 next unless $subname =~ $matcher;
218 9 50       32 next SUB unless $spanner;
219              
220             # Avoid double-wrapping subs
221 9 50       39 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         50 "Adding OpenTelemetry auto-instrumentation for $fullname"
230             );
231              
232             $spanner = sub {
233 8     8   19 my ( $package, $subname, $orig, @args ) = @_;
234             OpenTelemetry
235             ->tracer_provider
236             ->tracer( name => $package, version => $package->VERSION )
237             ->in_span(
238 8         13232 "${package}::${subname}" => sub { $orig->(@args) },
239 8         57 );
240 9 50       361 } unless is_coderef $spanner;
241              
242             install_modifier $package => around => $subname => sub {
243 8     8   53446 local @_ = ( $package, $subname, @_ );
244 8         27 goto $spanner;
245 9         71 };
246             }
247             }
248             }
249              
250             1;