Skip to content

Commit

Permalink
Add opentelemetry]
Browse files Browse the repository at this point in the history
  • Loading branch information
insani7y committed Jul 13, 2024
1 parent 8ce347f commit 6916196
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 38 deletions.
13 changes: 10 additions & 3 deletions microbootstrap/bootstrappers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import typing

from microbootstrap.helpers import merge_dataclasses_configs, merge_dict_configs
from microbootstrap.instruments import SentryConfig
from microbootstrap.instruments import OpentelemetryConfig, SentryConfig


if typing.TYPE_CHECKING:
Expand All @@ -22,21 +22,28 @@ class ApplicationBootstrapper(typing.Protocol[SettingsT, ApplicationT, dataclass
settings: SettingsT
sentry_instrument_type: type[Instrument[SentryConfig]] = dataclasses.field(init=False)
sentry_instrument: Instrument[SentryConfig] = dataclasses.field(init=False)
opentelemetry_instrument_type: type[Instrument[OpentelemetryConfig]] = dataclasses.field(init=False)
opentelemetry_instrument: Instrument[OpentelemetryConfig] = dataclasses.field(init=False)
application_type: type[ApplicationT] = dataclasses.field(init=False)
application_config: dataclasses._DataclassT = dataclasses.field(init=False)

def __post_init__(self) -> None:
settings_dump = self.settings.model_dump()
self.sentry_instrument = self.sentry_instrument_type(SentryConfig(**settings_dump))
self.opentelemetry_instrument = self.opentelemetry_instrument_type(OpentelemetryConfig(**settings_dump))

@abc.abstractmethod
def configure_application(self: SelfT, application_config: dataclasses._DataclassT) -> SelfT:
self.application_config = merge_dataclasses_configs(self.application_config, application_config)
return self

@abc.abstractmethod
def configure_opentelemetry(self: SelfT) -> SelfT:
raise NotImplementedError
def configure_opentelemetry(
self: SelfT,
opentelemetry_config: OpentelemetryConfig,
) -> SelfT:
self.opentelemetry_instrument.configure_instrument(opentelemetry_config)
return self

@abc.abstractmethod
def configure_sentry(
Expand Down
41 changes: 20 additions & 21 deletions microbootstrap/bootstrappers/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import sentry_sdk
from litestar import status_codes
from litestar.config.app import AppConfig
from litestar.contrib.opentelemetry.config import OpenTelemetryConfig
from litestar.contrib.opentelemetry.middleware import OpenTelemetryInstrumentationMiddleware
from litestar.exceptions.http_exceptions import HTTPException

from microbootstrap.bootstrappers.base import ApplicationBootstrapper
from microbootstrap.instruments import SentryInstrument
from microbootstrap.instruments import OpentelemetryInstrument, SentryInstrument
from microbootstrap.settings.litestar import LitestarBootstrapSettings


Expand All @@ -19,32 +21,29 @@ async def sentry_exception_catcher_hook(
exception: Exception,
_request_scope: litestar.types.Scope,
) -> None:
if not isinstance(exception, HTTPException):
if (
not isinstance(exception, HTTPException)
or exception.status_code >= status_codes.HTTP_500_INTERNAL_SERVER_ERROR
):
sentry_sdk.capture_exception(exception)

if exception.status_code >= status_codes.HTTP_500_INTERNAL_SERVER_ERROR:
sentry_sdk.capture_exception(exception)

def bootstrap(self) -> dict[str, typing.Any]:
if not self.is_ready():
# TODO: use some logger # noqa: TD002
print("Sentry is not ready for bootstrapping. Provide a sentry_dsn") # noqa: T201
return {}

sentry_sdk.init(
dsn=self.instrument_config.sentry_dsn,
sample_rate=self.instrument_config.sentry_sample_rate,
traces_sample_rate=self.instrument_config.sentry_traces_sample_rate,
environment=self.instrument_config.sentry_environment,
max_breadcrumbs=self.instrument_config.sentry_additional_paramsmax_breadcrumbs,
attach_stacktrace=self.instrument_config.sentry_attach_stacktrace,
integrations=self.instrument_config.sentry_integrations,
**self.instrument_config.sentry_additional_params,
)
@property
def successful_bootstrap_result(self) -> dict[str, typing.Any]:
return {"after_exception": [self.sentry_exception_catcher_hook]}


class LitetstarOpentelemetryInstrument(OpentelemetryInstrument):
@property
def successful_bootstrap_result(self) -> dict[str, typing.Any]:
return {
"middleware": OpenTelemetryInstrumentationMiddleware(
OpenTelemetryConfig(tracer_provider=self.tracer_provider),
),
}


class LitestarBootstrapper(
ApplicationBootstrapper[LitestarBootstrapSettings, litestar.Litestar, AppConfig],
):
sentry_instrument_type = LitestarSentryInstrument
opentelemetry_instrument_type = LitetstarOpentelemetryInstrument
8 changes: 5 additions & 3 deletions microbootstrap/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@


if typing.TYPE_CHECKING:
from dataclasses import _DataclassT

from pydantic import BaseModel


Expand All @@ -24,9 +26,9 @@ def merge_pydantic_configs(


def merge_dataclasses_configs(
config_to_merge: dataclasses._DataclassT,
config_with_changes: dataclasses._DataclassT,
) -> dataclasses._DataclassT:
config_to_merge: "_DataclassT",
config_with_changes: "_DataclassT",
) -> "_DataclassT":
config_class: typing.Final = config_to_merge.__class__
resulting_dict_config: typing.Final = merge_dict_configs(
dataclasses.asdict(config_to_merge),
Expand Down
3 changes: 2 additions & 1 deletion microbootstrap/instruments/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Contains all instruments that can be used for bootstrapping."""

from microbootstrap.instruments.base import Instrument
from microbootstrap.instruments.opentelemery_instrument import OpentelemetryConfig, OpentelemetryInstrument
from microbootstrap.instruments.sentry_instrument import SentryConfig, SentryInstrument


__all__ = ("SentryInstrument", "SentryConfig", "Instrument")
__all__ = ("SentryInstrument", "SentryConfig", "Instrument", "OpentelemetryConfig", "OpentelemetryInstrument")
9 changes: 7 additions & 2 deletions microbootstrap/instruments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import dataclasses
import typing

from microbootstrap.helpers import merge_configs
from microbootstrap.helpers import merge_pydantic_configs


if typing.TYPE_CHECKING:
Expand All @@ -20,11 +20,16 @@ def configure_instrument(
self,
incoming_config: InstrumentConfigT,
) -> None:
self.instrument_config = merge_configs(self.instrument_config, incoming_config)
self.instrument_config = merge_pydantic_configs(self.instrument_config, incoming_config)

@property
def is_ready(self) -> bool:
raise NotImplementedError

@property
def successful_bootstrap_result(self) -> dict[str, typing.Any]:
raise NotImplementedError

def bootstrap(self) -> dict[str, typing.Any]:
raise NotImplementedError

Expand Down
66 changes: 66 additions & 0 deletions microbootstrap/instruments/opentelemery_instrument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations
import dataclasses
import typing

import pydantic
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk import resources
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from microbootstrap.instruments import Instrument


class OpentelemetryConfig(pydantic.BaseModel):
service_name: str | None = None
service_version: str | None = None
container_name: str | None = None
opentelemetry_endpoint: str | None = None
opentelemetry_namespace: str | None = None
opentelemetry_add_system_metrics: bool = dataclasses.field(default=False)
opentelemetry_insecure: bool = dataclasses.field(default=True)

class Config:
arbitrary_types_allowed = True


class OpentelemetryInstrument(Instrument[OpentelemetryConfig]):
@property
def is_ready(self) -> bool:
return all(
[
self.instrument_config.opentelemetry_endpoint,
self.instrument_config.opentelemetry_namespace,
self.instrument_config.service_name,
self.instrument_config.service_version,
self.instrument_config.container_name,
],
)

def teardown(self) -> None:
pass

def bootstrap(self) -> dict[str, typing.Any]:
if not self.is_ready:
print("Opentelemetry is not ready for bootstrapping. Provide required params.") # noqa: T201
return {}

resource: typing.Final = resources.Resource.create(
attributes={
resources.SERVICE_NAME: self.instrument_config.service_name,
resources.TELEMETRY_SDK_LANGUAGE: "python",
resources.SERVICE_NAMESPACE: self.instrument_config.opentelemetry_namespace,
resources.SERVICE_VERSION: self.instrument_config.service_version,
resources.CONTAINER_NAME: self.instrument_config.container_name,
},
)
self.tracer_provider = TracerProvider(resource=resource)
self.tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint=self.instrument_config.opentelemetry_endpoint,
insecure=self.instrument_config.opentelemetry_insecure,
),
),
)
return self.successful_bootstrap_result
32 changes: 26 additions & 6 deletions microbootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
import typing

import pydantic
import sentry_sdk
from sentry_sdk.integrations import Integration # noqa: TCH002

from microbootstrap.instruments.base import InstrumentABC


if typing.TYPE_CHECKING:
from sentry_sdk.integrations import Integration
from microbootstrap.instruments.base import Instrument


class SentryConfig(pydantic.BaseModel):
Expand All @@ -20,10 +18,32 @@ class SentryConfig(pydantic.BaseModel):
sentry_integrations: list[Integration] | None = None
sentry_additional_params: dict[str, typing.Any] | None = None

class Config:
arbitrary_types_allowed = True

class SentryInstrument(InstrumentABC[SentryConfig]):

class SentryInstrument(Instrument[SentryConfig]):
@property
def is_ready(self) -> bool:
return bool(self.instrument_config.sentry_dsn)

def teardown(self) -> None:
return

def bootstrap(self) -> dict[str, typing.Any]:
if not self.is_ready:
# TODO: use some logger # noqa: TD002
print("Sentry is not ready for bootstrapping. Provide a sentry_dsn") # noqa: T201
return {}

sentry_sdk.init(
dsn=self.instrument_config.sentry_dsn,
sample_rate=self.instrument_config.sentry_sample_rate,
traces_sample_rate=self.instrument_config.sentry_traces_sample_rate,
environment=self.instrument_config.sentry_environment,
max_breadcrumbs=self.instrument_config.sentry_additional_paramsmax_breadcrumbs,
attach_stacktrace=self.instrument_config.sentry_attach_stacktrace,
integrations=self.instrument_config.sentry_integrations,
**self.instrument_config.sentry_additional_params,
)
return self.successful_bootstrap_result
4 changes: 2 additions & 2 deletions microbootstrap/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

from microbootstrap.base.logging import base as logging_base
from microbootstrap.base.opentelemetry import OpenTelemetryInstrumentor # noqa: TCH001
from microbootstrap.instruments import SentryConfig
from microbootstrap.instruments import OpentelemetryConfig, SentryConfig


class BootstrapSettings(pydantic_settings.BaseSettings, SentryConfig):
class BootstrapSettings(pydantic_settings.BaseSettings, SentryConfig, OpentelemetryConfig):
debug: bool = False
app_environment: str | None = None
namespace: str = "default"
Expand Down

0 comments on commit 6916196

Please sign in to comment.