ldobserve

The LaunchDarkly observability plugin for Python.

In typical usage you will only need to instantiate the ObservabilityPlugin and pass it to the LaunchDarkly client during initialization.

The settings for the observability plugins are defined by the ObservabilityConfig class.

The ldobserve.observe singleton is used for manual tracking of events, metrics, and logs.

Quick Start

from ldobserve import ObservabilityConfig, ObservabilityPlugin
import ldclient
from ldclient.config import Config

ldclient.set_config(Config("YOUR_SDK_KEY",
plugins=[
    ObservabilityPlugin(
        ObservabilityConfig(
            service_name="your-service-name",
            service_version="your-service-sha",
        )
    )]))
  1"""
  2The LaunchDarkly observability plugin for Python.
  3
  4In typical usage you will only need to instantiate the :class:`ObservabilityPlugin` and pass it to the LaunchDarkly client during initialization.
  5
  6The settings for the observability plugins are defined by the :class:`ObservabilityConfig` class.
  7
  8The `ldobserve.observe` singleton is used for manual tracking of events, metrics, and logs.
  9
 10# Quick Start
 11```python
 12from ldobserve import ObservabilityConfig, ObservabilityPlugin
 13import ldclient
 14from ldclient.config import Config
 15
 16ldclient.set_config(Config("YOUR_SDK_KEY",
 17plugins=[
 18    ObservabilityPlugin(
 19        ObservabilityConfig(
 20            service_name="your-service-name",
 21            service_version="your-service-sha",
 22        )
 23    )]))
 24```
 25
 26"""
 27
 28import logging
 29import os
 30from typing import List, Optional
 31from ldclient.hook import Hook as LDHook
 32from opentelemetry.instrumentation import auto_instrumentation
 33from ldobserve.config import ObservabilityConfig
 34from ldobserve.observe import _ObserveInstance
 35import ldobserve.observe as observe
 36from ldobserve.config import _ProcessedConfig
 37import ldobserve.observe
 38from ldobserve._otel.configuration import _OTELConfiguration
 39from ldclient.plugin import Plugin, EnvironmentMetadata, PluginMetadata
 40from ldotel.tracing import Hook, HookOptions
 41from ldclient.client import LDClient
 42from opentelemetry.instrumentation.environment_variables import (
 43    OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
 44)
 45from opentelemetry.sdk.environment_variables import OTEL_EXPERIMENTAL_RESOURCE_DETECTORS
 46
 47
 48def _extend_environment_list(env_var_name: str, *new_items: str) -> None:
 49    """
 50    Extend an environment variable containing a comma-separated list with additional items.
 51
 52    This function reads the current value of the specified environment variable,
 53    adds the provided items to the comma-separated list, and sets the
 54    environment variable with the extended list.
 55
 56    Args:
 57        env_var_name: The name of the environment variable to extend.
 58        *new_items: Variable number of items to add to the list.
 59
 60    Example:
 61        >>> _extend_environment_list("MY_LIST", "item1", "item2")
 62        # If MY_LIST was "existing_item", it will become "existing_item,item1,item2"
 63    """
 64    current_value = os.getenv(env_var_name, "")
 65
 66    # Split the current value by comma and strip whitespace
 67    # The if condition uses strip to determine if the result is non-empty.
 68    # When it is, then the returned item still needs stripped when creating the new list.
 69    # current_value = ",,,toast,"
 70    # [item.strip() for item in current_value.split(",") if item.strip()]
 71    # Result: ['toast']
 72    current_list = [item.strip() for item in current_value.split(",") if item.strip()]
 73
 74    # Add new items, avoiding duplicates
 75    for item in new_items:
 76        stripped_item = item.strip()
 77        if stripped_item and stripped_item not in current_list:
 78            current_list.append(stripped_item)
 79
 80    # Join the list back into a comma-separated string
 81    new_value = ",".join(current_list)
 82
 83    # Set the environment variable
 84    os.environ[env_var_name] = new_value
 85
 86
 87def _extend_experimental_resource_detectors(*detectors: str) -> None:
 88    """
 89    Extend the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable with additional detectors.
 90
 91    This function reads the current value of OTEL_EXPERIMENTAL_RESOURCE_DETECTORS,
 92    adds the provided detectors to the comma-separated list, and sets the
 93    environment variable with the extended list.
 94    """
 95    _extend_environment_list(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, *detectors)
 96
 97
 98def _extend_disabled_instrumentations(*instrumentations: str) -> None:
 99    """
100    Extend the OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable with additional instrumentations.
101
102    This function reads the current value of OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
103    adds the provided instrumentations to the comma-separated list, and sets the
104    environment variable with the extended list.
105
106    Args:
107        *instrumentations: Variable number of instrumentation names to add to the disabled list.
108                          These should be the names of OpenTelemetry instrumentations to disable.
109
110    Example:
111        >>> extend_disabled_instrumentations("redis", "kafka")
112        # If OTEL_PYTHON_DISABLED_INSTRUMENTATIONS was "grpc_client",
113        # it will become "grpc_client,redis,kafka"
114    """
115    _extend_environment_list(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, *instrumentations)
116
117
118class ObservabilityPlugin(Plugin):
119    def __init__(self, config: Optional[ObservabilityConfig] = None):
120        self._config = _ProcessedConfig(config or ObservabilityConfig())
121        # Instruct auto-instrumentation to not instrument logging.
122        # We will either have already done it, or it is disabled.
123        _extend_disabled_instrumentations("logging")
124
125        if self._config.disabled_instrumentations:
126            _extend_disabled_instrumentations(*self._config.disabled_instrumentations)
127
128        # If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is not set, then we will use the config.
129        # Consider an empty environment variable to be the same as not setting it.
130        if not os.getenv(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS):
131            # Configure resource detectors based on config
132            resource_detectors = []
133            if self._config.process_resources:
134                resource_detectors.append("process")
135            if self._config.os_resources:
136                resource_detectors.append("os")
137
138            if resource_detectors:
139                _extend_experimental_resource_detectors(*resource_detectors)
140
141        auto_instrumentation.initialize()
142
143    def metadata(_self) -> PluginMetadata:
144        return PluginMetadata(name="launchdarkly-observability")
145
146    def register(self, _client: LDClient, metadata: EnvironmentMetadata) -> None:
147        if metadata.sdk_key is None:
148            logging.getLogger(__name__).warning(
149                "The observability plugin was registered without an SDK key. "
150                "This will result in no data being sent to LaunchDarkly."
151            )
152            return
153
154        _init(metadata.sdk_key, self._config)
155
156    def get_hooks(_self, _metadata: EnvironmentMetadata) -> List[LDHook]:
157        return [Hook(options=HookOptions(include_value=True))]
158
159
160def _init(project_id: str, config: _ProcessedConfig):
161    otel_configuration = _OTELConfiguration(project_id, config)
162    ldobserve.observe._instance = _ObserveInstance(project_id, otel_configuration)
163
164
165__all__ = ["ObservabilityPlugin", "ObservabilityConfig", "observe"]
class ObservabilityPlugin(ldclient.plugin.Plugin):
119class ObservabilityPlugin(Plugin):
120    def __init__(self, config: Optional[ObservabilityConfig] = None):
121        self._config = _ProcessedConfig(config or ObservabilityConfig())
122        # Instruct auto-instrumentation to not instrument logging.
123        # We will either have already done it, or it is disabled.
124        _extend_disabled_instrumentations("logging")
125
126        if self._config.disabled_instrumentations:
127            _extend_disabled_instrumentations(*self._config.disabled_instrumentations)
128
129        # If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is not set, then we will use the config.
130        # Consider an empty environment variable to be the same as not setting it.
131        if not os.getenv(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS):
132            # Configure resource detectors based on config
133            resource_detectors = []
134            if self._config.process_resources:
135                resource_detectors.append("process")
136            if self._config.os_resources:
137                resource_detectors.append("os")
138
139            if resource_detectors:
140                _extend_experimental_resource_detectors(*resource_detectors)
141
142        auto_instrumentation.initialize()
143
144    def metadata(_self) -> PluginMetadata:
145        return PluginMetadata(name="launchdarkly-observability")
146
147    def register(self, _client: LDClient, metadata: EnvironmentMetadata) -> None:
148        if metadata.sdk_key is None:
149            logging.getLogger(__name__).warning(
150                "The observability plugin was registered without an SDK key. "
151                "This will result in no data being sent to LaunchDarkly."
152            )
153            return
154
155        _init(metadata.sdk_key, self._config)
156
157    def get_hooks(_self, _metadata: EnvironmentMetadata) -> List[LDHook]:
158        return [Hook(options=HookOptions(include_value=True))]

