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