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"]
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.
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()
144 def metadata(_self) -> PluginMetadata: 145 return PluginMetadata(name="launchdarkly-observability")
Get metadata about the plugin implementation.
Returns
Metadata containing information about the plugin
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
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
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)
Used to set a custom OTLP endpoint.
Alternatively, set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable.
Specifies the URL used for non-OTLP operations.
This includes accessing client sampling configuration.
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.
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.
The name of the service to use for the OpenTelemetry resource.
Alternatively, set the OTEL_SERVICE_NAME environment variable.
The version of the service to use for the OpenTelemetry resource.
The environment of the service to use for the OpenTelemetry resource.
If True, the OpenTelemetry export error logging will be disabled.
Defaults to False.
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.
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.