Abstract base class for extending SDK functionality via plugins.

All provided plugin implementations MUST inherit from this class.

This class includes default implementations for optional methods. This allows LaunchDarkly to expand the list of plugin methods without breaking customer integrations.

Plugins provide an interface which allows for initialization, access to credentials, and hook registration in a single interface.

ObservabilityPlugin(config: Optional[ObservabilityConfig] = None)
120    def __init__(self, config: Optional[ObservabilityConfig] = None):
121        self._config = _ProcessedConfig(config or ObservabilityConfig())
122        # Instruct auto-instrumentation to not instrument logging.
123        # We will either have already done it, or it is disabled.
124        _extend_disabled_instrumentations("logging")
125
126        if self._config.disabled_instrumentations:
127            _extend_disabled_instrumentations(*self._config.disabled_instrumentations)
128
129        # If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is not set, then we will use the config.
130        # Consider an empty environment variable to be the same as not setting it.
131        if not os.getenv(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS):
132            # Configure resource detectors based on config
133            resource_detectors = []
134            if self._config.process_resources:
135                resource_detectors.append("process")
136            if self._config.os_resources:
137                resource_detectors.append("os")
138
139            if resource_detectors:
140                _extend_experimental_resource_detectors(*resource_detectors)
141
142        auto_instrumentation.initialize()
def metadata(_self) -> ldclient.plugin.PluginMetadata:
144    def metadata(_self) -> PluginMetadata:
145        return PluginMetadata(name="launchdarkly-observability")

