lite-bootstrap 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/PKG-INFO +12 -1
- lite_bootstrap-0.4.0/lite_bootstrap/bootstraps/base.py +22 -0
- lite_bootstrap-0.4.0/lite_bootstrap/bootstraps/fastapi_bootstrap/__init__.py +33 -0
- lite_bootstrap-0.4.0/lite_bootstrap/bootstraps/fastapi_bootstrap/healthchecks_instrument.py +30 -0
- lite_bootstrap-0.4.0/lite_bootstrap/bootstraps/fastapi_bootstrap/logging_instrument.py +7 -0
- lite_bootstrap-0.4.0/lite_bootstrap/bootstraps/fastapi_bootstrap/opentelemetry_instrument.py +30 -0
- lite_bootstrap-0.4.0/lite_bootstrap/bootstraps/fastapi_bootstrap/sentry_instrument.py +7 -0
- lite_bootstrap-0.4.0/lite_bootstrap/instruments/base.py +13 -0
- lite_bootstrap-0.4.0/lite_bootstrap/instruments/healthchecks_instrument.py +30 -0
- lite_bootstrap-0.4.0/lite_bootstrap/instruments/logging_instrument.py +133 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/instruments/opentelemetry_instrument.py +34 -34
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/instruments/sentry_instrument.py +14 -11
- lite_bootstrap-0.4.0/lite_bootstrap/service_config.py +9 -0
- lite_bootstrap-0.4.0/lite_bootstrap/types.py +4 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/pyproject.toml +6 -0
- lite_bootstrap-0.4.0/tests/conftest.py +36 -0
- lite_bootstrap-0.4.0/tests/instruments/__init__.py +0 -0
- lite_bootstrap-0.4.0/tests/instruments/test_logging_instrument.py +83 -0
- lite_bootstrap-0.4.0/tests/instruments/test_opentelemetry_instrument.py +31 -0
- lite_bootstrap-0.4.0/tests/instruments/test_sentry_instrument.py +10 -0
- lite_bootstrap-0.4.0/tests/test_fastapi_bootstrap.py +48 -0
- lite_bootstrap-0.2.0/lite_bootstrap/bootstraps/base.py +0 -18
- lite_bootstrap-0.2.0/lite_bootstrap/bootstraps/fastapi_bootstrap.py +0 -51
- lite_bootstrap-0.2.0/lite_bootstrap/instruments/base.py +0 -12
- lite_bootstrap-0.2.0/tests/conftest.py +0 -27
- lite_bootstrap-0.2.0/tests/test_fastapi_bootstrap.py +0 -33
- lite_bootstrap-0.2.0/tests/test_opentelemetry_bootstrap.py +0 -24
- lite_bootstrap-0.2.0/tests/test_sentry_bootstrap.py +0 -9
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/.github/workflows/ci.yml +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/.github/workflows/publish.yml +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/.gitignore +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/Justfile +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/README.md +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/__init__.py +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/bootstraps/__init__.py +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/instruments/__init__.py +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/py.typed +0 -0
- {lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lite-bootstrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Lite package for bootstrapping new microservices
|
|
5
5
|
Project-URL: repository, https://github.com/modern-python/lite-bootstrap
|
|
6
6
|
Author-email: Artur Shiriev <me@shiriev.ru>
|
|
@@ -15,8 +15,19 @@ Classifier: Typing :: Typed
|
|
|
15
15
|
Requires-Python: <4,>=3.10
|
|
16
16
|
Provides-Extra: fastapi
|
|
17
17
|
Requires-Dist: fastapi; extra == 'fastapi'
|
|
18
|
+
Provides-Extra: fastapi-all
|
|
19
|
+
Requires-Dist: fastapi; extra == 'fastapi-all'
|
|
20
|
+
Requires-Dist: opentelemetry-api; extra == 'fastapi-all'
|
|
21
|
+
Requires-Dist: opentelemetry-exporter-otlp; extra == 'fastapi-all'
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation; extra == 'fastapi-all'
|
|
23
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'fastapi-all'
|
|
24
|
+
Requires-Dist: opentelemetry-sdk; extra == 'fastapi-all'
|
|
25
|
+
Requires-Dist: sentry-sdk; extra == 'fastapi-all'
|
|
26
|
+
Requires-Dist: structlog; extra == 'fastapi-all'
|
|
18
27
|
Provides-Extra: fastapi-otl
|
|
19
28
|
Requires-Dist: opentelemetry-instrumentation-fastapi; extra == 'fastapi-otl'
|
|
29
|
+
Provides-Extra: logging
|
|
30
|
+
Requires-Dist: structlog; extra == 'logging'
|
|
20
31
|
Provides-Extra: otl
|
|
21
32
|
Requires-Dist: opentelemetry-api; extra == 'otl'
|
|
22
33
|
Requires-Dist: opentelemetry-exporter-otlp; extra == 'otl'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from lite_bootstrap.instruments.base import BaseInstrument
|
|
5
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
6
|
+
from lite_bootstrap.types import ApplicationT
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseBootstrap(abc.ABC, typing.Generic[ApplicationT]):
|
|
10
|
+
application: ApplicationT
|
|
11
|
+
instruments: typing.Sequence[BaseInstrument]
|
|
12
|
+
service_config: ServiceConfig
|
|
13
|
+
|
|
14
|
+
def bootstrap(self) -> None:
|
|
15
|
+
for one_instrument in self.instruments:
|
|
16
|
+
if one_instrument.is_ready(self.service_config):
|
|
17
|
+
one_instrument.bootstrap(self.service_config, self.application)
|
|
18
|
+
|
|
19
|
+
def teardown(self) -> None:
|
|
20
|
+
for one_instrument in self.instruments:
|
|
21
|
+
if one_instrument.is_ready(self.service_config):
|
|
22
|
+
one_instrument.teardown(self.application)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import fastapi
|
|
5
|
+
|
|
6
|
+
from lite_bootstrap.bootstraps.base import BaseBootstrap
|
|
7
|
+
from lite_bootstrap.bootstraps.fastapi_bootstrap.healthchecks_instrument import FastAPIHealthChecksInstrument
|
|
8
|
+
from lite_bootstrap.bootstraps.fastapi_bootstrap.logging_instrument import FastAPILoggingInstrument
|
|
9
|
+
from lite_bootstrap.bootstraps.fastapi_bootstrap.opentelemetry_instrument import FastAPIOpenTelemetryInstrument
|
|
10
|
+
from lite_bootstrap.bootstraps.fastapi_bootstrap.sentry_instrument import FastAPISentryInstrument
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FastAPIBootstrap",
|
|
15
|
+
"FastAPIHealthChecksInstrument",
|
|
16
|
+
"FastAPILoggingInstrument",
|
|
17
|
+
"FastAPIOpenTelemetryInstrument",
|
|
18
|
+
"FastAPISentryInstrument",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
25
|
+
class FastAPIBootstrap(BaseBootstrap[fastapi.FastAPI]):
|
|
26
|
+
application: fastapi.FastAPI
|
|
27
|
+
instruments: typing.Sequence[
|
|
28
|
+
FastAPIOpenTelemetryInstrument
|
|
29
|
+
| FastAPISentryInstrument
|
|
30
|
+
| FastAPIHealthChecksInstrument
|
|
31
|
+
| FastAPILoggingInstrument
|
|
32
|
+
]
|
|
33
|
+
service_config: ServiceConfig
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import fastapi
|
|
5
|
+
|
|
6
|
+
from lite_bootstrap.instruments.healthchecks_instrument import HealthChecksInstrument, HealthCheckTypedDict
|
|
7
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
11
|
+
class FastAPIHealthChecksInstrument(HealthChecksInstrument):
|
|
12
|
+
enabled: bool = True
|
|
13
|
+
path: str = "/health/"
|
|
14
|
+
include_in_schema: bool = False
|
|
15
|
+
|
|
16
|
+
def build_fastapi_health_check_router(self, service_config: ServiceConfig) -> fastapi.APIRouter:
|
|
17
|
+
fastapi_router: typing.Final = fastapi.APIRouter(
|
|
18
|
+
tags=["probes"],
|
|
19
|
+
include_in_schema=self.include_in_schema,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@fastapi_router.get(self.path)
|
|
23
|
+
async def health_check_handler() -> HealthCheckTypedDict:
|
|
24
|
+
return self.render_health_check_data(service_config)
|
|
25
|
+
|
|
26
|
+
return fastapi_router
|
|
27
|
+
|
|
28
|
+
def bootstrap(self, service_config: ServiceConfig, application: fastapi.FastAPI | None = None) -> None:
|
|
29
|
+
if application:
|
|
30
|
+
application.include_router(self.build_fastapi_health_check_router(service_config))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import dataclasses
|
|
3
|
+
|
|
4
|
+
import fastapi
|
|
5
|
+
from opentelemetry.trace import get_tracer_provider
|
|
6
|
+
|
|
7
|
+
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrument
|
|
8
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
with contextlib.suppress(ImportError):
|
|
12
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
|
16
|
+
class FastAPIOpenTelemetryInstrument(OpenTelemetryInstrument):
|
|
17
|
+
excluded_urls: list[str] = dataclasses.field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
def bootstrap(self, service_config: ServiceConfig, application: fastapi.FastAPI | None = None) -> None:
|
|
20
|
+
super().bootstrap(service_config, application)
|
|
21
|
+
FastAPIInstrumentor.instrument_app(
|
|
22
|
+
app=application,
|
|
23
|
+
tracer_provider=get_tracer_provider(),
|
|
24
|
+
excluded_urls=",".join(self.excluded_urls),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def teardown(self, application: fastapi.FastAPI | None = None) -> None:
|
|
28
|
+
if application:
|
|
29
|
+
FastAPIInstrumentor.uninstrument_app(application)
|
|
30
|
+
super().teardown()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
|
|
3
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
4
|
+
from lite_bootstrap.types import ApplicationT
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseInstrument(abc.ABC):
|
|
8
|
+
def bootstrap(self, service_config: ServiceConfig, application: ApplicationT | None = None) -> None: ... # noqa: B027
|
|
9
|
+
|
|
10
|
+
def teardown(self, application: ApplicationT | None = None) -> None: ... # noqa: B027
|
|
11
|
+
|
|
12
|
+
@abc.abstractmethod
|
|
13
|
+
def is_ready(self, service_config: ServiceConfig) -> bool: ...
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
import typing_extensions
|
|
4
|
+
|
|
5
|
+
from lite_bootstrap.instruments.base import BaseInstrument
|
|
6
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HealthCheckTypedDict(typing_extensions.TypedDict, total=False):
|
|
10
|
+
service_version: str | None
|
|
11
|
+
service_name: str | None
|
|
12
|
+
health_status: bool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
16
|
+
class HealthChecksInstrument(BaseInstrument):
|
|
17
|
+
enabled: bool = True
|
|
18
|
+
path: str = "/health/"
|
|
19
|
+
include_in_schema: bool = False
|
|
20
|
+
|
|
21
|
+
def is_ready(self, _: ServiceConfig) -> bool:
|
|
22
|
+
return self.enabled
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def render_health_check_data(service_config: ServiceConfig) -> HealthCheckTypedDict:
|
|
26
|
+
return {
|
|
27
|
+
"service_version": service_config.service_version,
|
|
28
|
+
"service_name": service_config.service_name,
|
|
29
|
+
"health_status": True,
|
|
30
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import dataclasses
|
|
3
|
+
import logging
|
|
4
|
+
import logging.handlers
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
from lite_bootstrap.instruments.base import BaseInstrument
|
|
8
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
9
|
+
from lite_bootstrap.types import ApplicationT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from structlog.typing import EventDict, WrappedLogger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
with contextlib.suppress(ImportError):
|
|
17
|
+
import structlog
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ScopeType = typing.MutableMapping[str, typing.Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AddressProtocol(typing.Protocol):
|
|
24
|
+
host: str
|
|
25
|
+
port: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RequestProtocol(typing.Protocol):
|
|
29
|
+
client: AddressProtocol
|
|
30
|
+
scope: ScopeType
|
|
31
|
+
method: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def tracer_injection(_: "WrappedLogger", __: str, event_dict: "EventDict") -> "EventDict":
|
|
35
|
+
try:
|
|
36
|
+
from opentelemetry import trace
|
|
37
|
+
except ImportError: # pragma: no cover
|
|
38
|
+
return event_dict
|
|
39
|
+
|
|
40
|
+
event_dict["tracing"] = {}
|
|
41
|
+
current_span = trace.get_current_span()
|
|
42
|
+
if current_span == trace.INVALID_SPAN:
|
|
43
|
+
return event_dict
|
|
44
|
+
|
|
45
|
+
span_context = current_span.get_span_context()
|
|
46
|
+
if span_context == trace.INVALID_SPAN_CONTEXT: # pragma: no cover
|
|
47
|
+
return event_dict
|
|
48
|
+
|
|
49
|
+
event_dict["tracing"]["trace_id"] = format(span_context.span_id, "016x")
|
|
50
|
+
event_dict["tracing"]["span_id"] = format(span_context.trace_id, "032x")
|
|
51
|
+
|
|
52
|
+
return event_dict
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
DEFAULT_STRUCTLOG_PROCESSORS: typing.Final[list[typing.Any]] = [
|
|
56
|
+
structlog.stdlib.filter_by_level,
|
|
57
|
+
structlog.stdlib.add_log_level,
|
|
58
|
+
structlog.stdlib.add_logger_name,
|
|
59
|
+
tracer_injection,
|
|
60
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
61
|
+
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"),
|
|
62
|
+
structlog.processors.StackInfoRenderer(),
|
|
63
|
+
structlog.processors.format_exc_info,
|
|
64
|
+
structlog.processors.UnicodeDecoder(),
|
|
65
|
+
]
|
|
66
|
+
DEFAULT_STRUCTLOG_FORMATTER_PROCESSOR: typing.Final = structlog.processors.JSONRenderer()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MemoryLoggerFactory(structlog.stdlib.LoggerFactory):
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
*args: typing.Any, # noqa: ANN401
|
|
73
|
+
logging_buffer_capacity: int,
|
|
74
|
+
logging_flush_level: int,
|
|
75
|
+
logging_log_level: int,
|
|
76
|
+
log_stream: typing.Any = None, # noqa: ANN401
|
|
77
|
+
**kwargs: typing.Any, # noqa: ANN401
|
|
78
|
+
) -> None:
|
|
79
|
+
super().__init__(*args, **kwargs)
|
|
80
|
+
self.logging_buffer_capacity = logging_buffer_capacity
|
|
81
|
+
self.logging_flush_level = logging_flush_level
|
|
82
|
+
self.logging_log_level = logging_log_level
|
|
83
|
+
self.log_stream = log_stream
|
|
84
|
+
|
|
85
|
+
def __call__(self, *args: typing.Any) -> logging.Logger: # noqa: ANN401
|
|
86
|
+
logger: typing.Final = super().__call__(*args)
|
|
87
|
+
stream_handler: typing.Final = logging.StreamHandler(stream=self.log_stream)
|
|
88
|
+
handler: typing.Final = logging.handlers.MemoryHandler(
|
|
89
|
+
capacity=self.logging_buffer_capacity,
|
|
90
|
+
flushLevel=self.logging_flush_level,
|
|
91
|
+
target=stream_handler,
|
|
92
|
+
)
|
|
93
|
+
logger.addHandler(handler)
|
|
94
|
+
logger.setLevel(self.logging_log_level)
|
|
95
|
+
logger.propagate = False
|
|
96
|
+
return logger
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
100
|
+
class LoggingInstrument(BaseInstrument):
|
|
101
|
+
logging_log_level: int = logging.INFO
|
|
102
|
+
logging_flush_level: int = logging.ERROR
|
|
103
|
+
logging_buffer_capacity: int = 10
|
|
104
|
+
logging_extra_processors: list[typing.Any] = dataclasses.field(default_factory=list)
|
|
105
|
+
logging_unset_handlers: list[str] = dataclasses.field(
|
|
106
|
+
default_factory=list,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def is_ready(self, service_config: ServiceConfig) -> bool:
|
|
110
|
+
return not service_config.service_debug
|
|
111
|
+
|
|
112
|
+
def bootstrap(self, _: ServiceConfig, __: ApplicationT | None = None) -> None:
|
|
113
|
+
for unset_handlers_logger in self.logging_unset_handlers:
|
|
114
|
+
logging.getLogger(unset_handlers_logger).handlers = []
|
|
115
|
+
|
|
116
|
+
structlog.configure(
|
|
117
|
+
processors=[
|
|
118
|
+
*DEFAULT_STRUCTLOG_PROCESSORS,
|
|
119
|
+
*self.logging_extra_processors,
|
|
120
|
+
DEFAULT_STRUCTLOG_FORMATTER_PROCESSOR,
|
|
121
|
+
],
|
|
122
|
+
context_class=dict,
|
|
123
|
+
logger_factory=MemoryLoggerFactory(
|
|
124
|
+
logging_buffer_capacity=self.logging_buffer_capacity,
|
|
125
|
+
logging_flush_level=self.logging_flush_level,
|
|
126
|
+
logging_log_level=self.logging_log_level,
|
|
127
|
+
),
|
|
128
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
129
|
+
cache_logger_on_first_use=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def teardown(self, _: ApplicationT | None = None) -> None:
|
|
133
|
+
structlog.reset_defaults()
|
{lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/instruments/opentelemetry_instrument.py
RENAMED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import dataclasses
|
|
2
3
|
import typing
|
|
3
4
|
|
|
4
|
-
import
|
|
5
|
-
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
6
|
-
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
|
|
7
|
-
from opentelemetry.sdk import resources
|
|
8
|
-
from opentelemetry.sdk.trace import TracerProvider
|
|
9
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
5
|
+
from opentelemetry.trace import set_tracer_provider
|
|
10
6
|
|
|
11
7
|
from lite_bootstrap.instruments.base import BaseInstrument
|
|
8
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
9
|
+
from lite_bootstrap.types import ApplicationT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
with contextlib.suppress(ImportError):
|
|
13
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
14
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
|
|
15
|
+
from opentelemetry.sdk import resources
|
|
16
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
17
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
@@ -17,48 +23,34 @@ class InstrumentorWithParams:
|
|
|
17
23
|
additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
|
|
18
24
|
|
|
19
25
|
|
|
20
|
-
@dataclasses.dataclass(kw_only=True, slots=True)
|
|
26
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
21
27
|
class OpenTelemetryInstrument(BaseInstrument):
|
|
22
|
-
service_version: str = "1.0.0"
|
|
23
|
-
service_name: str | None = None
|
|
24
28
|
container_name: str | None = None
|
|
25
29
|
endpoint: str | None = None
|
|
26
30
|
namespace: str | None = None
|
|
27
|
-
insecure: bool =
|
|
31
|
+
insecure: bool = True
|
|
28
32
|
instrumentors: list[InstrumentorWithParams | BaseInstrumentor] = dataclasses.field(default_factory=list)
|
|
33
|
+
span_exporter: SpanExporter | None = None
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def is_ready(self) -> bool:
|
|
33
|
-
return all(
|
|
34
|
-
(
|
|
35
|
-
self.endpoint,
|
|
36
|
-
self.service_name,
|
|
37
|
-
),
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
def teardown(self) -> None:
|
|
41
|
-
for one_instrumentor in self.instrumentors:
|
|
42
|
-
if isinstance(one_instrumentor, InstrumentorWithParams):
|
|
43
|
-
one_instrumentor.instrumentor.uninstrument(**one_instrumentor.additional_params)
|
|
44
|
-
else:
|
|
45
|
-
one_instrumentor.uninstrument()
|
|
35
|
+
def is_ready(self, _: ServiceConfig) -> bool:
|
|
36
|
+
return bool(self.endpoint)
|
|
46
37
|
|
|
47
|
-
def bootstrap(self) -> None:
|
|
38
|
+
def bootstrap(self, service_config: ServiceConfig, _: ApplicationT | None = None) -> None:
|
|
48
39
|
attributes = {
|
|
49
|
-
resources.SERVICE_NAME:
|
|
40
|
+
resources.SERVICE_NAME: service_config.service_name,
|
|
50
41
|
resources.TELEMETRY_SDK_LANGUAGE: "python",
|
|
51
42
|
resources.SERVICE_NAMESPACE: self.namespace,
|
|
52
|
-
resources.SERVICE_VERSION:
|
|
43
|
+
resources.SERVICE_VERSION: service_config.service_version,
|
|
53
44
|
resources.CONTAINER_NAME: self.container_name,
|
|
54
45
|
}
|
|
55
46
|
resource: typing.Final = resources.Resource.create(
|
|
56
47
|
attributes={k: v for k, v in attributes.items() if v},
|
|
57
48
|
)
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
50
|
+
tracer_provider.add_span_processor(
|
|
60
51
|
BatchSpanProcessor(
|
|
61
|
-
|
|
52
|
+
self.span_exporter
|
|
53
|
+
or OTLPSpanExporter(
|
|
62
54
|
endpoint=self.endpoint,
|
|
63
55
|
insecure=self.insecure,
|
|
64
56
|
),
|
|
@@ -67,8 +59,16 @@ class OpenTelemetryInstrument(BaseInstrument):
|
|
|
67
59
|
for one_instrumentor in self.instrumentors:
|
|
68
60
|
if isinstance(one_instrumentor, InstrumentorWithParams):
|
|
69
61
|
one_instrumentor.instrumentor.instrument(
|
|
70
|
-
tracer_provider=
|
|
62
|
+
tracer_provider=tracer_provider,
|
|
71
63
|
**one_instrumentor.additional_params,
|
|
72
64
|
)
|
|
73
65
|
else:
|
|
74
|
-
one_instrumentor.instrument(tracer_provider=
|
|
66
|
+
one_instrumentor.instrument(tracer_provider=tracer_provider)
|
|
67
|
+
set_tracer_provider(tracer_provider)
|
|
68
|
+
|
|
69
|
+
def teardown(self, _: ApplicationT | None = None) -> None:
|
|
70
|
+
for one_instrumentor in self.instrumentors:
|
|
71
|
+
if isinstance(one_instrumentor, InstrumentorWithParams):
|
|
72
|
+
one_instrumentor.instrumentor.uninstrument(**one_instrumentor.additional_params)
|
|
73
|
+
else:
|
|
74
|
+
one_instrumentor.uninstrument()
|
{lite_bootstrap-0.2.0 → lite_bootstrap-0.4.0}/lite_bootstrap/instruments/sentry_instrument.py
RENAMED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import dataclasses
|
|
2
3
|
import typing
|
|
3
4
|
|
|
4
|
-
import pydantic
|
|
5
|
-
import sentry_sdk
|
|
6
|
-
from sentry_sdk.integrations import Integration
|
|
7
|
-
|
|
8
5
|
from lite_bootstrap.instruments.base import BaseInstrument
|
|
6
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
7
|
+
from lite_bootstrap.types import ApplicationT
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
with contextlib.suppress(ImportError):
|
|
11
|
+
import sentry_sdk
|
|
12
|
+
from sentry_sdk.integrations import Integration
|
|
9
13
|
|
|
10
14
|
|
|
11
|
-
@dataclasses.dataclass(kw_only=True, slots=True)
|
|
15
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
12
16
|
class SentryInstrument(BaseInstrument):
|
|
13
17
|
dsn: str | None = None
|
|
14
|
-
sample_rate: float =
|
|
18
|
+
sample_rate: float = dataclasses.field(default=1.0)
|
|
15
19
|
traces_sample_rate: float | None = None
|
|
16
|
-
environment: str | None = None
|
|
17
20
|
max_breadcrumbs: int = 15
|
|
18
21
|
max_value_length: int = 16384
|
|
19
22
|
attach_stacktrace: bool = True
|
|
@@ -21,15 +24,15 @@ class SentryInstrument(BaseInstrument):
|
|
|
21
24
|
additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
|
|
22
25
|
tags: dict[str, str] | None = None
|
|
23
26
|
|
|
24
|
-
def is_ready(self) -> bool:
|
|
27
|
+
def is_ready(self, _: ServiceConfig) -> bool:
|
|
25
28
|
return bool(self.dsn)
|
|
26
29
|
|
|
27
|
-
def bootstrap(self) -> None:
|
|
30
|
+
def bootstrap(self, service_config: ServiceConfig, _: ApplicationT | None = None) -> None:
|
|
28
31
|
sentry_sdk.init(
|
|
29
32
|
dsn=self.dsn,
|
|
30
33
|
sample_rate=self.sample_rate,
|
|
31
34
|
traces_sample_rate=self.traces_sample_rate,
|
|
32
|
-
environment=
|
|
35
|
+
environment=service_config.service_environment,
|
|
33
36
|
max_breadcrumbs=self.max_breadcrumbs,
|
|
34
37
|
max_value_length=self.max_value_length,
|
|
35
38
|
attach_stacktrace=self.attach_stacktrace,
|
|
@@ -39,4 +42,4 @@ class SentryInstrument(BaseInstrument):
|
|
|
39
42
|
tags: dict[str, str] = self.tags or {}
|
|
40
43
|
sentry_sdk.set_tags(tags)
|
|
41
44
|
|
|
42
|
-
def teardown(self) -> None: ...
|
|
45
|
+
def teardown(self, application: ApplicationT | None = None) -> None: ...
|
|
@@ -43,12 +43,18 @@ otl = [
|
|
|
43
43
|
"opentelemetry-exporter-otlp",
|
|
44
44
|
"opentelemetry-instrumentation",
|
|
45
45
|
]
|
|
46
|
+
logging = [
|
|
47
|
+
"structlog",
|
|
48
|
+
]
|
|
46
49
|
fastapi = [
|
|
47
50
|
"fastapi",
|
|
48
51
|
]
|
|
49
52
|
fastapi-otl = [
|
|
50
53
|
"opentelemetry-instrumentation-fastapi",
|
|
51
54
|
]
|
|
55
|
+
fastapi-all = [
|
|
56
|
+
"lite-bootstrap[sentry,otl,logging,fastapi,fastapi-otl]"
|
|
57
|
+
]
|
|
52
58
|
|
|
53
59
|
[dependency-groups]
|
|
54
60
|
dev = [
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
|
|
7
|
+
|
|
8
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomInstrumentor(BaseInstrumentor): # type: ignore[misc]
|
|
12
|
+
def instrumentation_dependencies(self) -> typing.Collection[str]:
|
|
13
|
+
return []
|
|
14
|
+
|
|
15
|
+
def _uninstrument(self, **kwargs: typing.Mapping[str, typing.Any]) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def fastapi_app() -> FastAPI:
|
|
21
|
+
return FastAPI()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def service_config() -> ServiceConfig:
|
|
26
|
+
return ServiceConfig(
|
|
27
|
+
service_name="microservice",
|
|
28
|
+
service_version="2.0.0",
|
|
29
|
+
service_environment="test",
|
|
30
|
+
service_debug=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(autouse=True)
|
|
35
|
+
def mock_sentry_init(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
36
|
+
monkeypatch.setattr("sentry_sdk.init", Mock)
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from io import StringIO
|
|
3
|
+
|
|
4
|
+
import structlog
|
|
5
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
|
|
6
|
+
from opentelemetry.trace import get_tracer
|
|
7
|
+
|
|
8
|
+
from lite_bootstrap.instruments.logging_instrument import LoggingInstrument, MemoryLoggerFactory
|
|
9
|
+
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrument
|
|
10
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = structlog.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_logging_instrument(service_config: ServiceConfig) -> None:
|
|
17
|
+
logging_instrument = LoggingInstrument(logging_unset_handlers=["uvicorn"], logging_buffer_capacity=0)
|
|
18
|
+
try:
|
|
19
|
+
logging_instrument.bootstrap(service_config)
|
|
20
|
+
logger.info("testing logging", key="value")
|
|
21
|
+
finally:
|
|
22
|
+
logging_instrument.teardown()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_logging_instrument_tracer_injection(service_config: ServiceConfig) -> None:
|
|
26
|
+
logging_instrument = LoggingInstrument(logging_unset_handlers=["uvicorn"], logging_buffer_capacity=0)
|
|
27
|
+
opentelemetry_instrument = OpenTelemetryInstrument(
|
|
28
|
+
endpoint="otl",
|
|
29
|
+
span_exporter=ConsoleSpanExporter(),
|
|
30
|
+
)
|
|
31
|
+
try:
|
|
32
|
+
logging_instrument.bootstrap(service_config)
|
|
33
|
+
opentelemetry_instrument.bootstrap(service_config)
|
|
34
|
+
tracer = get_tracer(__name__)
|
|
35
|
+
logger.info("testing tracer injection without spans")
|
|
36
|
+
with tracer.start_as_current_span("my_fake_span") as span:
|
|
37
|
+
logger.info("testing tracer injection without span attributes")
|
|
38
|
+
span.set_attribute("example_attribute", "value")
|
|
39
|
+
span.add_event("example_event", {"event_attr": 1})
|
|
40
|
+
logger.info("testing tracer injection with span attributes")
|
|
41
|
+
finally:
|
|
42
|
+
logging_instrument.teardown()
|
|
43
|
+
opentelemetry_instrument.teardown()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_memory_logger_factory_info() -> None:
|
|
47
|
+
test_capacity = 10
|
|
48
|
+
test_flush_level = logging.ERROR
|
|
49
|
+
test_stream = StringIO()
|
|
50
|
+
|
|
51
|
+
logger_factory = MemoryLoggerFactory(
|
|
52
|
+
logging_buffer_capacity=test_capacity,
|
|
53
|
+
logging_flush_level=test_flush_level,
|
|
54
|
+
logging_log_level=logging.INFO,
|
|
55
|
+
log_stream=test_stream,
|
|
56
|
+
)
|
|
57
|
+
test_logger = logger_factory()
|
|
58
|
+
test_message = "test message"
|
|
59
|
+
|
|
60
|
+
for current_log_index in range(test_capacity):
|
|
61
|
+
test_logger.info(test_message)
|
|
62
|
+
log_contents = test_stream.getvalue()
|
|
63
|
+
if current_log_index == test_capacity - 1:
|
|
64
|
+
assert test_message in log_contents
|
|
65
|
+
else:
|
|
66
|
+
assert not log_contents
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_memory_logger_factory_error() -> None:
|
|
70
|
+
test_capacity = 10
|
|
71
|
+
test_flush_level = logging.ERROR
|
|
72
|
+
test_stream = StringIO()
|
|
73
|
+
|
|
74
|
+
logger_factory = MemoryLoggerFactory(
|
|
75
|
+
logging_buffer_capacity=test_capacity,
|
|
76
|
+
logging_flush_level=test_flush_level,
|
|
77
|
+
logging_log_level=logging.INFO,
|
|
78
|
+
log_stream=test_stream,
|
|
79
|
+
)
|
|
80
|
+
test_logger = logger_factory()
|
|
81
|
+
error_message = "error message"
|
|
82
|
+
test_logger.error(error_message)
|
|
83
|
+
assert error_message in test_stream.getvalue()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
|
|
2
|
+
|
|
3
|
+
from lite_bootstrap.instruments.opentelemetry_instrument import InstrumentorWithParams, OpenTelemetryInstrument
|
|
4
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
5
|
+
from tests.conftest import CustomInstrumentor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_opentelemetry_instrument(service_config: ServiceConfig) -> None:
|
|
9
|
+
opentelemetry_instrument = OpenTelemetryInstrument(
|
|
10
|
+
endpoint="otl",
|
|
11
|
+
instrumentors=[
|
|
12
|
+
InstrumentorWithParams(instrumentor=CustomInstrumentor(), additional_params={"key": "value"}),
|
|
13
|
+
CustomInstrumentor(),
|
|
14
|
+
],
|
|
15
|
+
span_exporter=ConsoleSpanExporter(),
|
|
16
|
+
)
|
|
17
|
+
try:
|
|
18
|
+
opentelemetry_instrument.bootstrap(service_config)
|
|
19
|
+
finally:
|
|
20
|
+
opentelemetry_instrument.teardown()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_opentelemetry_instrument_empty_instruments(service_config: ServiceConfig) -> None:
|
|
24
|
+
opentelemetry_instrument = OpenTelemetryInstrument(
|
|
25
|
+
endpoint="otl",
|
|
26
|
+
span_exporter=ConsoleSpanExporter(),
|
|
27
|
+
)
|
|
28
|
+
try:
|
|
29
|
+
opentelemetry_instrument.bootstrap(service_config)
|
|
30
|
+
finally:
|
|
31
|
+
opentelemetry_instrument.teardown()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from lite_bootstrap.instruments.sentry_instrument import SentryInstrument
|
|
2
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_sentry_instrument(service_config: ServiceConfig) -> None:
|
|
6
|
+
SentryInstrument(dsn="https://testdsn@localhost/1", tags={"tag": "value"}).bootstrap(service_config)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_sentry_instrument_empty_dsn(service_config: ServiceConfig) -> None:
|
|
10
|
+
SentryInstrument(dsn="").bootstrap(service_config)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
from fastapi import FastAPI
|
|
3
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
|
|
4
|
+
from starlette import status
|
|
5
|
+
from starlette.testclient import TestClient
|
|
6
|
+
|
|
7
|
+
from lite_bootstrap.bootstraps.fastapi_bootstrap import (
|
|
8
|
+
FastAPIBootstrap,
|
|
9
|
+
FastAPIHealthChecksInstrument,
|
|
10
|
+
FastAPILoggingInstrument,
|
|
11
|
+
FastAPIOpenTelemetryInstrument,
|
|
12
|
+
FastAPISentryInstrument,
|
|
13
|
+
)
|
|
14
|
+
from lite_bootstrap.service_config import ServiceConfig
|
|
15
|
+
from tests.conftest import CustomInstrumentor
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = structlog.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_fastapi_bootstrap(fastapi_app: FastAPI, service_config: ServiceConfig) -> None:
|
|
22
|
+
fastapi_bootstrap = FastAPIBootstrap(
|
|
23
|
+
application=fastapi_app,
|
|
24
|
+
service_config=service_config,
|
|
25
|
+
instruments=[
|
|
26
|
+
FastAPIOpenTelemetryInstrument(
|
|
27
|
+
endpoint="otl",
|
|
28
|
+
instrumentors=[CustomInstrumentor()],
|
|
29
|
+
span_exporter=ConsoleSpanExporter(),
|
|
30
|
+
),
|
|
31
|
+
FastAPISentryInstrument(
|
|
32
|
+
dsn="https://testdsn@localhost/1",
|
|
33
|
+
),
|
|
34
|
+
FastAPIHealthChecksInstrument(
|
|
35
|
+
path="/health/",
|
|
36
|
+
),
|
|
37
|
+
FastAPILoggingInstrument(logging_buffer_capacity=0),
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
fastapi_bootstrap.bootstrap()
|
|
41
|
+
logger.info("testing logging", key="value")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
response = TestClient(fastapi_app).get("/health/")
|
|
45
|
+
assert response.status_code == status.HTTP_200_OK
|
|
46
|
+
assert response.json() == {"health_status": True, "service_name": "microservice", "service_version": "2.0.0"}
|
|
47
|
+
finally:
|
|
48
|
+
fastapi_bootstrap.teardown()
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import abc
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
from lite_bootstrap.instruments.base import BaseInstrument
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class BaseBootstrap(abc.ABC):
|
|
8
|
-
instruments: typing.Sequence[BaseInstrument]
|
|
9
|
-
|
|
10
|
-
def bootstrap(self) -> None:
|
|
11
|
-
for one_instrument in self.instruments:
|
|
12
|
-
if one_instrument.is_ready():
|
|
13
|
-
one_instrument.bootstrap()
|
|
14
|
-
|
|
15
|
-
def teardown(self) -> None:
|
|
16
|
-
for one_instrument in self.instruments:
|
|
17
|
-
if one_instrument.is_ready():
|
|
18
|
-
one_instrument.teardown()
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import dataclasses
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
import fastapi
|
|
5
|
-
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
6
|
-
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
|
7
|
-
|
|
8
|
-
from lite_bootstrap.bootstraps.base import BaseBootstrap
|
|
9
|
-
from lite_bootstrap.instruments.opentelemetry_instrument import OpenTelemetryInstrument
|
|
10
|
-
from lite_bootstrap.instruments.sentry_instrument import SentryInstrument
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclasses.dataclass(kw_only=True)
|
|
14
|
-
class FastAPIOpenTelemetryInstrument(OpenTelemetryInstrument):
|
|
15
|
-
excluded_urls: list[str] = dataclasses.field(default_factory=list)
|
|
16
|
-
app: fastapi.FastAPI = dataclasses.field(init=False)
|
|
17
|
-
|
|
18
|
-
def bootstrap(self) -> None:
|
|
19
|
-
super().bootstrap()
|
|
20
|
-
FastAPIInstrumentor.instrument_app(
|
|
21
|
-
app=self.app,
|
|
22
|
-
tracer_provider=self.tracer_provider,
|
|
23
|
-
excluded_urls=",".join(self.excluded_urls),
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
def teardown(self) -> None:
|
|
27
|
-
FastAPIInstrumentor.uninstrument_app(self.app)
|
|
28
|
-
super().teardown()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclasses.dataclass(kw_only=True)
|
|
32
|
-
class FastAPISentryInstrument(SentryInstrument):
|
|
33
|
-
app: fastapi.FastAPI = dataclasses.field(init=False)
|
|
34
|
-
|
|
35
|
-
def bootstrap(self) -> None:
|
|
36
|
-
super().bootstrap()
|
|
37
|
-
self.app.add_middleware(SentryAsgiMiddleware) # type: ignore[arg-type]
|
|
38
|
-
|
|
39
|
-
def teardown(self) -> None:
|
|
40
|
-
FastAPIInstrumentor.uninstrument_app(self.app)
|
|
41
|
-
super().teardown()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
45
|
-
class FastAPIBootstrap(BaseBootstrap):
|
|
46
|
-
app: fastapi.FastAPI
|
|
47
|
-
instruments: typing.Sequence[FastAPIOpenTelemetryInstrument | FastAPISentryInstrument]
|
|
48
|
-
|
|
49
|
-
def __post_init__(self) -> None:
|
|
50
|
-
for one_instrument in self.instruments:
|
|
51
|
-
one_instrument.app = self.app
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import typing
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from fastapi import APIRouter, FastAPI
|
|
5
|
-
from fastapi.responses import JSONResponse
|
|
6
|
-
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class CustomInstrumentor(BaseInstrumentor): # type: ignore[misc]
|
|
10
|
-
def instrumentation_dependencies(self) -> typing.Collection[str]:
|
|
11
|
-
return []
|
|
12
|
-
|
|
13
|
-
def _uninstrument(self, **kwargs: typing.Mapping[str, typing.Any]) -> None:
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture
|
|
18
|
-
def fastapi_app() -> FastAPI:
|
|
19
|
-
app: typing.Final = FastAPI()
|
|
20
|
-
router: typing.Final = APIRouter()
|
|
21
|
-
|
|
22
|
-
@router.get("/test")
|
|
23
|
-
async def for_test_endpoint() -> JSONResponse:
|
|
24
|
-
return JSONResponse(content={"key": "value"})
|
|
25
|
-
|
|
26
|
-
app.include_router(router)
|
|
27
|
-
return app
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from fastapi import FastAPI
|
|
2
|
-
from starlette import status
|
|
3
|
-
from starlette.testclient import TestClient
|
|
4
|
-
|
|
5
|
-
from lite_bootstrap.bootstraps.fastapi_bootstrap import (
|
|
6
|
-
FastAPIBootstrap,
|
|
7
|
-
FastAPIOpenTelemetryInstrument,
|
|
8
|
-
FastAPISentryInstrument,
|
|
9
|
-
)
|
|
10
|
-
from tests.conftest import CustomInstrumentor
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def test_fastapi_bootstrap(fastapi_app: FastAPI) -> None:
|
|
14
|
-
fastapi_bootstrap = FastAPIBootstrap(
|
|
15
|
-
app=fastapi_app,
|
|
16
|
-
instruments=[
|
|
17
|
-
FastAPIOpenTelemetryInstrument(
|
|
18
|
-
endpoint="localhost",
|
|
19
|
-
service_name="test_service",
|
|
20
|
-
instrumentors=[CustomInstrumentor()],
|
|
21
|
-
),
|
|
22
|
-
FastAPISentryInstrument(
|
|
23
|
-
dsn="https://testdsn@test.sentry.com/1",
|
|
24
|
-
),
|
|
25
|
-
],
|
|
26
|
-
)
|
|
27
|
-
fastapi_bootstrap.bootstrap()
|
|
28
|
-
fastapi_bootstrap.teardown()
|
|
29
|
-
|
|
30
|
-
response = TestClient(fastapi_app).get("/test")
|
|
31
|
-
assert response.status_code == status.HTTP_200_OK
|
|
32
|
-
|
|
33
|
-
fastapi_bootstrap.teardown()
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from lite_bootstrap.instruments.opentelemetry_instrument import InstrumentorWithParams, OpenTelemetryInstrument
|
|
2
|
-
from tests.conftest import CustomInstrumentor
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def test_bootstrap_opentelemetry() -> None:
|
|
6
|
-
opentelemetry = OpenTelemetryInstrument(
|
|
7
|
-
endpoint="localhost",
|
|
8
|
-
service_name="test_service",
|
|
9
|
-
instrumentors=[
|
|
10
|
-
InstrumentorWithParams(instrumentor=CustomInstrumentor(), additional_params={"key": "value"}),
|
|
11
|
-
CustomInstrumentor(),
|
|
12
|
-
],
|
|
13
|
-
)
|
|
14
|
-
opentelemetry.bootstrap()
|
|
15
|
-
opentelemetry.teardown()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_bootstrap_opentelemetry_empty_instruments() -> None:
|
|
19
|
-
opentelemetry = OpenTelemetryInstrument(
|
|
20
|
-
endpoint="localhost",
|
|
21
|
-
service_name="test_service",
|
|
22
|
-
)
|
|
23
|
-
opentelemetry.bootstrap()
|
|
24
|
-
opentelemetry.teardown()
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from lite_bootstrap.instruments.sentry_instrument import SentryInstrument
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_sentry_bootstrap() -> None:
|
|
5
|
-
SentryInstrument(dsn="https://testdsn@test.sentry.com/1", tags={"tag": "value"}).bootstrap()
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_sentry_bootstrap_empty_dsn() -> None:
|
|
9
|
-
SentryInstrument(dsn="").bootstrap()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|