The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

OpenTracing::Manual::Integration - For Framework or Integration Developers

DESCRIPTION

This part of the OpenTracing::Manual will describe how distributed traces are progressed from one service to the other, using the concept of carriers.

TABLE OF CONTENTS

"Bootstrap an Implementation"
"OpenTracing Carriers"
"Extracting Context from an Incoming Request"
"Inject Context into an Outgoing Request"
"Testing your Framework Plugins"

INTRODUCTION

Most of the time, a framework will have some sort of architecture that allows to add plugins into the framework itself. These plugins, conveniently, bootstrap each application running the framework. But with OpenTracing, backends can be easily swapped, with only minor changes in the code itself. All calls should follow the API.

Any framework plugin has the following responsibilities:

Bootstrap an Implementation and GlobalTracer
Extract any existing trace information to create a Context

On the other hand, on the outgoing side of the server, the responsibility is to:

Inject the current Context into a 'Carrier'

THE DETAILS

Bootstrap an Implementation

Plugins should either use a generic way to bootstrap an implementation, or tailored for just one implementation.

    use MyFramework::Plugin::OpenTracing;
    #
    # Bootstrap implementation from the `OPENTRACING_IMPLEMENTATION`
    # environment variable

or specifying it on the use statement

    use MyFramework::Plugin::OpenTracing ( 'SomeImplementation',
        option => 'foo'
    );

or having a tailored plugin:

    use MyFrameWork::Plugin::OpenTracing::SomeImplementation;

But the latter approach requires writing multiple modules or subclasses.

Whatever way you choose, the responsibility of the Framework Plugin is to set the OpenTracing::GlobalTracer such that it can be used inside the application.

    use OpenTracing::Implementation qw/SomeImplementation/;

Or a more verbose way:

    use OpenTracing::Implementation
    
    my $tracer = OpenTracing::Implementation->bootstrap_global_tracer(
        SomeImplementation,
        option_one => 'foo',
        option_two => 'bar',
    );

See OpenTracing::Implementation for more on how to bootstrap.

OpenTracing Carriers

The OpenTracing specification requires a Tracer implementation to understand how a SpanContext will be injected into or extracted from a so-called carrier or request (https://opentracing.io/docs/overview/inject-extract/).

The carrier formats required are text, http, and binary, but there is no strict standard on how those formats look exactly look. The formats are implementation dependent.

For the time being, with this Perl implementation and definition, only HTTP::Headers will be used as a carrier. But once more, there is no definition on what HTTP Header information is used and how it is formatted.

The HTTP::Headers object is most common in Perl programming, and other variants like HTTP::Headers::Fast or HTTP:::Headers::Fast::XS share the same public interface. Most frameworks know how to handle those.

Extracting Context from an Incoming Request

To extract the tracing context from an upstream service, you will need to provide it as an HTTP::Headers object.

    my $http_headers = YourFramework->request->headers;
    #
    # as long as this is a HTTP::Headers object
    
    my $root_context = $TRACER->extract_context( $http_headers );

All Spans are part of a context. That is also true for the rootspan in a framework. Spans are started as a child_of a specific SpanContext.

The SpanContext for a root-span is the incoming request that may or may not contain tracer information from its requestor. Use the extract_context from a Tracer object

Since some implementations may use immutable objects and may have required attributes for a SpanContext and since the entire API is being accessed through the Tracer object, you may need to use builders as part of the attributes. Such builders can bridge the gap between the framework and the implementation like so, at initialization time:

    $TRACER->set_default_context_builder sub {
        my $service_url = YourFramework->request->url;
        my $service_type = 'WEB',
        
        return {
            tracer_service_endpoint => $service_url,
            tracer_service_type     => $service_type,
        }
    };

And as such the returned hash reference might be merged with extracted tracer info and used to create a complete SpanContext object.

But remember, such mechanisms are entirely dependent on the implementation.

Inject Context into an Outgoing Request

To provide your tracing context to a downstream service, create the HTTP::Headers object, then inject your current context into the headers before passing the request on to the microservice.

    my $span_context = $TRACER->get_active_span->get_context;
    
    use HTTP::Headers;
    my $http_headers = HTTP::Headers->new( ... );
    
    my $cntx_headers =
        $tracer->inject_context( $http_headers, $opentracing_spancontext );
    
    my $request = HTTP::Request->new(
        GET => 'https://...', $cntx_headers
    );
    my response = LWP::UserAgent->request( $request );

NOTE

The OpenTracing specifications mention a CARRIER_FORMAT, which was used in an earlier version. But since the carrier type can be detected by perl, this has been removed form the Perl interface for the inject_context and extract_context definitions. (See also "Propagating Tracer Information between Services" in OpenTracing::Manual::Implementation for an example)

Testing your Framework Plugins

To test that your Framework Plugin is doing the right thing, all you need to do is run it with the Test Implementation. And then compare the collected span information using globaltracer_cmp_deeply with a expected bag of Spans.

    use Test::Most;
    use Test::OpenTracing::Integration;
    
    my $test_application = Test::Application->new;
    my $http_request = HTTP::Request->new( ... );
    
    lives_ok{ $test_application->execute_request( $http_request );
    } "Can do request";
    
    globaltracer_cmp_deeply [ ... ],
        "... and produced the expected spans";
    
    done_testing;
    
    package Test::Application;
    
    use YourFramework;
    
    use YourFramework::Plugin::OpenTracing qw/Test/
    
    sub execute_request { ... }

It is crucial is that you use the Test Implementation. This will keep an in-memory recording of all spans and have a additional get_recorded_trace to get the collected spans, which can be used to compare with expected spans.

Tracing database calls

If you use DBI for database handling, you can

  use DBIx::OpenTracing;

to automatically trace all database queries.

See DBIx::OpenTracing for details.

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::Implementation

For Tracing Service Implementations

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.