Get metadata about the plugin implementation.

Returns

Metadata containing information about the plugin

def register( self, _client: ldclient.client.LDClient, metadata: ldclient.plugin.EnvironmentMetadata) -> None:
147    def register(self, _client: LDClient, metadata: EnvironmentMetadata) -> None:
148        if metadata.sdk_key is None:
149            logging.getLogger(__name__).warning(
150                "The observability plugin was registered without an SDK key. "
151                "This will result in no data being sent to LaunchDarkly."
152            )
153            return
154
155        _init(metadata.sdk_key, self._config)

Register the plugin with the SDK client.

This method is called during SDK initialization to allow the plugin to set up any necessary integrations, register hooks, or perform other initialization tasks.

Parameters
  • client: The LDClient instance
  • metadata: Metadata about the environment in which the SDK is running
def get_hooks( _self, _metadata: ldclient.plugin.EnvironmentMetadata) -> List[ldclient.hook.Hook]:
157    def get_hooks(_self, _metadata: EnvironmentMetadata) -> List[LDHook]:
158        return [Hook(options=HookOptions(include_value=True))]

Get a list of hooks that this plugin provides.

This method is called before register() to collect all hooks from plugins. The hooks returned will be added to the SDK's hook configuration.

Parameters
  • metadata: Metadata about the environment in which the SDK is running
Returns

A list of hooks to be registered with the SDK

@dataclass(kw_only=True)
class ObservabilityConfig:
 23@dataclass(kw_only=True)
 24class ObservabilityConfig:
 25    otlp_endpoint: Optional[str] = None
 26    """
 27    Used to set a custom OTLP endpoint.
 28
 29    Alternatively, set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable.
 30    """
 31
 32    backend_url: Optional[str] = None
 33    """
 34    Specifies the URL used for non-OTLP operations.
 35
 36    This includes accessing client sampling configuration.
 37    """
 38
 39    instrument_logging: Optional[bool] = None
 40    """
 41    If True, the OpenTelemetry logging instrumentation will be enabled.
 42
 43    If False, the OpenTelemetry logging instrumentation will be disabled.
 44
 45    Alternatively, set the OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED environment variable.
 46
 47    If a custom logging configuration is desired, then it should be configured before initializing the 
 48    Observability plugin. The Observability plugin will configure default logging prior to adding the 
 49    OpenTelemetry logging instrumentation.
 50
 51    For example:
 52    >>> import logging
 53    >>> logging.basicConfig(level=logging.INFO)
 54
 55    Defaults to True.
 56    """
 57
 58    log_level: Optional[int] = None
 59    """
 60    The log level to use for the OpenTelemetry logging instrumentation.
 61
 62    This does not affect the log level of the default logging configuration (stdout).
 63
 64    Defaults to logging.INFO.
 65    """
 66
 67    service_name: Optional[str] = None
 68    """
 69    The name of the service to use for the OpenTelemetry resource.
 70
 71    Alternatively, set the OTEL_SERVICE_NAME environment variable.
 72    """
 73
 74    service_version: Optional[str] = None
 75    """
 76    The version of the service to use for the OpenTelemetry resource.
 77    """
 78
 79    environment: Optional[str] = None
 80    """
 81    The environment of the service to use for the OpenTelemetry resource.
 82    """
 83
 84    disable_export_error_logging: Optional[bool] = None
 85    """
 86    If True, the OpenTelemetry export error logging will be disabled.
 87
 88    Defaults to False.
 89    """
 90
 91    log_correlation: Optional[bool] = None
 92    """
 93    If True, the logging format will be updated to enable log correlation.
 94    If :class:`ObservabilityConfig.instrument_logging` is False, then this setting will have no effect.
 95    If logging is configured before the Observability plugin is initialized, then this setting will have no effect.
 96
 97    Any custom logging format will allow log correlation if it includes:
 98    - %(otelTraceID)s
 99    - %(otelSpanID)s
100    - %(otelServiceName)s
101    - %(otelTraceSampled)s
102
103    The default logging format is:
104    >>> "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s trace_sampled=%(otelTraceSampled)s] - %(message)s"
105
106    Alternatively, set the OTEL_PYTHON_LOG_CORRELATION environment variable to "true".
107
108    If the OTEL_PYTHON_LOG_FORMAT environment variable is set, then it will be used as the logging format.
109
110    Defaults to True.
111    """
112
113    disabled_instrumentations: Optional[list[str]] = None
114    """
115    A list of OpenTelemetry instrumentations to disable.
116
117    Alternatively the OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable can be used.
118
119    If this list and the OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable are both set, then the lists will be combined.
120    """
121
122    process_resources: Optional[bool] = None
123    """
124    Determines if process resource attributes are included in the OpenTelemetry resource.
125
126    If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is set, then this setting will have no effect.
127
128    Defaults to True.
129    """
130
131    os_resources: Optional[bool] = None
132    """
133    Determines if OS resource attributes are included in the OpenTelemetry resource.
134
135        If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is set, then this setting will have no effect.
136
137    Defaults to True.
138    """
139
140    def __getitem__(self, key: str):
141        return getattr(self, key)
ObservabilityConfig( *, otlp_endpoint: Optional[str] = None, backend_url: Optional[str] = None, instrument_logging: Optional[bool] = None, log_level: Optional[int] = None, service_name: Optional[str] = None, service_version: Optional[str] = None, environment: Optional[str] = None, disable_export_error_logging: Optional[bool] = None, log_correlation: Optional[bool] = None, disabled_instrumentations: Optional[list[str]] = None, process_resources: Optional[bool] = None, os_resources: Optional[bool] = None)
otlp_endpoint: Optional[str] = None

