ldobserve.observe
1import contextlib 2import logging 3import typing 4from opentelemetry.context import Context 5from opentelemetry.instrumentation.logging import LEVELS 6from opentelemetry.sdk._logs import LoggingHandler 7from opentelemetry.trace import Span, Tracer 8import opentelemetry.trace as trace 9from opentelemetry.util.types import Attributes 10from opentelemetry._logs import get_logger_provider 11from ldobserve._otel.configuration import _OTELConfiguration 12from ldobserve._source_context import exception_attributes 13from ldobserve._util.dict import flatten_dict 14 15from opentelemetry.metrics import ( 16 _Gauge as APIGauge, 17 Histogram as APIHistogram, 18 Counter as APICounter, 19 UpDownCounter as APIUpDownCounter, 20) 21 22_NAME = "launchdarkly-observability" 23_ERROR_NAME = "launchdarkly.error" 24 25 26class _ObserveInstance: 27 _project_id: str 28 _tracer: Tracer 29 30 _provider = get_logger_provider() 31 _logger = logging.getLogger(__name__) 32 _otel_configuration: _OTELConfiguration 33 34 _gauges: dict[str, APIGauge] = dict() 35 _counters: dict[str, APICounter] = dict() 36 _histograms: dict[str, APIHistogram] = dict() 37 _up_down_counters: dict[str, APIUpDownCounter] = dict() 38 39 @property 40 def log_handler(self) -> logging.Handler: 41 return self._otel_configuration.log_handler 42 43 def __init__(self, project_id: str, otel_configuration: _OTELConfiguration): 44 self._otel_configuration = otel_configuration 45 46 # Logger that will only log to OpenTelemetry. 47 self._logger.propagate = False 48 self._logger.addHandler(otel_configuration.log_handler) 49 self._project_id = project_id 50 self._tracer = otel_configuration.tracer 51 52 def record_exception( 53 self, error: Exception, attributes: typing.Optional[Attributes] = None 54 ): 55 if error is None: 56 return # Nothing to record 57 58 span = trace.get_current_span() 59 ctx = contextlib.nullcontext(span) 60 if not span or not span.is_recording(): 61 ctx = self.start_span(_ERROR_NAME) 62 63 with ctx as span: 64 # Source context first, so caller-supplied attributes win on conflict. 65 attrs = exception_attributes(error) 66 if attributes: 67 addedAttributes = flatten_dict(attributes, sep=".") 68 attrs.update(addedAttributes) 69 70 span.record_exception(error, attrs) 71 72 def record_metric( 73 self, name: str, value: float, attributes: typing.Optional[Attributes] = None 74 ): 75 if name not in self._gauges: 76 self._gauges[name] = self._otel_configuration.meter.create_gauge(name) 77 self._gauges[name].set(value, attributes=attributes) 78 79 def record_count( 80 self, name: str, value: int, attributes: typing.Optional[Attributes] = None 81 ): 82 if name not in self._counters: 83 self._counters[name] = self._otel_configuration.meter.create_counter(name) 84 self._counters[name].add(value, attributes=attributes) 85 86 def record_incr(self, name: str, attributes: typing.Optional[Attributes] = None): 87 return self.record_count(name, 1, attributes) 88 89 def record_histogram( 90 self, name: str, value: float, attributes: typing.Optional[Attributes] = None 91 ): 92 if name not in self._histograms: 93 self._histograms[name] = self._otel_configuration.meter.create_histogram( 94 name 95 ) 96 self._histograms[name].record(value, attributes=attributes) 97 98 def record_up_down_counter( 99 self, name: str, value: int, attributes: typing.Optional[Attributes] = None 100 ): 101 if name not in self._up_down_counters: 102 self._up_down_counters[name] = ( 103 self._otel_configuration.meter.create_up_down_counter(name) 104 ) 105 self._up_down_counters[name].add(value, attributes=attributes) 106 107 def log( 108 self, message: str, level: int, attributes: typing.Optional[Attributes] = None 109 ): 110 self._logger.log(level, message, extra=attributes) 111 112 @contextlib.contextmanager 113 def start_span( 114 self, 115 name: str, 116 attributes: Attributes = None, 117 record_exception: bool = True, 118 set_status_on_exception: bool = True, 119 ) -> typing.Iterator["Span"]: 120 """ 121 Context manager for creating a new span and setting it as the current span. 122 123 Exiting the context manager will call the span's end method, 124 as well as return the current span to its previous value by 125 returning to the previous context. 126 127 Args: 128 name: The name of the span. 129 attributes: The attributes of the span. 130 record_exception: Whether to record any exceptions raised within the 131 context as error event on the span. 132 set_status_on_exception: Only relevant if the returned span is used 133 in a with/context manager. Defines whether the span status will 134 135 Yields: 136 The newly-created span. 137 """ 138 with self._tracer.start_as_current_span( 139 name, 140 attributes=attributes, 141 record_exception=record_exception, 142 set_status_on_exception=set_status_on_exception, 143 ) as span: 144 yield span 145 146 147_instance: typing.Optional[_ObserveInstance] = None 148 149 150def _use_instance(func): 151 """Helper function to delegate calls to the instance if it exists.""" 152 if not _instance: 153 logging.getLogger(__name__).warning( 154 "The observability singleton was used before it was initialized." 155 ) 156 return 157 return func(_instance) 158 159 160def record_exception(error: Exception, attributes: typing.Optional[Attributes] = None): 161 """ 162 Record arbitrary exceptions raised within your app. 163 164 Example: 165 import ldobserve.observe as observe 166 # Observability plugin must be initialized. 167 168 def my_fn(): 169 try: 170 for i in range(20): 171 result = 100 / (10 - i) 172 print(f'dangerous: {result}') 173 except Exception as e: 174 observe.record_exception(e) 175 176 177 :param e: the exception to record. the contents and stacktrace will be recorded. 178 :param attributes: additional metadata to attribute to this error. 179 :return: None 180 """ 181 _use_instance(lambda instance: instance.record_exception(error, attributes)) 182 183 184def record_metric( 185 name: str, value: float, attributes: typing.Optional[Attributes] = None 186): 187 """ 188 Record arbitrary metric values via as a Gauge. 189 A Gauge records any point-in-time measurement, such as the current CPU utilization %. 190 Values with the same metric name and attributes are aggregated via the OTel SDK. 191 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 192 :param name: the name of the metric. 193 :param value: the float value of the metric. 194 :param attributes: additional metadata which can be used to filter and group values. 195 :return: None 196 """ 197 _use_instance(lambda instance: instance.record_metric(name, value, attributes)) 198 199 200def record_count(name: str, value: int, attributes: typing.Optional[Attributes] = None): 201 """ 202 Record arbitrary metric values via as a Counter. 203 A Counter efficiently records an increment in a metric, such as number of cache hits. 204 Values with the same metric name and attributes are aggregated via the OTel SDK. 205 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 206 :param name: the name of the metric. 207 :param value: the float value of the metric. 208 :param attributes: additional metadata which can be used to filter and group values. 209 :return: None 210 """ 211 _use_instance(lambda instance: instance.record_count(name, value, attributes)) 212 213 214def record_incr(name: str, attributes: typing.Optional[Attributes] = None): 215 """ 216 Record arbitrary metric +1 increment via as a Counter. 217 A Counter efficiently records an increment in a metric, such as number of cache hits. 218 Values with the same metric name and attributes are aggregated via the OTel SDK. 219 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 220 :param name: the name of the metric. 221 :param attributes: additional metadata which can be used to filter and group values. 222 :return: None 223 """ 224 _use_instance(lambda instance: instance.record_incr(name, attributes)) 225 226 227def record_histogram( 228 name: str, value: float, attributes: typing.Optional[Attributes] = None 229): 230 """ 231 Record arbitrary metric values via as a Histogram. 232 A Histogram efficiently records near-by point-in-time measurement into a bucketed aggregate. 233 Values with the same metric name and attributes are aggregated via the OTel SDK. 234 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 235 :param name: the name of the metric. 236 :param value: the float value of the metric. 237 :param attributes: additional metadata which can be used to filter and group values. 238 :return: None 239 """ 240 _use_instance(lambda instance: instance.record_histogram(name, value, attributes)) 241 242 243def record_up_down_counter( 244 name: str, value: int, attributes: typing.Optional[Attributes] = None 245): 246 """ 247 Record arbitrary metric values via as a UpDownCounter. 248 A UpDownCounter efficiently records an increment or decrement in a metric, such as number of paying customers. 249 Values with the same metric name and attributes are aggregated via the OTel SDK. 250 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 251 :param name: the name of the metric. 252 :param value: the float value of the metric. 253 :param attributes: additional metadata which can be used to filter and group values. 254 :return: None 255 """ 256 _use_instance( 257 lambda instance: instance.record_up_down_counter(name, value, attributes) 258 ) 259 260 261def record_log( 262 message: str, 263 level: int, 264 attributes: typing.Optional[Attributes] = None, 265): 266 """ 267 Records a log. This log will be recorded to LaunchDarkly, but will not be send to other log handlers. 268 A Log records a message with a level and optional attributes. 269 :param message: the message to record. 270 :param level: the level of the log. 271 :param attributes: additional metadata which can be used to filter and group values. 272 :return: None 273 """ 274 _use_instance(lambda instance: instance.log(message, level, attributes)) 275 276 277def logging_handler() -> logging.Handler: 278 """A logging handler implementing `logging.Handler` that allows plugging LaunchDarkly Observability 279 into your existing logging setup. Standard logging will be automatically instrumented unless 280 :class:`ObservabilityConfig.instrument_logging <ldobserve.config.ObservabilityConfig.instrument_logging>` is set to False. 281 282 Example: 283 import ldobserve.observe as observe 284 from loguru import logger 285 286 # Observability plugin must be initialized. 287 # If the Observability plugin is not initialized, then a NullHandler will be returned. 288 289 logger.add( 290 observe.logging_handler(), 291 format="{message}", 292 level="INFO", 293 backtrace=True, 294 ) 295 """ 296 if not _instance: 297 return logging.NullHandler() 298 return _instance.log_handler 299 300 301@contextlib.contextmanager 302def start_span( 303 name: str, 304 attributes: Attributes = None, 305 record_exception: bool = True, 306 set_status_on_exception: bool = True, 307) -> typing.Iterator["Span"]: 308 """ 309 Context manager for creating a new span and setting it as the current span. 310 311 Exiting the context manager will call the span's end method, 312 as well as return the current span to its previous value by 313 returning to the previous context. 314 315 Args: 316 name: The name of the span. 317 attributes: The attributes of the span. 318 record_exception: Whether to record any exceptions raised within the 319 context as error event on the span. 320 set_status_on_exception: Only relevant if the returned span is used 321 in a with/context manager. Defines whether the span status will 322 323 Yields: 324 The newly-created span. 325 """ 326 if _instance: 327 with _instance.start_span( 328 name, 329 attributes=attributes, 330 record_exception=record_exception, 331 set_status_on_exception=set_status_on_exception, 332 ) as span: 333 yield span 334 else: 335 # If not initialized, then get a tracer and use it to create a span. 336 # We don't want to prevent user code from executing correctly if 337 # the plugin is not initialized. 338 logging.getLogger(__name__).warning( 339 "The observability singleton was used before it was initialized." 340 ) 341 with trace.get_tracer(__name__).start_as_current_span( 342 name, 343 attributes=attributes, 344 record_exception=record_exception, 345 set_status_on_exception=set_status_on_exception, 346 ) as span: 347 yield span 348 349 350def is_initialized() -> bool: 351 return _instance != None
161def record_exception(error: Exception, attributes: typing.Optional[Attributes] = None): 162 """ 163 Record arbitrary exceptions raised within your app. 164 165 Example: 166 import ldobserve.observe as observe 167 # Observability plugin must be initialized. 168 169 def my_fn(): 170 try: 171 for i in range(20): 172 result = 100 / (10 - i) 173 print(f'dangerous: {result}') 174 except Exception as e: 175 observe.record_exception(e) 176 177 178 :param e: the exception to record. the contents and stacktrace will be recorded. 179 :param attributes: additional metadata to attribute to this error. 180 :return: None 181 """ 182 _use_instance(lambda instance: instance.record_exception(error, attributes))
Record arbitrary exceptions raised within your app.
Example: import ldobserve.observe as observe # Observability plugin must be initialized.
def my_fn():
try:
for i in range(20):
result = 100 / (10 - i)
print(f'dangerous: {result}')
except Exception as e:
observe.record_exception(e)
Parameters
- e: the exception to record. the contents and stacktrace will be recorded.
- attributes: additional metadata to attribute to this error.
Returns
None
185def record_metric( 186 name: str, value: float, attributes: typing.Optional[Attributes] = None 187): 188 """ 189 Record arbitrary metric values via as a Gauge. 190 A Gauge records any point-in-time measurement, such as the current CPU utilization %. 191 Values with the same metric name and attributes are aggregated via the OTel SDK. 192 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 193 :param name: the name of the metric. 194 :param value: the float value of the metric. 195 :param attributes: additional metadata which can be used to filter and group values. 196 :return: None 197 """ 198 _use_instance(lambda instance: instance.record_metric(name, value, attributes))
Record arbitrary metric values via as a Gauge. A Gauge records any point-in-time measurement, such as the current CPU utilization %. Values with the same metric name and attributes are aggregated via the OTel SDK. See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details.
Parameters
- name: the name of the metric.
- value: the float value of the metric.
- attributes: additional metadata which can be used to filter and group values.
Returns
None
201def record_count(name: str, value: int, attributes: typing.Optional[Attributes] = None): 202 """ 203 Record arbitrary metric values via as a Counter. 204 A Counter efficiently records an increment in a metric, such as number of cache hits. 205 Values with the same metric name and attributes are aggregated via the OTel SDK. 206 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 207 :param name: the name of the metric. 208 :param value: the float value of the metric. 209 :param attributes: additional metadata which can be used to filter and group values. 210 :return: None 211 """ 212 _use_instance(lambda instance: instance.record_count(name, value, attributes))
Record arbitrary metric values via as a Counter. A Counter efficiently records an increment in a metric, such as number of cache hits. Values with the same metric name and attributes are aggregated via the OTel SDK. See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details.
Parameters
- name: the name of the metric.
- value: the float value of the metric.
- attributes: additional metadata which can be used to filter and group values.
Returns
None
215def record_incr(name: str, attributes: typing.Optional[Attributes] = None): 216 """ 217 Record arbitrary metric +1 increment via as a Counter. 218 A Counter efficiently records an increment in a metric, such as number of cache hits. 219 Values with the same metric name and attributes are aggregated via the OTel SDK. 220 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 221 :param name: the name of the metric. 222 :param attributes: additional metadata which can be used to filter and group values. 223 :return: None 224 """ 225 _use_instance(lambda instance: instance.record_incr(name, attributes))
Record arbitrary metric +1 increment via as a Counter. A Counter efficiently records an increment in a metric, such as number of cache hits. Values with the same metric name and attributes are aggregated via the OTel SDK. See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details.
Parameters
- name: the name of the metric.
- attributes: additional metadata which can be used to filter and group values.
Returns
None
228def record_histogram( 229 name: str, value: float, attributes: typing.Optional[Attributes] = None 230): 231 """ 232 Record arbitrary metric values via as a Histogram. 233 A Histogram efficiently records near-by point-in-time measurement into a bucketed aggregate. 234 Values with the same metric name and attributes are aggregated via the OTel SDK. 235 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 236 :param name: the name of the metric. 237 :param value: the float value of the metric. 238 :param attributes: additional metadata which can be used to filter and group values. 239 :return: None 240 """ 241 _use_instance(lambda instance: instance.record_histogram(name, value, attributes))
Record arbitrary metric values via as a Histogram. A Histogram efficiently records near-by point-in-time measurement into a bucketed aggregate. Values with the same metric name and attributes are aggregated via the OTel SDK. See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details.
Parameters
- name: the name of the metric.
- value: the float value of the metric.
- attributes: additional metadata which can be used to filter and group values.
Returns
None
244def record_up_down_counter( 245 name: str, value: int, attributes: typing.Optional[Attributes] = None 246): 247 """ 248 Record arbitrary metric values via as a UpDownCounter. 249 A UpDownCounter efficiently records an increment or decrement in a metric, such as number of paying customers. 250 Values with the same metric name and attributes are aggregated via the OTel SDK. 251 See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details. 252 :param name: the name of the metric. 253 :param value: the float value of the metric. 254 :param attributes: additional metadata which can be used to filter and group values. 255 :return: None 256 """ 257 _use_instance( 258 lambda instance: instance.record_up_down_counter(name, value, attributes) 259 )
Record arbitrary metric values via as a UpDownCounter. A UpDownCounter efficiently records an increment or decrement in a metric, such as number of paying customers. Values with the same metric name and attributes are aggregated via the OTel SDK. See https://opentelemetry.io/docs/specs/otel/metrics/data-model/ for more details.
Parameters
- name: the name of the metric.
- value: the float value of the metric.
- attributes: additional metadata which can be used to filter and group values.
Returns
None
262def record_log( 263 message: str, 264 level: int, 265 attributes: typing.Optional[Attributes] = None, 266): 267 """ 268 Records a log. This log will be recorded to LaunchDarkly, but will not be send to other log handlers. 269 A Log records a message with a level and optional attributes. 270 :param message: the message to record. 271 :param level: the level of the log. 272 :param attributes: additional metadata which can be used to filter and group values. 273 :return: None 274 """ 275 _use_instance(lambda instance: instance.log(message, level, attributes))
Records a log. This log will be recorded to LaunchDarkly, but will not be send to other log handlers. A Log records a message with a level and optional attributes.
Parameters
- message: the message to record.
- level: the level of the log.
- attributes: additional metadata which can be used to filter and group values.
Returns
None
278def logging_handler() -> logging.Handler: 279 """A logging handler implementing `logging.Handler` that allows plugging LaunchDarkly Observability 280 into your existing logging setup. Standard logging will be automatically instrumented unless 281 :class:`ObservabilityConfig.instrument_logging <ldobserve.config.ObservabilityConfig.instrument_logging>` is set to False. 282 283 Example: 284 import ldobserve.observe as observe 285 from loguru import logger 286 287 # Observability plugin must be initialized. 288 # If the Observability plugin is not initialized, then a NullHandler will be returned. 289 290 logger.add( 291 observe.logging_handler(), 292 format="{message}", 293 level="INFO", 294 backtrace=True, 295 ) 296 """ 297 if not _instance: 298 return logging.NullHandler() 299 return _instance.log_handler
A logging handler implementing logging.Handler that allows plugging LaunchDarkly Observability
into your existing logging setup. Standard logging will be automatically instrumented unless
ObservabilityConfig.instrument_logging <ldobserve.config.ObservabilityConfig.instrument_logging> is set to False.
Example: import ldobserve.observe as observe from loguru import logger
# Observability plugin must be initialized.
# If the Observability plugin is not initialized, then a NullHandler will be returned.
logger.add(
observe.logging_handler(),
format="{message}",
level="INFO",
backtrace=True,
)
302@contextlib.contextmanager 303def start_span( 304 name: str, 305 attributes: Attributes = None, 306 record_exception: bool = True, 307 set_status_on_exception: bool = True, 308) -> typing.Iterator["Span"]: 309 """ 310 Context manager for creating a new span and setting it as the current span. 311 312 Exiting the context manager will call the span's end method, 313 as well as return the current span to its previous value by 314 returning to the previous context. 315 316 Args: 317 name: The name of the span. 318 attributes: The attributes of the span. 319 record_exception: Whether to record any exceptions raised within the 320 context as error event on the span. 321 set_status_on_exception: Only relevant if the returned span is used 322 in a with/context manager. Defines whether the span status will 323 324 Yields: 325 The newly-created span. 326 """ 327 if _instance: 328 with _instance.start_span( 329 name, 330 attributes=attributes, 331 record_exception=record_exception, 332 set_status_on_exception=set_status_on_exception, 333 ) as span: 334 yield span 335 else: 336 # If not initialized, then get a tracer and use it to create a span. 337 # We don't want to prevent user code from executing correctly if 338 # the plugin is not initialized. 339 logging.getLogger(__name__).warning( 340 "The observability singleton was used before it was initialized." 341 ) 342 with trace.get_tracer(__name__).start_as_current_span( 343 name, 344 attributes=attributes, 345 record_exception=record_exception, 346 set_status_on_exception=set_status_on_exception, 347 ) as span: 348 yield span
Context manager for creating a new span and setting it as the current span.
Exiting the context manager will call the span's end method, as well as return the current span to its previous value by returning to the previous context.
Args: name: The name of the span. attributes: The attributes of the span. record_exception: Whether to record any exceptions raised within the context as error event on the span. set_status_on_exception: Only relevant if the returned span is used in a with/context manager. Defines whether the span status will
Yields: The newly-created span.