--- title: Manual Instrumentation linkTitle: Manual weight: 3 --- Manual instrumentation is the process of adding observability code to your application. ## Initializing the SDK First, ensure you have the API and SDK packages: ``` pip install opentelemetry-api pip install opentelemetry-sdk ``` To start tracing, you'll need to initialize a [`TracerProvider`](/docs/concepts/signals/traces/#tracer-provider) and optionally set it as the global default. ```python from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) provider = TracerProvider() processor = BatchSpanProcessor(ConsoleSpanExporter()) provider.add_span_processor(processor) # Sets the global default tracer provider trace.set_tracer_provider(provider) # Creates a tracer from the global tracer provider tracer = trace.get_tracer(__name__) ``` To start collecting metrics, you'll need to initialize a [`MeterProvider`](/docs/reference/specification/metrics/api/#meterprovider) and optionally set it as the global default. ```python from opentelemetry import metrics from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( ConsoleMetricExporter, PeriodicExportingMetricReader, ) metric_reader = PeriodicExportingMetricReader(ConsoleMetricExporter()) provider = MeterProvider(metric_readers=[metric_reader]) # Sets the global default meter provider metrics.set_meter_provider(provider) # Creates a meter from the global meter provider meter = metrics.get_meter(__name__) ``` ## Tracing ### Creating spans To create a [span](/docs/concepts/signals/traces/#spans-in-opentelemetry), you'll typically want it to be started as the current span. ```python def do_work(): with tracer.start_as_current_span("span-name") as span: # do some work that 'span' will track print("doing some work...") # When the 'with' block goes out of scope, 'span' is closed for you ``` You can also use `start_span` to create a span without making it the current span. This is usually done to track concurrent or asynchronous operations. ### Creating nested spans If you have a distinct sub-operation you'd like to track as a part of another one, you can create [spans](/docs/concepts/signals/traces/#spans-in-opentelemetry) to represent the relationship: ```python def do_work(): with tracer.start_as_current_span("parent") as parent: # do some work that 'parent' tracks print("doing some work...") # Create a nested span to track nested work with tracer.start_as_current_span("child") as child: # do some work that 'child' tracks print("doing some nested work...") # the nested span is closed when it's out of scope # This span is also closed when it goes out of scope ``` When you view spans in a trace visualization tool, `child` will be tracked as a nested span under `parent`. ### Creating spans with decorators It's common to have a single [span](/docs/concepts/signals/traces/#spans-in-opentelemetry) track the execution of an entire function. In that scenario, there is a decorator you can use to reduce code: ```python @tracer.start_as_current_span("do_work") def do_work(): print("doing some work...") ``` Use of the decorator is equivalent to creating the span inside `do_work()` and ending it when `do_work()` is finished. To use the decorator, you must have a `tracer` instance available global to your function declaration. If you need to add [attributes](#add-attributes-to-a-span), [events](#adding-events), or [links](#adding-links) then it's less convenient to use a decorator. ### Get the current span Sometimes it's helpful to access whatever the current [span](/docs/concepts/signals/traces/#spans-in-opentelemetry) is at a point in time so that you can enrich it with more information. ```python from opentelemetry import trace current_span = trace.get_current_span() # enrich 'current_span' with some information ``` ### Add attributes to a span [Attributes](/docs/concepts/signals/traces/#attributes) let you attach key/value pairs to a [span](/docs/concepts/signals/traces/#spans-in-opentelemetry) so it carries more information about the current operation that it's tracking. ```python from opentelemetry import trace current_span = trace.get_current_span() current_span.set_attribute("operation.value", 1) current_span.set_attribute("operation.name", "Saying hello!") current_span.set_attribute("operation.other-stuff", [1, 2, 3]) ``` ### Adding events An [event](/docs/concepts/signals/traces/#span-events) is a human-readable message on a [span](/docs/concepts/signals/traces/#spans-in-opentelemetry) that represents "something happening" during its lifetime. You can think of it as a primitive log. ```python from opentelemetry import trace current_span = trace.get_current_span() current_span.add_event("Gonna try it!") # Do the thing current_span.add_event("Did it!") ``` ### Adding links A [span](/docs/concepts/signals/traces/#spans-in-opentelemetry) can be created with zero or more span [links](/docs/concepts/signals/traces/#span-links) that causally link it to another span. A link needs a span context to be created. ```python from opentelemetry import trace ctx = trace.get_current_span().get_span_context() link_from_current = trace.Link(ctx) with tracer.start_as_current_span("new-span", links=[link_from_current]) as new_span: # do something that 'new_span' tracks # The link in 'new_span' casually associated it with the previous one, # but it is not a child span. ``` ### Set span status A [status](/docs/concepts/signals/traces/#span-status) can be set on a [span](/docs/concepts/signals/traces/#spans-in-opentelemetry), typically used to specify that a span has not completed successfully - `StatusCode.ERROR`. In rare scenarios, you could override the Error status with `StatusCode.OK`, but don’t set `StatusCode.OK` on successfully-completed spans. The status can be set at any time before the span is finished: ```python from opentelemetry import trace from opentelemetry.trace import Status, StatusCode current_span = trace.get_current_span() try: # something that might fail except: current_span.set_status(Status(StatusCode.ERROR)) ``` ### Record exceptions in spans It can be a good idea to record exceptions when they happen. It’s recommended to do this in conjunction with setting [span status](#set-span-status). ```python from opentelemetry import trace from opentelemetry.trace import Status, StatusCode current_span = trace.get_current_span() try: # something that might fail # Consider catching a more specific exception in your code except Exception as ex: current_span.set_status(Status(StatusCode.ERROR)) current_span.record_exception(ex) ``` ### Change the default propagation format By default, OpenTelemetry Python will use the following propagation formats: * W3C Trace Context * W3C Baggage If you have a need to change the defaults, you can do so either via environment variables or in code: #### Using Environment Variables You can set the `OTEL_PROPAGATORS` environment variable with a comma-separated list. Accepted values are: * `"tracecontext"`: W3C Trace Context * `"baggage"`: W3C Baggage * `"b3"`: B3 Single * `"b3multi"`: B3 Multi * `"jaeger"`: Jaeger * `"xray"`: AWS X-Ray (third party) * `"ottrace"`: OT Trace (third party) * `"none"`: No automatically configured propagator. The default configuration is equivalent to `OTEL_PROPAGATORS="tracecontext,baggage"`. #### Using SDK APIs Alternatively, you can change the format in code. For example, if you need to use Zipkin's B3 propagation format instead, you can install the B3 package: ```shell pip install opentelemetry-propagator-b3 ``` And then set the B3 propagator in your tracing initialization code: ```python from opentelemetry.propagate import set_global_textmap from opentelemetry.propagators.b3 import B3Format set_global_textmap(B3Format()) ``` Note that environment variables will override what's configured in code. ## Metrics ### Creating and using synchronous instruments Instruments are used to make measurements of your application. [Synchronous instruments](/docs/reference/specification/metrics/api/#synchronous-and-asynchronous-instruments) are used inline with application/business processing logic, like when handling a request or calling another service. First, create your instrument. Instruments are generally created once at the module or class level and then used inline with business logic. This example uses a [Counter](/docs/reference/specification/metrics/api/#counter) instrument to count the number of work items completed: ```python work_counter = meter.create_counter( "work.counter", unit="1", description="Counts the amount of work done" ) ``` Using the Counter's [add operation](/docs/reference/specification/metrics/api/#add), the code below increments the count by one, using the work item's type as an attribute. ```python def do_work(work_item): # count the work being doing work_counter.add(1, {"work.type": work_item.work_type}) print("doing some work...") ``` ### Creating and using asynchronous instruments [Asynchronous instruments](/docs/reference/specification/metrics/api/#synchronous-and-asynchronous-instruments) give the user a way to register callback functions, which are invoked on demand to make measurements. This is useful to periodically measure a value that cannot be instrumented directly. Async instruments are created with zero or more callbacks which will be invoked during metric collection. Each callback accepts options from the SDK and returns its observations. This example uses an [Asynchronous Gauge](/docs/reference/specification/metrics/api/#asynchronous-gauge) instrument to report the current config version provided by a configuration server by scraping an HTTP endpoint. First, write a callback to make observations: ```python from typing import Iterable from opentelemetry.metrics import CallbackOptions, Observation def scrape_config_versions(options: CallbackOptions) -> Iterable[Observation]: r = requests.get( "http://configserver/version_metadata", timeout=options.timeout_millis / 10**3 ) for metadata in r.json(): yield Observation( metadata["version_num"], {"config.name": metadata["version_num"]} ) ``` Note that OpenTelemetry will pass options to your callback containing a timeout. Callbacks should respect this timeout to avoid blocking indefinitely. Finally, create the instrument with the callback to register it: ```python meter.create_observable_gauge( "config.version", callbacks=[scrape_config_versions], description="The active config version for each configuration", ) ``` ## Additional References - Trace - [Trace Concepts](/docs/concepts/signals/traces/) - [Trace Specification](/docs/reference/specification/overview/#tracing-signal) - [Python Trace API Documentation](https://opentelemetry-python.readthedocs.io/en/latest/api/trace.html) - [Python Trace SDK Documentation](https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.html) - Metrics - [Metrics Concepts](/docs/concepts/signals/metrics/) - [Metrics Specification](/docs/reference/specification/metrics/) - [Python Metrics API Documentation](https://opentelemetry-python.readthedocs.io/en/latest/api/metrics.html) - [Python Metrics SDK Documentation](https://opentelemetry-python.readthedocs.io/en/latest/sdk/metrics.html)