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