Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Configuration

## Service name and version

`service_name` and `service_version` default to `None` since 2.0. Left unset:

- OpenTelemetry leaves `service.name` and `service.version` out of the resource, so the SDK takes them from `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES`. With neither set, `service.name` is `unknown_service`.
- Sentry gets no `release` from lite-bootstrap, and sentry-sdk picks one itself, from `SENTRY_RELEASE`, the git revision or CI variables. A `service_version` you set is passed as `release` and wins over all of those.
- Everything else that needs a string uses `"micro-service"` and `"1.0.0"`, as before 2.0: the title and version of an app FastAPI builds for you, Litestar's OpenAPI title and version, Litestar's Prometheus `app_name` label, the health-check body, and Pyroscope's application name when `opentelemetry_service_name` is unset too.

## Debug mode

`service_debug` defaults to `False`. Debug mode puts tracebacks in error responses, so keep it off in production.
Expand All @@ -16,6 +24,8 @@ To bootstrap Sentry, you must provide at least:

- `sentry_dsn` - tells sentry-sdk where to send the events.

`service_version`, when set, is passed as `release`; see [Service name and version](#service-name-and-version).

Additional parameters can also be supplied through the settings object:

- `sentry_traces_sample_rate` - in the range of 0.0 to 1.0, the percentage chance a given transaction will be sent
Expand Down Expand Up @@ -295,8 +305,8 @@ OTEL_EXPERIMENTAL_RESOURCE_DETECTORS=process,os

The attributes lite-bootstrap sets (`service.name`, `service.namespace`, `service.version`,
`container.name`, and `deployment.environment.name` from `service_environment`) are merged last and override anything a detector or environment variable supplies.
`service_name` defaults to `"micro-service"`, so `OTEL_SERVICE_NAME` is overridden unless `service_name`
is empty: set `opentelemetry_service_name` or `service_name` instead.
`service_name` and `service_version` default to `None`, and an unset attribute is left out, so
`OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` can supply it.


## Pyroscope
Expand Down
4 changes: 2 additions & 2 deletions lite_bootstrap/bootstrappers/fastapi_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ def __post_init__(self) -> None:
# FastAPIConfig stays frozen for user-facing immutability; __post_init__ needs
# to set application after construction, so we bypass the freeze here.
object.__setattr__(self, "application", application)
application.title = self.service_name
application.title = self.service_name_or_default
application.debug = self.service_debug
application.version = self.service_version
application.version = self.service_version_or_default
elif self.application_kwargs:
warn_at_caller("application_kwargs must be used without application")

Expand Down
6 changes: 3 additions & 3 deletions lite_bootstrap/bootstrappers/litestar_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ class LitestarPrometheusController(PrometheusController):
"group_path": config.prometheus_group_path,
**config.prometheus_additional_params,
}
litestar_prometheus_config = PrometheusConfig(app_name=config.service_name, **prometheus_params)
litestar_prometheus_config = PrometheusConfig(app_name=config.service_name_or_default, **prometheus_params)

