Log Service
The log service: the platform’s pod logs, from the log store.
The Helm chart keeps the last 72 hours of the logs of the app, the Celery workers, beat and
MariaDB in Loki, which Grafana Alloy fills. Alloy labels each line with its
component (the pod’s app.kubernetes.io/name, e.g. smarter-worker), pod and
container, and redacts email addresses, IP addresses, tokens, api keys and passwords
before it is stored. See helm/charts/smarter/values.yaml.
Loki does not depend on a cloud, so LogService is implemented by
LokiLogService, at smarter_settings.loki_url, for every cloud provider. Tests and
local development use InMemoryLogService, installed with configure_logs().
from datetime import timedelta
from smarter.apps.infrastructure.services import infrastructure
from smarter.apps.infrastructure.services.logs import LogQuery
lines = infrastructure.logs.query(
LogQuery.last(timedelta(hours=1), components=("smarter-worker",), contains=("ERROR",))
)
A LogQuery is structured, not LogQL: the service builds the query itself, and escapes
every value, so that a caller, e.g. an LLM’s tool call, cannot inject LogQL.
The Smarter Operations MCP server’s log tools (smarter.apps.mcpserver.logs.tools) and its
help desk snapshot (smarter.apps.mcpserver.helpdesk.users) are the service’s callers. The
service only reads: nothing is written to Loki, and nothing is cached.
- class smarter.apps.infrastructure.services.logs.InMemoryLogService(ready=True, **kwargs)[source]
Bases:
LogServicePod logs in memory, for tests and local development: add lines to
lines.Queries are answered in Python:
containscase insensitively, andpatternwithre, which accepts the RE2 syntax that Loki uses for the patterns that matter here.Unlike Loki, a
min_levelquery keeps only the lines whose own level is at least the minimum, which is what callers filter Loki’s result down to.- Parameters:
ready (
bool) – Whether the service is ready.
- __init__(ready=True, **kwargs)[source]
Create the service, with no lines.
- Parameters:
ready (
bool) – Whether the service is ready. False makes every call raiseInfrastructureNotReadyError, like a platform without a log store.kwargs – Passed to
InfrastructureService.
- label_values(label, start, end)[source]
Return the values of a label among the lines of
linesin a period.
- lines
tests add lines here.
- Type:
The log, in any order
- queries
Every query, in order, for tests to inspect.
- query(query)[source]
Return the lines of
linesthat match a query, and record the query inqueries.- Parameters:
query (
LogQuery) – The query.- Return type:
- Returns:
At most
query.limitlines, latest first ifquery.directionisbackward.- Raises:
InfrastructureNotReadyError – If the service is not ready.
- smarter.apps.infrastructure.services.logs.LOG_LEVEL_ORDER: tuple[LogLevel, ...] = (LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR, LogLevel.CRITICAL)
The levels, least severe first.
( <LogLevel.DEBUG: 'debug'>, <LogLevel.INFO: 'info'>, <LogLevel.WARNING: 'warning'>, <LogLevel.ERROR: 'error'>, <LogLevel.CRITICAL: 'critical'>, )
- smarter.apps.infrastructure.services.logs.LOG_RETENTION = datetime.timedelta(days=3)
limits_config.retention_periodin the Helm chart’s Loki.- Type:
How long the log store keeps a line
- class smarter.apps.infrastructure.services.logs.LogLevel(*values)[source]
Bases:
StrEnumThe level of a log line, as
parse_level()reads it from the line’s text.Loki stores lines as text, without a level, so the level is read from the words that the platform’s, Celery’s and MariaDB’s formats put near the start of each line. The values are lower case, as an LLM writes them in a tool call, e.g.
warning.- CRITICAL = 'critical'
- DEBUG = 'debug'
- ERROR = 'error'
- INFO = 'info'
- WARNING = 'warning'
- class smarter.apps.infrastructure.services.logs.LogLine(**data)[source]
Bases:
BaseModelOne line of a pod’s log, with the labels that Alloy gave it.
It is frozen: a line is read, never changed.
- property level: LogLevel | None
The line’s level, read from its text by
parse_level(), or None if it names none.
- model_config = {'frozen': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class smarter.apps.infrastructure.services.logs.LogQuery(**data)[source]
Bases:
BaseModelA query of the pod logs.
Every value is validated, and the service escapes it, so a query cannot carry LogQL of its own. It is frozen, and refuses fields that it does not define. Build one with
last(), for the most recent period, or withstartandend.logql()turns it into LogQL.- Parameters:
start – The earliest time, in UTC. It may not be earlier than
LOG_RETENTIONago.end – The latest time, in UTC.
components – The components to read, e.g.
("smarter", "smarter-worker"), or all of them.pod – One pod’s name, or every pod of the components.
contains – Text that every line must contain, case insensitively.
pattern – A regular expression (RE2 syntax) that every line must match.
min_level – The least severe level to return, e.g.
warningfor warnings, errors and critical lines. Lines without a level, e.g. a traceback’s continuation, are then left out.limit – The most lines to return.
direction –
backwardreturns the latest lines first,forwardthe earliest.
- components: tuple[str, ...]
The components to read, each a Kubernetes name, or empty for all of them.
- contains: tuple[str, ...]
Texts that every line must contain, case insensitively, each one line of 1 to 200 characters.
- direction: Literal['backward', 'forward']
backwardfor the latest lines first,forwardfor the earliest.
- classmethod last(period, **kwargs)[source]
Return a query of the most recent period, e.g. the last hour.
A period longer than
LOG_RETENTIONis shortened to it, since Loki keeps nothing older.- Parameters:
period (
timedelta) – How far back to read, at mostLOG_RETENTION.kwargs – The query’s other fields.
- Return type:
- Returns:
The query, from
periodago until now.- Raises:
pydantic.ValidationError – If a field is not valid.
- limit: int
The most lines to return, from 1 to
MAX_QUERY_LINES.
- logql()[source]
Return the query as LogQL, with every value escaped.
The stream selector picks the components, as an alternation of their escaped names, or every component, and the pod. Each filter is then a line filter,
|~: eachcontainstext, escaped and case insensitive, thepattern, and the levels frommin_levelup, asLOG_LEVEL_RE2. Every value is escaped for RE2 withre2_escape(), where it is taken literally, and for a LogQL string withlogql_string().- Return type:
- Returns:
e.g.
{component=~"smarter|smarter-worker"} |~ "(?i)timeout".
- model_config = {'extra': 'forbid', 'frozen': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- pattern: str | None
A regular expression (RE2 syntax) of at most 200 characters, that every line must match, or None.
- start: datetime
The earliest time, in UTC, within
LOG_RETENTIONof now.
- classmethod utc(value)[source]
Return a time in UTC: a time without a time zone is taken to be UTC, and any other is converted.
- classmethod valid_components(value)[source]
Check that each component is a Kubernetes name, which cannot carry LogQL.
- classmethod valid_contains(value)[source]
Check that each text is a single line, of 1 to 200 characters.
- classmethod valid_pattern(value)[source]
Check that the pattern is at most 200 characters, and compiles.
It is compiled with Python’s
re, which accepts nearly all of RE2’s syntax. A pattern that RE2 rejects anyway is reported by Loki, as aLogServiceError.- Parameters:
- Return type:
- Returns:
The pattern, unchanged.
- Raises:
ValueError – If the pattern is too long, or does not compile.
- valid_period()[source]
Check that the period ends after it starts, and starts within the retention period.
Five minutes of grace allow for the time between
last()and this check.- Return type:
- Returns:
The query, unchanged.
- Raises:
ValueError – If the period is empty, or starts too long ago.
- class smarter.apps.infrastructure.services.logs.LogService(provider_name, *args, **kwargs)[source]
Bases:
InfrastructureServiceThe platform’s pod logs, from the log store: the interface of the facade’s
.logs.LokiLogServiceimplements it for every platform, andInMemoryLogServicefor tests and local development. Callers reach it assmarter.apps.infrastructure.services.infrastructure.logs, never directly.- error_class
The exception that the service raises when its backend fails.
alias of
LogServiceError
- abstractmethod label_values(label, start, end)[source]
Return the values of a label in a period, e.g. the pods that logged in the last hour.
- Parameters:
- Return type:
- Returns:
The values, sorted.
- Raises:
LogServiceError – If the store could not be reached.
InfrastructureNotReadyError – If there is no log store.
- abstractmethod query(query)[source]
Return the lines that match a query.
- Parameters:
query (
LogQuery) – The query.- Return type:
- Returns:
At most
query.limitlines, latest first ifquery.directionisbackward.- Raises:
LogServiceError – If the store could not be reached, or rejected the query.
InfrastructureNotReadyError – If there is no log store.
- class smarter.apps.infrastructure.services.logs.LokiLogService(allow_in_tests=False, **kwargs)[source]
Bases:
LogServiceThe pod logs, in Loki at
smarter_settings.loki_url.It is ready when the url is set. In the unit tests, it refuses to query, unless
allow_in_testsis True, e.g. withrequestsmocked. Loki is in the platform’s namespace, so the url is a private, in-cluster address, and redirects are not followed.Side effects: every query is an HTTP GET to Loki, and the first success or failure after a change of state sends the infrastructure service’s connection signal (see
connection_state()).- Parameters:
allow_in_tests (
bool) – Query in the unit tests.
- __init__(allow_in_tests=False, **kwargs)[source]
Create the service.
- Parameters:
allow_in_tests (
bool) – Query in the unit tests, e.g. withrequestsmocked.kwargs – Passed to
InfrastructureService.
- label_values(label, start, end)[source]
Return the values of a label in a period, from Loki’s
label/<name>/valuesapi.Side effects: one HTTP GET to Loki.
- Parameters:
- Return type:
- Returns:
The values, sorted.
- Raises:
LogServiceError – If Loki could not be reached, or answered with something other than a label values response.
InfrastructureNotReadyError – If there is no log store.
- query(query)[source]
Return the lines that match a query, from Loki’s
query_rangeapi.Loki returns one stream for each set of labels, each ordered on its own, so the lines of every stream are merged, and sorted by time, in the query’s direction.
Side effects: one HTTP GET to Loki, and a DEBUG log of the LogQL and the number of lines.
- Parameters:
query (
LogQuery) – The query.- Return type:
- Returns:
At most
query.limitlines, latest first ifquery.directionisbackward.- Raises:
LogServiceError – If Loki could not be reached, rejected the query, or answered with something other than a query response.
InfrastructureNotReadyError – If there is no log store.
- smarter.apps.infrastructure.services.logs.MAX_QUERY_LINES = 5000
Loki’s default
max_entries_limit_per_query.- Type:
The most lines that one query returns
- smarter.apps.infrastructure.services.logs.at_least(level, minimum)[source]
Return whether a level is at least as severe as a minimum.
A line without a level, e.g. a traceback’s continuation, is not.
- Parameters:
- Return type:
- Returns:
True if the level is the minimum, or more severe, by
LOG_LEVEL_ORDER.
- smarter.apps.infrastructure.services.logs.configure_logs(factory)[source]
Set the factory of the log service that
get_logs()returns.The current service is dropped, so that the next
get_logs()creates one with the new factory. Tests install anInMemoryLogServicethis way, and restore the default after.- Parameters:
factory (
Callable[[],LogService] |None) – Returns the service, or None to restore the default,LokiLogService.- Return type:
- smarter.apps.infrastructure.services.logs.get_logs()[source]
Return the log service, which is created once, by the factory of
configure_logs().- Return type:
- Returns:
The service: the factory’s, or a
LokiLogServiceby default.
- smarter.apps.infrastructure.services.logs.logql_string(value)[source]
Escape a value for a LogQL double-quoted string, whose escapes are Go’s.
- smarter.apps.infrastructure.services.logs.parse_level(line)[source]
Return the level of a log line, from its text.
The platform’s lines put their level near the start, so only the first 120 characters are read, which keeps a message that mentions “ERROR” from making an INFO line an error.
- smarter.apps.infrastructure.services.logs.re2_escape(text)[source]
Escape text so that an RE2 regular expression matches it literally.
Unlike
re.escape(), it escapes onlyRE2_SPECIAL: RE2 rejects an escaped space or letter, whichre.escape()can produce.