Skip to content

Commit 02d3b22

Browse files
authored
Honour OTEL_SERVICE_NAME and send service_version as the Sentry release (#373)
1 parent ecead76 commit 02d3b22

13 files changed

Lines changed: 131 additions & 21 deletions

‎docs/introduction/configuration.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Configuration
22

3+
## Service name and version
4+
5+
`service_name` and `service_version` default to `None` since 2.0. Left unset:
6+
7+
- 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`.
8+
- 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.
9+
- 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.
10+
311
## Debug mode
412

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

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

27+
`service_version`, when set, is passed as `release`; see [Service name and version](#service-name-and-version).
28+
1929
Additional parameters can also be supplied through the settings object:
2030

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

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

301311

302312
## Pyroscope

‎lite_bootstrap/bootstrappers/fastapi_bootstrapper.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ def __post_init__(self) -> None:
9393
# FastAPIConfig stays frozen for user-facing immutability; __post_init__ needs
9494
# to set application after construction, so we bypass the freeze here.
9595
object.__setattr__(self, "application", application)
96-
application.title = self.service_name
96+
application.title = self.service_name_or_default
9797
application.debug = self.service_debug
98-
application.version = self.service_version
98+
application.version = self.service_version_or_default
9999
elif self.application_kwargs:
100100
warn_at_caller("application_kwargs must be used without application")
101101

‎lite_bootstrap/bootstrappers/litestar_bootstrapper.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ class LitestarPrometheusController(PrometheusController):
414414
"group_path": config.prometheus_group_path,
415415
**config.prometheus_additional_params,
416416
}
417-
litestar_prometheus_config = PrometheusConfig(app_name=config.service_name, **prometheus_params)
417+
litestar_prometheus_config = PrometheusConfig(app_name=config.service_name_or_default, **prometheus_params)
418418

419419
config.application_config.route_handlers.append(LitestarPrometheusController)
420420
if config.is_prometheus_http_metrics_on(
@@ -466,8 +466,8 @@ def bootstrap(self) -> None:
466466
)
467467
config.application_config.openapi_config = OpenAPIConfig(
468468
path=config.swagger_path,
469-
title=config.service_name,
470-
version=config.service_version,
469+
title=config.service_name_or_default,
470+
version=config.service_version_or_default,
471471
description=config.service_description,
472472
render_plugins=render_plugins,
473473
**config.swagger_additional_params,

‎lite_bootstrap/instruments/base.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,28 @@
77
from lite_bootstrap.types import UNSET, UnsetType
88

99

10+
_DEFAULT_SERVICE_NAME: typing.Final = "micro-service"
11+
_DEFAULT_SERVICE_VERSION: typing.Final = "1.0.0"
12+
13+
1014
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
1115
class BaseConfig:
12-
service_name: str = "micro-service"
16+
service_name: str | None = None
1317
service_description: str | None = None
14-
service_version: str = "1.0.0"
18+
service_version: str | None = None
1519
service_environment: str | None = None
1620
service_debug: bool = False
1721
# Deprecated field name -> the field that replaced it. The deprecated field defaults to UNSET.
1822
_renamed_fields: typing.ClassVar[Mapping[str, str]] = {}
1923

24+
@property
25+
def service_name_or_default(self) -> str:
26+
return self.service_name or _DEFAULT_SERVICE_NAME
27+
28+
@property
29+
def service_version_or_default(self) -> str:
30+
return self.service_version or _DEFAULT_SERVICE_VERSION
31+
2032
def __post_init__(self) -> None:
2133
"""Terminate the MRO __post_init__ cascade safely, after forwarding deprecated field names.
2234

‎lite_bootstrap/instruments/healthchecks_instrument.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class HealthChecksConfig(BaseConfig):
3030
@property
3131
def health_check_data(self) -> HealthCheckTypedDict:
3232
return {
33-
"service_version": self.service_version,
34-
"service_name": self.service_name,
33+
"service_version": self.service_version_or_default,
34+
"service_name": self.service_name_or_default,
3535
"health_status": True,
3636
}
3737

‎lite_bootstrap/instruments/pyroscope_instrument.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def bootstrap(self) -> None:
4141
namespace = config.opentelemetry_namespace
4242
tags = ({"service_namespace": namespace} if namespace else {}) | config.pyroscope_tags
4343
pyroscope.configure(
44-
application_name=config.opentelemetry_service_name or config.service_name,
44+
application_name=config.opentelemetry_service_name or config.service_name_or_default,
4545
server_address=config.pyroscope_endpoint,
4646
sample_rate=config.pyroscope_sample_rate,
4747
tags=tags,

‎lite_bootstrap/instruments/sentry_instrument.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ def _build_init_params(self) -> dict[str, typing.Any]:
250250
enrich_sentry_event_from_structlog_log, config.sentry_before_send
251251
),
252252
}
253+
if config.service_version:
254+
init_params["release"] = config.service_version
253255
init_params.update(config.sentry_additional_params)
254256
return init_params
255257

‎tests/instruments/test_opentelemetry_instrument.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dataclasses
12
import logging
23
import os
34
import re
@@ -126,7 +127,7 @@ def test_opentelemetry_sampler_wins_over_sampler_env_vars(monkeypatch: pytest.Mo
126127
@pytest.mark.parametrize(
127128
("config", "expected_service_name"),
128129
[
129-
(OpenTelemetryConfig(opentelemetry_log_traces=True), "micro-service"),
130+
(OpenTelemetryConfig(opentelemetry_log_traces=True), "from-env"),
130131
(OpenTelemetryConfig(opentelemetry_log_traces=True, service_name="configured"), "configured"),
131132
(
132133
OpenTelemetryConfig(
@@ -136,7 +137,7 @@ def test_opentelemetry_sampler_wins_over_sampler_env_vars(monkeypatch: pytest.Mo
136137
),
137138
],
138139
)
139-
def test_opentelemetry_configured_service_name_wins_over_env(
140+
def test_opentelemetry_service_name_falls_back_to_otel_service_name(
140141
monkeypatch: pytest.MonkeyPatch, config: OpenTelemetryConfig, expected_service_name: str
141142
) -> None:
142143
monkeypatch.setenv("OTEL_SERVICE_NAME", "from-env")
@@ -149,6 +150,23 @@ def test_opentelemetry_configured_service_name_wins_over_env(
149150
instrument.teardown()
150151

151152

153+
@pytest.mark.parametrize(("service_version", "expected_service_version"), [("2.3.4", "2.3.4"), (None, "from-env")])
154+
def test_opentelemetry_service_version_falls_back_to_otel_resource_attributes(
155+
monkeypatch: pytest.MonkeyPatch, service_version: str | None, expected_service_version: str
156+
) -> None:
157+
monkeypatch.setenv("OTEL_RESOURCE_ATTRIBUTES", "service.version=from-env")
158+
config = OpenTelemetryConfig(opentelemetry_log_traces=True)
159+
if service_version is not None:
160+
config = dataclasses.replace(config, service_version=service_version)
161+
instrument = OpenTelemetryInstrument(bootstrap_config=config)
162+
try:
163+
instrument.bootstrap()
164+
assert instrument._tracer_provider is not None # noqa: SLF001
165+
assert instrument._tracer_provider.resource.attributes["service.version"] == expected_service_version # noqa: SLF001
166+
finally:
167+
instrument.teardown()
168+
169+
152170
@pytest.mark.parametrize(("service_environment", "expected"), [("prod", "prod"), (None, None)])
153171
def test_opentelemetry_resource_carries_deployment_environment(
154172
service_environment: str | None, expected: str | None

‎tests/instruments/test_pyroscope_instrument.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ def test_pyroscope_bootstrap_uses_opentelemetry_service_name() -> None:
7575
assert mock_pyroscope.configure.call_args.kwargs["application_name"] == "otel-name"
7676

7777

78+
def test_pyroscope_application_name_falls_back_when_service_name_unset() -> None:
79+
config = FreeConfig(pyroscope_endpoint="http://pyroscope:4040")
80+
with patch(_PYROSCOPE_PYROSCOPE) as mock_pyroscope:
81+
PyroscopeInstrument(bootstrap_config=config).bootstrap()
82+
assert mock_pyroscope.configure.call_args.kwargs["application_name"] == "micro-service"
83+
84+
7885
def test_pyroscope_standalone_config_accepts_otel_fields() -> None:
7986
config = PyroscopeConfig(
8087
service_name="fallback",

‎tests/instruments/test_sentry_instrument.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,34 @@ def test_sentry_additional_params_auto_enabling_integrations_wins_over_the_field
463463
)
464464

465465
assert _active_integrations(bootstrap_config) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT
466+
467+
468+
@pytest.mark.parametrize(
469+
("service_version", "sentry_release_env", "expected_release"),
470+
[
471+
pytest.param("2.3.4", None, "2.3.4", id="service-version"),
472+
pytest.param("2.3.4", "from-env", "2.3.4", id="service-version-wins-over-env"),
473+
pytest.param(None, "from-env", "from-env", id="env-when-unset"),
474+
],
475+
)
476+
def test_sentry_release_follows_service_version(
477+
monkeypatch: pytest.MonkeyPatch,
478+
minimal_sentry_config: SentryConfig,
479+
service_version: str | None,
480+
sentry_release_env: str | None,
481+
expected_release: str,
482+
) -> None:
483+
if sentry_release_env is None:
484+
monkeypatch.delenv("SENTRY_RELEASE", raising=False)
485+
else:
486+
monkeypatch.setenv("SENTRY_RELEASE", sentry_release_env)
487+
config = minimal_sentry_config
488+
if service_version is not None:
489+
config = dataclasses.replace(config, service_version=service_version)
490+
instrument = SentryInstrument(bootstrap_config=config)
491+
instrument.bootstrap()
492+
493+
try:
494+
assert sentry_sdk.get_client().options["release"] == expected_release
495+
finally:
496+
instrument.teardown()

0 commit comments

Comments
 (0)