config.application_config.route_handlers.append(LitestarPrometheusController)
if config.is_prometheus_http_metrics_on(
Expand Down Expand Up @@ -466,8 +466,8 @@ def bootstrap(self) -> None:
)
config.application_config.openapi_config = OpenAPIConfig(
path=config.swagger_path,
title=config.service_name,
version=config.service_version,
title=config.service_name_or_default,
version=config.service_version_or_default,
description=config.service_description,
render_plugins=render_plugins,
**config.swagger_additional_params,
Expand Down
16 changes: 14 additions & 2 deletions lite_bootstrap/instruments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@
from lite_bootstrap.types import UNSET, UnsetType


_DEFAULT_SERVICE_NAME: typing.Final = "micro-service"
_DEFAULT_SERVICE_VERSION: typing.Final = "1.0.0"


@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class BaseConfig:
service_name: str = "micro-service"
service_name: str | None = None
service_description: str | None = None
service_version: str = "1.0.0"
service_version: str | None = None
service_environment: str | None = None
service_debug: bool = False
# Deprecated field name -> the field that replaced it. The deprecated field defaults to UNSET.
_renamed_fields: typing.ClassVar[Mapping[str, str]] = {}

@property
def service_name_or_default(self) -> str:
return self.service_name or _DEFAULT_SERVICE_NAME

@property
def service_version_or_default(self) -> str:
return self.service_version or _DEFAULT_SERVICE_VERSION

def __post_init__(self) -> None:
"""Terminate the MRO __post_init__ cascade safely, after forwarding deprecated field names.

Expand Down
4 changes: 2 additions & 2 deletions lite_bootstrap/instruments/healthchecks_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class HealthChecksConfig(BaseConfig):
@property
def health_check_data(self) -> HealthCheckTypedDict:
return {
"service_version": self.service_version,
"service_name": self.service_name,
"service_version": self.service_version_or_default,
"service_name": self.service_name_or_default,
"health_status": True,
}

Expand Down
2 changes: 1 addition & 1 deletion lite_bootstrap/instruments/pyroscope_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def bootstrap(self) -> None:
namespace = config.opentelemetry_namespace
tags = ({"service_namespace": namespace} if namespace else {}) | config.pyroscope_tags
pyroscope.configure(
application_name=config.opentelemetry_service_name or config.service_name,
application_name=config.opentelemetry_service_name or config.service_name_or_default,
server_address=config.pyroscope_endpoint,
sample_rate=config.pyroscope_sample_rate,
tags=tags,
Expand Down
2 changes: 2 additions & 0 deletions lite_bootstrap/instruments/sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ def _build_init_params(self) -> dict[str, typing.Any]:
enrich_sentry_event_from_structlog_log, config.sentry_before_send
),
}
if config.service_version:
init_params["release"] = config.service_version
init_params.update(config.sentry_additional_params)
return init_params

Expand Down
22 changes: 20 additions & 2 deletions tests/instruments/test_opentelemetry_instrument.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dataclasses
import logging
import os
import re
Expand Down Expand Up @@ -126,7 +127,7 @@ def test_opentelemetry_sampler_wins_over_sampler_env_vars(monkeypatch: pytest.Mo
@pytest.mark.parametrize(
("config", "expected_service_name"),
[
(OpenTelemetryConfig(opentelemetry_log_traces=True), "micro-service"),
(OpenTelemetryConfig(opentelemetry_log_traces=True), "from-env"),
(OpenTelemetryConfig(opentelemetry_log_traces=True, service_name="configured"), "configured"),
(
OpenTelemetryConfig(
Expand All @@ -136,7 +137,7 @@ def test_opentelemetry_sampler_wins_over_sampler_env_vars(monkeypatch: pytest.Mo
),
],
)
def test_opentelemetry_configured_service_name_wins_over_env(
def test_opentelemetry_service_name_falls_back_to_otel_service_name(
monkeypatch: pytest.MonkeyPatch, config: OpenTelemetryConfig, expected_service_name: str
) -> None:
monkeypatch.setenv("OTEL_SERVICE_NAME", "from-env")
Expand All @@ -149,6 +150,23 @@ def test_opentelemetry_configured_service_name_wins_over_env(
instrument.teardown()


@pytest.mark.parametrize(("service_version", "expected_service_version"), [("2.3.4", "2.3.4"), (None, "from-env")])
def test_opentelemetry_service_version_falls_back_to_otel_resource_attributes(
monkeypatch: pytest.MonkeyPatch, service_version: str | None, expected_service_version: str
) -> None:
monkeypatch.setenv("OTEL_RESOURCE_ATTRIBUTES", "service.version=from-env")
config = OpenTelemetryConfig(opentelemetry_log_traces=True)
if service_version is not None:
config = dataclasses.replace(config, service_version=service_version)
instrument = OpenTelemetryInstrument(bootstrap_config=config)
try:
instrument.bootstrap()
assert instrument._tracer_provider is not None # noqa: SLF001
assert instrument._tracer_provider.resource.attributes["service.version"] == expected_service_version # noqa: SLF001
finally:
instrument.teardown()


@pytest.mark.parametrize(("service_environment", "expected"), [("prod", "prod"), (None, None)])
def test_opentelemetry_resource_carries_deployment_environment(
service_environment: str | None, expected: str | None
Expand Down
7 changes: 7 additions & 0 deletions tests/instruments/test_pyroscope_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ def test_pyroscope_bootstrap_uses_opentelemetry_service_name() -> None:
assert mock_pyroscope.configure.call_args.kwargs["application_name"] == "otel-name"


def test_pyroscope_application_name_falls_back_when_service_name_unset() -> None:
config = FreeConfig(pyroscope_endpoint="http://pyroscope:4040")
with patch(_PYROSCOPE_PYROSCOPE) as mock_pyroscope:
PyroscopeInstrument(bootstrap_config=config).bootstrap()
assert mock_pyroscope.configure.call_args.kwargs["application_name"] == "micro-service"


def test_pyroscope_standalone_config_accepts_otel_fields() -> None:
config = PyroscopeConfig(
service_name="fallback",
Expand Down
31 changes: 31 additions & 0 deletions tests/instruments/test_sentry_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,34 @@ def test_sentry_additional_params_auto_enabling_integrations_wins_over_the_field
)

assert _active_integrations(bootstrap_config) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT


@pytest.mark.parametrize(
("service_version", "sentry_release_env", "expected_release"),
[
pytest.param("2.3.4", None, "2.3.4", id="service-version"),
pytest.param("2.3.4", "from-env", "2.3.4", id="service-version-wins-over-env"),
pytest.param(None, "from-env", "from-env", id="env-when-unset"),
],
)
def test_sentry_release_follows_service_version(
monkeypatch: pytest.MonkeyPatch,
minimal_sentry_config: SentryConfig,
service_version: str | None,
sentry_release_env: str | None,
expected_release: str,
) -> None:
if sentry_release_env is None:
monkeypatch.delenv("SENTRY_RELEASE", raising=False)
else:
monkeypatch.setenv("SENTRY_RELEASE", sentry_release_env)
config = minimal_sentry_config
if service_version is not None:
config = dataclasses.replace(config, service_version=service_version)
instrument = SentryInstrument(bootstrap_config=config)
instrument.bootstrap()

try:
assert sentry_sdk.get_client().options["release"] == expected_release
finally:
instrument.teardown()
28 changes: 21 additions & 7 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ def test_config_from_object() -> None:
def test_from_object_skips_none_attribute() -> None:
@dataclasses.dataclass
class Source:
service_name: str | None = None
service_debug: bool | None = None
service_version: str = "2.0.0"

config = BaseConfig.from_object(Source())
assert config.service_name == "micro-service"
assert config.service_debug is False
assert config.service_version == "2.0.0"


Expand All @@ -70,8 +70,8 @@ class Source:
pass

config = BaseConfig.from_object(Source())
assert config.service_name == "micro-service"
assert config.service_version == "1.0.0"
assert config.service_name is None
assert config.service_version is None
assert config.service_debug is False


Expand All @@ -89,12 +89,12 @@ class Source:
def test_from_dict_drops_unknown_keys_silently() -> None:
config = BaseConfig.from_dict({"service_name": "test", "unknown_key": "value"})
assert config.service_name == "test"
assert config.service_version == "1.0.0"
assert config.service_version is None


def test_from_dict_explicit_none_overrides_default() -> None:
config = BaseConfig.from_dict({"service_name": None})
assert config.service_name is None
config = BaseConfig.from_dict({"service_debug": None})
assert config.service_debug is None


@pytest.mark.parametrize("config_cls", [FreeConfig, LitestarConfig, FastStreamConfig, FastAPIConfig])
Expand All @@ -107,3 +107,17 @@ def test_otel_insecure_warning_fires_through_config_cascade(config_cls: type) ->
)
matching = [w for w in caught if "unencrypted" in str(w.message)]
assert matching, f"{config_cls.__name__}: {[str(w.message) for w in caught]}"


@pytest.mark.parametrize(
("config", "expected_name", "expected_version"),
[
pytest.param(BaseConfig(), "micro-service", "1.0.0", id="unset"),
pytest.param(BaseConfig(service_name="svc", service_version="2.3.4"), "svc", "2.3.4", id="set"),
],
)
def test_service_name_and_version_fall_back_for_readers_that_need_a_string(
config: BaseConfig, expected_name: str, expected_version: str
) -> None:
assert config.service_name_or_default == expected_name
assert config.service_version_or_default == expected_version
6 changes: 6 additions & 0 deletions tests/test_fastapi_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,3 +658,9 @@ def test_fastapi_health_check_runs_probes(
"service_version": "2.0.0",
}
assert failed_probe_names(probe_warnings) == expected_failed


def test_built_app_title_and_version_fall_back_when_unset() -> None:
application = FastAPIConfig().app
assert application.title == "micro-service"
assert application.version == "1.0.0"
10 changes: 10 additions & 0 deletions tests/test_litestar_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,3 +837,13 @@ def test_litestar_health_check_runs_probes(
"service_version": "2.0.0",
}
assert failed_probe_names(probe_warnings) == expected_failed


def test_litestar_openapi_and_prometheus_fall_back_to_default_service_name() -> None:
application = LitestarBootstrapper(bootstrap_config=LitestarConfig(logging_buffer_capacity=0)).bootstrap()
assert application.openapi_config is not None
assert application.openapi_config.title == "micro-service"
assert application.openapi_config.version == "1.0.0"
with TestClient(app=application) as client:
client.get("/health/")
assert 'app_name="micro-service"' in client.get("/metrics").text
Loading