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
def record_exception( error: Exception, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def record_metric( name: str, value: float, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def record_count( name: str, value: int, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def record_incr( name: str, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def record_histogram( name: str, value: float, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def record_up_down_counter( name: str, value: int, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def record_log( message: str, level: int, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = 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

def logging_handler() -> logging.Handler:
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,
)
@contextlib.contextmanager
def start_span( name: str, attributes: Optional[Mapping[str, Union[str, bool, int, float, Sequence[str], Sequence[bool], Sequence[int], Sequence[float]]]] = None, record_exception: bool = True, set_status_on_exception: bool = True) -> Iterator[opentelemetry.trace.span.Span]:
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.

def is_initialized() -> bool:
349def is_initialized() -> bool:
350    return _instance != None