package OpenTracing::WrapScope; our $VERSION = 'v0.107.3'; use strict; use warnings; use warnings::register; use feature qw[ state ]; use B::Hooks::OP::Check::LeaveEval; use Caller::Hide qw/hide_package/; use Carp qw/croak/; use List::Util qw/uniq/; use OpenTracing::GlobalTracer; use PerlX::Maybe; use Scalar::Util qw/blessed/; use Sub::Info qw/sub_info/; hide_package(__PACKAGE__); my %subs_to_install; END { foreach my $sub (keys %subs_to_install) { next unless $subs_to_install{$sub}{warn_undetected}; warnings::warn "OpenTracing::WrapScope couldn't find sub: $sub"; } } sub _register_to_install { my ($signature, %args) = @_; my $warn_undetected = $args{warn_undetected}; my ($sub) = _split_signature($signature); $subs_to_install{$sub} = {%args, signature => $signature }; return; } sub _split_signature { my ($sig) = @_; return $sig unless $sig =~ s/\s*\((.*)\)\s*\z//; return ($sig, $1); # just the name left over } # try to install any available subs whenever we get new code B::Hooks::OP::Check::LeaveEval::register(sub { return unless %subs_to_install; foreach my $sub (keys %subs_to_install) { next unless defined &$sub; install_wrapped($subs_to_install{$sub}{signature}); delete $subs_to_install{$sub}; } return; }); sub import { shift; # __PACKAGE__ my $target_package = caller; my $warn_undetected = 1; my ($use_env, @subs, @files); while (my (undef, $arg) = each @_) { if ($arg eq '-env') { $use_env = 1; } elsif ($arg eq '-file') { my (undef, $next) = each @_ or last; push @files, ref $next eq 'ARRAY' ? @$next : $next; } elsif ($arg eq '-quiet') { $warn_undetected = 0; } else { push @subs, _qualify_sub($arg, $target_package); } } if ($use_env and $ENV{OPENTRACING_WRAPSCOPE_FILE}) { push @files, split ':', $ENV{OPENTRACING_WRAPSCOPE_FILE}; } push @subs, map { _load_sub_spec($_) } grep { -f } map { glob } uniq @files; foreach my $sub (@subs) { _register_to_install($sub, warn_undetected => $warn_undetected); } return; } sub install_wrapped { foreach my $sub (@_) { my ($sub_name, $args) = _split_signature($sub); my $full_sub = _qualify_sub($sub_name, scalar caller); if (not defined &$full_sub) { warnings::warn "Couldn't find sub: $full_sub"; next; } my $wrapped = wrapped(\&$full_sub, $args); my ($class, $method) = split /(?:'|::)(?=\w+\z)/, $full_sub; if (_is_moose_class($class)) { # Moose complains about replaced subs if ($class->meta->is_immutable) { warnings::warn "Can't wrap Moose method $sub from an immutable class"; next; } $class->meta->add_method($method => $wrapped); } else { no strict 'refs'; no warnings 'redefine'; *$full_sub = $wrapped; } } return; } sub _is_moose_class { my ($class) = @_; my $meta = eval { $class->meta } or return; return blessed $meta && $meta->isa('Moose::Meta::Class'); } sub wrapped { my ($coderef, $signature) = @_; my $info = sub_info($coderef); my @tag_generators = _parse_signature($signature); return sub { my ($call_package, $call_filename, $call_line) = caller(0); my $call_sub = (caller(1))[3]; my $tracer = OpenTracing::GlobalTracer->get_global_tracer; my $scope = $tracer->start_active_span( "$info->{package}::$info->{name}", tags => { 'source.subname' => $info->{name}, 'source.file' => $info->{file}, 'source.line' => $info->{start_line}, 'source.package' => $info->{package}, maybe 'caller.subname' => $call_sub, 'caller.file' => $call_filename, 'caller.line' => $call_line, 'caller.package' => $call_package, map { $_->(@_) } @tag_generators, }, ); my $result; my $wantarray = wantarray; # eval will have its own my $ok = eval { if (defined $wantarray) { $result = $wantarray ? [&$coderef] : &$coderef; } else { &$coderef; } 1; }; # TODO: message should go to logs but we don't have those yet $scope->get_span->add_tags(error => 1, message => "$@") unless $ok; $scope->close(); die $@ unless $ok; return if not defined wantarray; return wantarray ? @$result : $result; }; } sub _is_qualified { $_[0] =~ /\A\w+(?:'|::)/ } sub _qualify_sub { my ($sub, $pkg) = @_; return $sub if _is_qualified($sub); $sub =~ s/\A(\w+)/${pkg}::$1/; return $sub; } sub _load_sub_spec { my ($filename) = @_; open my $fh_subs, '<', $filename or die "$filename: $!"; my @subs; while (<$fh_subs>) { chomp; s/\s*#.*\Z//; # remove comments next unless $_; croak "Unqualified subroutine: $_" unless _is_qualified($_); push @subs, $_; } close $fh_subs; return @subs; } sub wrap_from_file { my ($filename) = @_; install_wrapped( _load_sub_spec($filename) ); return; } { my $gr = qr{ (?(DEFINE) (? \s* , \s* ) (? [0-9]+ ) (? (?&INT) \s* \.\. \s* (?&INT) ) (? (?&RANGE) | (?&INT) ) (? '[^']*' | "[^"]*" ) (? \s* (?&AS_ARG) (?: (?&COMMA) (?&AS_ARG) )* \s* ) (? \s* (?&HS_ARG) (?: (?&COMMA) (?&HS_ARG) )* \s* ) ) }x; sub _parse_signature { my ($sig) = @_; return unless $sig; state $types = { '$' => { greedy => 0, generator => \&_gen_scalar }, '%' => { greedy => 1, generator => \&_gen_hash }, '@' => { greedy => 1, generator => \&_gen_array }, '\%' => { greedy => 0, generator => \&_gen_hash_ref }, '\@' => { greedy => 0, generator => \&_gen_array_ref }, }; state $re = qr{ (?: \A \s* | (?&COMMA) ) (?: (? undef ) | (? \$ ) (? \w+ ) | (? (?:\\)? % ) (? \w+ ) (?: \{ (? (?&HASH_SLICE) ) \} )? | (? (?:\\)? @ ) (? \w+ ) (?: \[ (? (?&ARRAY_SLICE) ) \] )? ) $gr }x; my @tag_generators; my $arg_idx = 0; while ($sig =~ /\G$re/xcg) { my ($type, $name, $slice) = @+{qw[ type name slice ]}; next if $type eq 'undef'; # hidden argument my $slice_ref = _parse_slice($type, $slice); my $type_ref = $types->{$type} or die "No such type: $type"; my $generator = $type_ref->{generator}->($name, $arg_idx, $slice_ref); push @tag_generators, $generator; last if $type_ref->{greedy}; } continue { ++$arg_idx; } my $pos = pos($sig) // 0; if ($pos != length($sig)) { Carp::croak "Failed to parse signature:\n$sig\n", ' ' x $pos, '^'; } return @tag_generators; } sub _parse_slice { my ($type, $slice) = @_; return unless $type and $slice; if ($type eq '@' or $type eq '\@') { my @args; while ($slice =~ /((?&AS_ARG)) $gr/gx) { my $arg = $1; push @args, $arg =~ /(\d+)\s*\.\.\s(\d+)/ ? $1 .. $2 # expand ranges : $arg; } return \@args; } if ($type eq '%' or $type eq '\%') { my @args; while ($slice =~ /((?&HS_ARG)) $gr/gx) { push @args, $1 =~ s/\A['"]|['"]\z//gr; # remove quotes } return \@args; } return; } } # undefs and references break OpenTracing::Interface type constraints sub _str { defined $_[0] ? "$_[0]" : 'undef' } sub _gen_scalar { my ($name, $arg_idx) = @_; return sub { return if $arg_idx > $#_; return ("arguments.$name" => _str($_[$arg_idx])); }; } sub _gen_hash { my ($name, $arg_idx, $slice_ref) = @_; return sub { return if $arg_idx > $#_; no warnings 'misc'; # odd-sized list, etc. these are not our fault my %hash = @_[ $arg_idx .. $#_ ]; return map {; "arguments.$name.$_" => _str($hash{$_}) } keys %hash unless $slice_ref; @$slice_ref = grep { exists $hash{$_} } @$slice_ref; my %sliced; @sliced{@$slice_ref} = @hash{@$slice_ref}; return map { ; "arguments.$name.$_" => _str($sliced{$_}) } keys %sliced; }; } sub _gen_array { my ($name, $arg_idx, $slice_ref) = @_; return sub { return if $arg_idx > $#_; my @args = @_[ $arg_idx .. $#_ ]; return map {; "arguments.$name.$_" => _str($args[$_]) } $slice_ref ? grep { $_ <= $#args } @$slice_ref : keys @args; }; } sub _gen_hash_ref { my ($name, $arg_idx, $slice_ref) = @_; return sub { return if $arg_idx > $#_; return if ref $_[$arg_idx] ne 'HASH'; no warnings 'misc'; # odd-sized list, etc. these are not our fault my %hash = %{ $_[$arg_idx] }; return map {; "arguments.$name.$_" => _str($hash{$_}) } keys %hash unless $slice_ref; @$slice_ref = grep { exists $hash{$_} } @$slice_ref; my %sliced; @sliced{@$slice_ref} = @hash{@$slice_ref}; return map {; "arguments.$name.$_" => _str($sliced{$_}) } keys %sliced; }; } sub _gen_array_ref { my ($name, $arg_idx, $slice_ref) = @_; return sub { return if $arg_idx > $#_; return if ref $_[$arg_idx] ne 'ARRAY'; my @args = @{ $_[$arg_idx] }; return map {; "arguments.$name.$_" => _str($args[$_]) } $slice_ref ? grep { $_ <= $#args } @$slice_ref : keys @args; }; } 1;