Used to set a custom OTLP endpoint.

Alternatively, set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable.

backend_url: Optional[str] = None

Specifies the URL used for non-OTLP operations.

This includes accessing client sampling configuration.

instrument_logging: Optional[bool] = None

If True, the OpenTelemetry logging instrumentation will be enabled.

If False, the OpenTelemetry logging instrumentation will be disabled.

Alternatively, set the OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED environment variable.

If a custom logging configuration is desired, then it should be configured before initializing the Observability plugin. The Observability plugin will configure default logging prior to adding the OpenTelemetry logging instrumentation.

For example:

>>> import logging
>>> logging.basicConfig(level=logging.INFO)

Defaults to True.

log_level: Optional[int] = None

The log level to use for the OpenTelemetry logging instrumentation.

This does not affect the log level of the default logging configuration (stdout).

Defaults to logging.INFO.

service_name: Optional[str] = None

The name of the service to use for the OpenTelemetry resource.

Alternatively, set the OTEL_SERVICE_NAME environment variable.

service_version: Optional[str] = None

The version of the service to use for the OpenTelemetry resource.

environment: Optional[str] = None

The environment of the service to use for the OpenTelemetry resource.

disable_export_error_logging: Optional[bool] = None

If True, the OpenTelemetry export error logging will be disabled.

Defaults to False.

log_correlation: Optional[bool] = None

If True, the logging format will be updated to enable log correlation. If ObservabilityConfig.instrument_logging is False, then this setting will have no effect. If logging is configured before the Observability plugin is initialized, then this setting will have no effect.

Any custom logging format will allow log correlation if it includes:

  • %(otelTraceID)s
  • %(otelSpanID)s
  • %(otelServiceName)s
  • %(otelTraceSampled)s

The default logging format is:

>>> "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] [trace_id=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s trace_sampled=%(otelTraceSampled)s] - %(message)s"

Alternatively, set the OTEL_PYTHON_LOG_CORRELATION environment variable to "true".

If the OTEL_PYTHON_LOG_FORMAT environment variable is set, then it will be used as the logging format.

Defaults to True.

disabled_instrumentations: Optional[list[str]] = None

A list of OpenTelemetry instrumentations to disable.

Alternatively the OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable can be used.

If this list and the OTEL_PYTHON_DISABLED_INSTRUMENTATIONS environment variable are both set, then the lists will be combined.

process_resources: Optional[bool] = None

Determines if process resource attributes are included in the OpenTelemetry resource.

If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is set, then this setting will have no effect.

Defaults to True.

os_resources: Optional[bool] = None

Determines if OS resource attributes are included in the OpenTelemetry resource.

If the OTEL_EXPERIMENTAL_RESOURCE_DETECTORS environment variable is set, then this setting will have no effect.

Defaults to True.