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

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):
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

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):
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

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

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):
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

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):
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

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):
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

def logging_handler() -> logging.Handler:
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,
)
@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]:
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.

def is_initialized() -> bool:
351def is_initialized() -> bool:
352    return _instance != None