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

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

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

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

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

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

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

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

def is_initialized() -> bool:
344def is_initialized() -> bool:
345    return _instance != None