Enabling and Using Tracing Functionality

Introduction

On this page, we will enable and use the Opentelemetry Tracing functionality to get a visual representation of our Extension. Once again, we will use the UIP VSCode Plugin to test our changes, for now.

Step 1 - Enabling Tracing

Go ahead and open configurations.yml and add in the properties block:

configurations.yml
properties:
  agent:
    log_level: Info
    netname: UIP-DBG-01
    otel:
      enable_tracing: true
      export_metrics: false
      trace_endpoint: http://192.168.56.11:4318
      metrics_endpoint: http://localhost:4318
      service_name: vscode-uip-debugger
      uip_service_name: uip/${extension_name}
api:
  extension_start:
    - name: es1
      log_level: Inherited
      runtime_dir: /home/shrey/dev/extensions/test/OtelDemoTest
      fields:
        src_folder: /tmp/test_src
        dst_folder: /tmp/test_dst
        file_type:
          - txt

As part of UIP VSCode Plugin version 2.1.0, the properties -> agent object was enhanced with new netname and otel properties. To enable tracing, we have set enable_tracing to true and set trace_endpoint to the Opentelemetry Collector URL (this will need to be changed according to your setup).

Enabling in UA

Similar properties exist in uags.conf and omss.conf that can be used to enable tracing in the Agent. See OTEL_ENABLE_TRACING - UAG configuration option and OTEL_ENABLE_TRACING - OMS configuration option.

Step 2 – Visualizing Default Spans

Now that we have enabled tracing, we actually don't have to do anything else to get a basic trace. Go ahead and delete all the files inside /tmp/test_dst and debug the Extension again using F5. Once finished, head on over to Jaeger (or whatever trace visualization tool you have set up) and you should see:

Without modifying the Extension code at all, we have a meaningful way of looking at the Extension instance. Go ahead and inspect the spans (debug start, api: extension_start, etc.) by clicking on them; you will find useful information such as the fields passed into extension_start(), rc, unv_output etc. all consolidated in one place.

Step 3 – Adding Additional Spans

Even though the default spans above offer insight into our Extension, we are not able to clearly see when a file gets transferred or even how long it takes. Let's add some custom spans to capture this information. Go ahead and update extension.py as follows:

extension.py
from __future__ import print_function
from universal_extension import UniversalExtension
from universal_extension import ExtensionResult
from universal_extension import ui
from universal_extension import logger

from universal_extension import utility
from universal_extension import otel

import time
import shutil
import os
import random
import json

if otel.is_compatible:
    from opentelemetry import trace


