Settings¶
servicewright reads a small, fixed set of things off a settings object that you own and
construct. It never loads a .env, never reads os.environ, and never defines a settings class
you have to inherit from.
The protocol¶
class BaseServiceSettingsProtocol(Protocol):
logging: LoggingSettingsProtocol | None
metrics: MetricsSettingsProtocol | None
tracing: TracingSettingsProtocol | None
error_tracking: ErrorTrackingSettingsProtocol | None
def get_app_version(self) -> str: ...
Four optional sections and a version. That is all the kernel asks for.
Sections are named by concern, never by vendor. There is no sentry section — there is an
error_tracking section, and ObsConfig decides that Sentry is the backend reading it. Swapping
Sentry for something else does not touch your settings class.
The sections¶
logging¶
| Field | Type | Meaning |
|---|---|---|
level |
str |
Root log level: DEBUG, INFO, ... |
use_json |
bool |
JSON lines vs human-readable console output |
metrics¶
| Field | Type | Meaning |
|---|---|---|
enabled |
bool |
Start a standalone exposition server |
host |
str |
Bind host for that server |
port |
int |
Bind port for that server |
prefix |
str \| None |
Metric name prefix, for backends that use one |
Note
The built-in Prometheus backend does not apply prefix itself. Metric names belong to the
recorder that owns them, so prefixes are set there — for example
GrpcEntrypoint(metrics_prefix="myapp"). The field exists for backends that prefix
globally.
tracing¶
| Field | Type | Meaning |
|---|---|---|
service_name |
str |
Resource service name (falls back to AppSpec.service_name) |
collector_url |
str \| None |
OTLP endpoint; None means no exporter |
sample_ratio |
float |
Ratio for the parent-based sampler |
insecure |
bool |
Plaintext OTLP connection |
enable_console_exporter |
bool |
Also print spans to stdout |
excluded_urls |
str \| None |
Comma-separated paths to skip |
error_tracking¶
| Field | Type | Meaning |
|---|---|---|
dsn |
str \| None |
Reporting endpoint; empty means the concern is off |
environment |
str |
Environment tag |
traces_sample_rate |
float |
Performance-trace sampling |
profiles_sample_rate |
float |
Profiling sampling |
debug |
bool |
SDK debug mode |
Narrowing a section¶
Sections may declare types narrower than the tables above: a Literal level, a bounded
sample_ratio, a port newtype. Section members are read-only properties and nothing in
servicewright writes to them, so a narrowed model still satisfies the protocol. LogLevelStr is
exported for the log level:
from servicewright.core.observability import LogLevelStr
class LoggingSettings(BaseModel):
level: LogLevelStr = "INFO"
use_json: bool = True
Optional extras¶
settings.environment— if present, it is used as the environment for observability setup. Otherwiseerror_tracking.environmentis used.- Backends may read additional fields off their own section via
getattr. The protocols declare the minimum, not the maximum, so a custom backend can carry its own configuration in the same section.
None disables a concern¶
@dataclass(frozen=True)
class Settings:
logging: LoggingSettings | None = LoggingSettings()
metrics: None = None # off
tracing: None = None # off
error_tracking: None = None # off
def get_app_version(self) -> str:
return "1.0.0"
A concern that is None gets a null-object sink: emitters can call it freely and nothing happens.
Your code never has to check whether metrics are configured.
Error tracking is special: the section may exist while dsn is empty, which also counts as off.
With pydantic-settings¶
Anything with the right attribute names works. A typical real service:
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
class LoggingSettings(BaseModel):
level: str = "INFO"
use_json: bool = True
class MetricsSettings(BaseModel):
enabled: bool = False
host: str = "0.0.0.0"
port: int = 9090
prefix: str | None = None
class TracingSettings(BaseModel):
service_name: str = ""
collector_url: str | None = None
sample_ratio: float = 1.0
insecure: bool = True
enable_console_exporter: bool = False
excluded_urls: str | None = None
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_nested_delimiter="__", env_file=".env")
app_version: str = "0.0.0"
environment: str = "local"
logging: LoggingSettings = LoggingSettings()
metrics: MetricsSettings = MetricsSettings()
tracing: TracingSettings | None = None
error_tracking: None = None
def get_app_version(self) -> str:
return self.app_version
Note
servicewright never imports pydantic. The example above works because the attributes line up, not because of any integration.
What is not in settings¶
Server configuration is not. HttpConfig, GrpcConfig and LitestarConfig are passed to their
entrypoint at construction:
This is deliberate. AppSpec stays transport-neutral, which is what lets the same spec run an
API in one process and a worker in another. It also means two HTTP entrypoints in one process can
have different ports without inventing a settings namespace for each.
Of course, nothing stops you feeding your own settings into that config:
You just do it explicitly, where you can see it.
Next¶
- Observability — how
ObsConfigmaps backends onto these sections.