The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

OpenTracing::Manual::Implementation - For Tracing Service Implementations

DESCRIPTION

This part of the OpenTracing::Manual will describe how communication with the backend is established through some sort of Agent. This manual also provides information on how to extract_context or inject_context and carriers.

TABLE OF CONTENTS

"Bootstrapping a Tracer Implementation"
"Writing your own Implementation and using Roles"
"Adding Implementation Specific Information to Traces"
"Sending Span Information to a Service Provider Backend"
"Propagating Tracer Information between Services"
"OpenTracing Roles and Types"
"Testing your Implementation"

INTRODUCTION

OpenTracing merely describes the API, see the OpenTracing::Interface documentation. It only requires that any implementation has a minimal set of methods that have a signature, or defined argument list. It is a deliberate choice to have the specification as POD and leaving the implementation to the Service Provider. The OpenTracing SDK for Perl however, comes with quite some useful tools to help building your own.

THE DETAILS

Bootstrapping a Tracer Implementation

Because of directory structure, Perl best practices and more, an implementation consists of several files, grouped under a single namespace. However, the API has no higher level definition of what an implementation is, it only speaks of the Tracer being the entry point of the API. It only looks more natural to be able to do things like:

    use "OpenTracing::Implementation qw/YourServiceProvider/;

Which bootstraps the OpenTracing::GlobalTracer.

Or be more specific in your own code:

    use aliased
        "OpenTracing::Implementation::MyServiceProvider",
        "Implementation" ;
    
    my $tracer = Implementation->bootstrap_tracer( %options );

Although the 'Implementation' could do all sorts of things with that call, it basically is the same as:

    use aliased
        "OpenTracing::Implementation::MyServiceProvider::Tracer" ;
    
    my $tracer = Tracer->new( %options );

Writing your own Implementation and using Roles

Since a lot of the responsibilities described in the <OpenTracing::Interface> are common across all implementations, there is a whole set of Moo::Roles files to quickly build your own classes.

    package OpenTracing::Implementation::MyServiceProvider::Scope
    
    use Moo;
    
    ...
    
    with 'OpenTracing::Role::Scope'
    
    1;

Look at OpenTracing::Roles to see what each of those roles provides.

Adding Implementation Specific Information to Traces.

The OpenTracing::Interface::SpanContext carries data across process boundaries. Specifically, it has two major components:

An implementation-dependent state to refer to the distinct span within a trace

for example the implementing Tracer's definition of spanID and traceID

Any Baggage Items