class Extension(UniversalExtension):
    def __init__(self):
        """Initializes an instance of the 'Extension' class"""
        # Call the base class initializer
        super(Extension, self).__init__()
        self.stop = False

        self.setup_tracer()

    def setup_tracer(self):
        if otel.is_compatible:
            self.tracer = trace.get_tracer(__name__)
        else:
            self.tracer = utility.NoOp()

    def transfer_file(self, src_path, dst_path, span):
        span.set_attributes({"src_file": src_path, "dst_folder": dst_path})

        # Ensure destination directory exits
        if not os.path.exists(dst_path):
            raise FileNotFoundError(
                "Destination directory ({0}) does not exist".format(dst_path)
            )

        # Ensure the source file is not already present in the destination
        # directory (unless overwrite is selected)
        if os.path.exists(os.path.join(dst_path, os.path.basename(src_path))):
            logger.info(
                "'{0}' already exists in '{1}'".format(
                    os.path.basename(src_path), dst_path
                )
            )
            if otel.is_compatible:
                span.set_status(
                    trace.Status(
                        status_code=trace.StatusCode.ERROR,
                        description="'{0}' already exists in '{1}'".format(
                            os.path.basename(src_path), dst_path
                        ),
                    )
                )
            return False

        shutil.copy(src_path, dst_path)
        time.sleep(random.uniform(0, 2))

        return True

    def extension_start(self, fields):
        """Required method that serves as the starting point for work performed
        for a task instance.

        Parameters
        ----------
        fields : dict
            populated with field values from the associated task instance
            launched in the Controller

        Returns
        -------
        ExtensionResult
            once the work is done, an instance of ExtensionResult must be
            returned. See the documentation for a full list of parameters that
            can be passed to the ExtensionResult class constructor
        """

        files_transferred = []
        src = fields["src_folder"]
        dst = fields["dst_folder"]

        file_types = [
            ft.lower() if ft.startswith(".") else "." + ft.lower()
            for ft in fields["file_type"]
        ]

        if not os.path.exists(src):
            raise FileNotFoundError("'{0}' does not exist".format(src))

        all_file_list = os.listdir(src)

        # filter the files
        file_list = []
        for f in all_file_list:
            file_path = os.path.join(src, f)
            file_type = os.path.splitext(file_path)[1]
            if os.path.isfile(file_path) and file_type in file_types:
                file_list.append(file_path)

        logger.info(
            "Found {0} files that can be transferred".format(len(file_list))
        )

        for f in file_list:
            if self.stop:
                break

            span_ctx = (
                utility.noop_context()
                if not otel.is_compatible
                else self.tracer.start_as_current_span("transferring file")
            )

            with span_ctx as span:
                if self.transfer_file(f, dst, span):
                    files_transferred.append(f)
                    ui.update_progress(
                        int(len(files_transferred) / len(file_list) * 100)
                    )
                    logger.info("Transferred '{0}' to '{1}'".format(f, dst))

        return ExtensionResult(
            rc=0 if len(file_list) - len(files_transferred) == 0 else 1,
            unv_output="The following files were transferred: \n {0}".format(
                json.dumps(files_transferred)
            ),
            message="{0} files found and {1} files transferred".format(
                len(file_list), len(files_transferred)
            ),
        )

    def extension_cancel(self):
        self.stop = True

  • Lines 7-8 import the new utility and otel modules used to integrate Opentelemetry into the Extension.
  • Lines 16-17 conditionally import the trace module from opentelemetry. We are doing it conditionally because Opentelemetry is only supported on Python 3.7 and higher. If your Extension is not supported on <3.7, then you do not need to guard the import with otel.is_compatible
  • Lines 27-33:
    • Lines 29-33 define a separate method to set up the Opentelemetry tracer used to create custom spans. Once again, it is guarded with otel.is_compatible.  If Opentelemetry is not compatible, then we assign utility.NoOp() to self.tracer, which will mimic the actual tracer object without affecting anything.
    • Line 27 calls the self.setup_tracer() method
  • Lines 35-66:
    • Line 35 modifies the transfer_file() method to accept an additional parameter called span
    • Line 36 adds the source file and the destination folder as attributes on the span object
    • Lines 52-60 explicitly set the status of the span as error when the source file exists in the destination folder.
  • Lines 116-129:
    • Lines 116-120 create a span_ctx variable that will store the Span Context. This is needed to ensure the Extension does not break, if Opentelemetry is not compatible.
    • Lines 122-128 use span_ctx to create a Span object called span which is then passed into self.transfer_file.

Now, let's visualize our changes. From the last debugging session, the /tmp/test_dst folder should contain a.txt and b.txt. Keep these in there (if you have deleted them, just copy them manually). Go ahead and modify the configurations.yml to also transfer zip and json files:

configurations.yml
      fields:
        src_folder: /tmp/test_src
        dst_folder: /tmp/test_dst
        file_type:
          - txt
          - zip
          - json

Once modified, press F5 and head on over to Jaeger. You should see:

We can now clearly see how long each file takes to transfer. We can see that a.txt and b.txt failed to transfer because they were already in the destination folder. c.zip and d.json succeeded, and we can see their transfer times. Inspect the spans, and you will be able to see the custom src_file and dst_folder attributes that we added.

< Prev    Next >