These are key:value pairs that cross process-boundaries. These may be useful to have some data available for access throughout the trace (https://opentracing.io/docs/overview/tags-logs-baggage/#baggage-items).

Depending on the purpose, it is most likely that you want to add additional information like a ServiceEndpoint to the SpanContext as 'private' attributes. As an implementor you do want to have a reliable way to persist that information. The BaggageItems can be altered at application level, as they are part of the 'public' API.

    package OpenTracing::Implementation::MyServiceProvider::SpanContext
    
    use Moo;
    
    with 'OpenTracing::Role::SpanContext'
    
    has service_endpoint => (
        is      => 'ro',
        default => { 'index.cgi' },
        isa     =>  Str,
    );
    
    1;

As implementor, it's your own responsibility to send that information back to the service provider.

Sending Span Information to a Service Provider Backend

How information is being send back to a service provider backend is beyond the scope of this manual. There are different scenarios to do so. Some may want to collect a larger number of spans and send those straight to the backend. Others may have a locally installed agent that will gather spans coming from multiple threads and send them as a batch to the backend.

Either way, as a implementor, you will need to add to the Tracer a send method that will communicate with the outer world.

    package OpenTracing::Implementation::MyServiceProvider::Tracer
    
    use Moo;
    
    with 'OpenTracing::Role::Tracer'
    
    has your_agent => (
        is      => 'lazy',
        isa     => 'OpenTracing::Implementation::MyServiceProvider::Agent',
        handles => qw/send_the_span/,
    );
    
    1;

Then, at the time you call finish, calling such method as mentioned (send_the_span) in the above example through a call back added as a on_finish attribute, would transmit the span.

Propagating Tracer Information between Services

At the boundary or edges of an application, Frameworks use the two methods inject_context and extract_context (https://opentracing.io/docs/overview/tracers/#propagating-a-trace-with-inject-extract).

It is required that these methods are provided in the implementation. The official documentation requires the use of a Carrier Format so that is clear what is exactly expected to happen within these inject_context and extract_context methods. But since this is Perl, it is possible to inspect the given carrier and check the type of the reference.

Perl implementations are expected handle the following carrier Type::Tiny types:

The possible solution might be implemented like:

    package OpenTracing::Implementation::MyServiceProvider::Tracer;
    
    # dispatch simple calls into seperate methods per type
    
    sub inject_context ($self, $carrier, $context) {
        
        ArrayRef->check($carrier) and return
            $self->inject_context_into_array_reference($carrier, $context);
        
        HashRef->check($carrier) and return
            $self->inject_context_into_hash_reference($carrier, $context);
        
        (InstanceOf['HTTP::Headers'])->check($carrier) and return
            $self->inject_context_into_http_headers($carrier, $context);
        
        return $carrier
    }
    
    sub extract_context ($self, $carrier) {
        ...
        return undef
    }
    
    #dispatched methods
    
    sub inject_context_into_array_reference  { ... }
    
    sub inject_context_into_hash_reference ($self, $carrier, $context) {
        return Hash::Merge->new('RIGHT_PRECEDENT')->merge(
            $carrier,
            {
                opentracing_context => {
                    trace_id  => $context->trace_id,
                    span_id   => $context->span_id,
                }
                 
            }
        )
    }
    
    sub inject_context_into_http_headers ($self, $carrier, $context) {
        return $carrier->clone->push_header(
            X_YOUR_IMPLEMENTATION_TRACE_ID => $context->trace_id,
            X_YOUR_IMPLEMENTATION_SPAN_ID  => $context->span_id,
        )
    }
    
    sub extract_context_from_array_reference { ... }
    
    sub extract_context_from_hash_reference ($self, $carrier) {
        $self->maybe_build_context_with( $carrier->{ opentracing_context } )
    }
    
    sub extract_context_from_http_headers ($self, $carrier) {
        $self->maybe_build_context_with(
            trace_id => $carrier->header('X_YOUR_IMPLEMENTATION_TRACE_ID'),
            span_id  => $carrier->header('X_YOUR_IMPLEMENTATION_SPAN_ID' ),
    }
    
    sub maybe_build_context_with ($self, %context_args) {
        return unless ( $context_args{trace_id} && $context_args{span_id};
        return $self->build_context_with(%context_args)
    }
    # This may return undef iff the carrier can be understood, and no relevant
    # information could be detected. This usually happens at incomming request
    # that are not part of a ditributed service.
    
    sub build_context_with ($self, %context_args) { ... }

Where the X_YOUR_IMPLEMENTATION_TRACE_ID is fully provider dependent. The other (micro) service you want to talk may be implemented using a complete different technology stack or language. But since (most likely) that service will use the same Distributed Tracing Backend, it expects the carrier to hold the trace information in a known format.

OpenTracing Roles and Types

The entire API is described in POD, it also provides a set of Roles. These roles can optionally be consumed to do all the type-checking for parameters, options, and returned results.

It also provides OpenTracing::Types. These duck-type checking types check that a object will at least have the methods described in the API. A isa check will dictate a subclassing, which is what is deliberately avoided.

Testing your Implementation

There are a few tests available for Implementation developers. Those will check that the implementation is at least compliant with the OpenTracing::Interface and can be found at Test::OpenTracing::Interface.

    use Test::Most;
    use Test::OpenTracing::Interface::Span;
    
    use YourImplementation::Span;
    
    my $test_span = new_ok( 'YourImplementation::Span' => { %options },
        "Created a Span object"
    );
    
    interface_can_ok( $test_span,
        "... and can do all the required methods defined"
    );
    
    interface_lives_ok( $test_span,
        "... and each method accepts described parameters and options"
    );
    
    interface_dies_ok( $test_span,
        "... and will not tollerate bad input"
    );

The latter one should work, but only if your implementation does do some sort of checking.

WARNING: If you do not check for the parameters and their types, please do check manually that the child_of and references options are mutual exclusive in start_active_span and start_span

Testing that your implementation is executing the inject_context and extract_context correctly, is entirely up to you. Also, it is up to you to check that the correct span information is being send to the tracer backend at finish.

SEE ALSO

OpenTracing::Interface

A role that defines the Tracer interface.

OpenTracing::Manual

A quick overview about Perl5 and OpenTracing

OpenTracing::Manual::Instrumentation

For Application developers and Devops.

OpenTracing::Manual::Integration

For Framework or Integration Developers

OpenTracing::Manual::Ecosystem

An overview of the OpenTracing puzzle pieces.

OpenTracing Overview

The OpenTracing API standard.

AUTHOR

Theo van Hoesel <tvanhoesel@perceptyx.com>

COPYRIGHT AND LICENSE

'OpenTracing API for Perl' is Copyright (C) 2019 .. 2020, Perceptyx Inc

This library is free software; you can redistribute it and/or modify it under the terms of the Artistic License 2.0.

This library is distributed in the hope that it will be useful, but it is provided "as is" and without any express or implied warranties.

For details, see the full text of the license in the file LICENSE.