Source of truth for integrating and using SMonitor in this library.
Metadata
- Source repository:
smonitor - Source document:
standards/SMONITOR_GUIDE.md - Source version:
smonitor@0.12.0 - Last synced: 2026-08-17
SMonitor is the diagnostics layer for the UIBCDF ecosystem. It centralizes warnings, errors, and developer signals so that user messages are consistent, actionable, and traceable across libraries.
SMonitor is not just a logging wrapper; it is a Signal Orchestrator that decouples event emission from message presentation.
- Consistency: Users see clear, helpful messages formatted as cards.
- Traceability: Developers can see the "Breadcrumb" trail across libraries.
- AI-Ready: Agents can parse structured events via the
agentprofile.
Treat this guide in three layers:
- Mandatory
- required for a correct SMonitor integration in any sibling library.
- Recommended
- expected for modern QA/support readiness.
- Advanced / CI-support
- valuable once the mandatory and recommended layers are already in place.
If time is constrained, implement in that order.
At minimum, a sibling library should have:
_smonitor.pyplus private catalog/meta files.ensure_configured(PACKAGE_ROOT)in package initialization.- catalog-driven emission via
DiagnosticBundle/ catalog exceptions and warnings. @signalon public orchestration entry points.context_extra(...)for repeated structured diagnostic fields.- one smoke test covering config + bundle export.
If the library package is named mylib, the following files must exist relative to the repository root:
_smonitor.py: Runtime configuration and message templates (CODES).
Example _smonitor.py:
PROFILE = "user"
SMONITOR = {
"level": "WARNING",
"trace_depth": 3,
"capture_warnings": True,
"capture_logging": True,
"theme": "plain",
"silence": ["pint", "networkx"], # Noisy loggers to ignore
}mylib/_private/smonitor/catalog.py: Catalog entries (meta-data about each signal).mylib/_private/smonitor/meta.py: Project metadata (URLs for documentation and issues).mylib/_private/smonitor/__init__.py: ExportsCATALOG,META, andPACKAGE_ROOT.
CODES and SIGNALS must be resolved from exactly one authoritative place.
Recommended pattern:
- define
CATALOG,CODES, andSIGNALSinmylib/_private/smonitor/catalog.py; - in
_smonitor.py, import them frommylib._private.smonitor.catalog.
This avoids drift where emitted catalog codes exist but template messages are missing at runtime.
Level: Mandatory
In your library's __init__.py, ensure SMonitor is configured on import. This activates the "System Nervous System":
from smonitor.integrations import ensure_configured
from ._private.smonitor import PACKAGE_ROOT
ensure_configured(PACKAGE_ROOT)Level: Mandatory
All diagnostic output must be driven by the catalog. Never hardcode strings in the scientific logic.
Use the DiagnosticBundle to create consistent warn and warn_once helpers in your library's _private/smonitor/emitter.py:
# mylib/_private/smonitor/emitter.py
from smonitor.integrations import DiagnosticBundle
from . import CATALOG, META, PACKAGE_ROOT
bundle = DiagnosticBundle(CATALOG, META, PACKAGE_ROOT)
warn = bundle.warn
warn_once = bundle.warn_once
resolve = bundle.resolveAll custom exceptions must inherit from CatalogException (provided by smonitor.integrations). This ensures messages are automatically hydrated from the catalog.
# mylib/_private/smonitor/exceptions.py
from smonitor.integrations import CatalogException
from . import CATALOG, META
class MyLibException(CatalogException):
def __init__(self, message=None, **kwargs):
super().__init__(message, catalog=CATALOG, meta=META, **kwargs)
class ArgumentError(MyLibException):
catalog_key = "ArgumentError"
# ... logic to prepare extra dict ...Similarly, use CatalogWarning for warning classes:
# mylib/_private/smonitor/warnings.py
from smonitor.integrations import CatalogWarning
from .emitter import bundle
class MyLibWarning(CatalogWarning):
# ... setup catalog and meta ...Note: The raw emit_from_catalog function is still available but DiagnosticBundle is the preferred high-level interface.
A catalog template may interpolate its own placeholders. Pass typed fields in
extra and let SMonitor render them — do not pre-render the sentence and hand
it over as a string:
# Correct: the template owns the wording, the call site owns the data.
# CODES["MYLIB-W010"]["user_message"] = "Atom name '{atom_name}' is not recognized."
warn(UnknownAtomNameWarning(atom_name=atom_name))
# Avoid: the template can only say "{message}", and the structured field
# never reaches report(), fingerprints, or resource counters.
warn(UnknownAtomNameWarning(message=f"Atom name '{atom_name}' ..."))warn(instance) carries the instance's extra into the emitted event, so those
fields reach report(), events_by_fingerprint, and most_noisy_resources as
typed data. {message} remains available for string callers
(warn("some text", MyWarning)).
Catching code should read exc.code and exc.extra rather than parsing the
rendered English message.
class UnknownAtomNameWarning(CatalogWarning):
catalog_key = "UnknownAtomNameWarning"
def __init__(self, message=None, *, atom_name=None):
super().__init__(message, catalog=CATALOG, extra={"atom_name": atom_name})Python rebuilds a warning or exception as type(w)(*w.args). pickle does it,
copy.deepcopy does it, warnings.warn(text, category) does it, and pytest-xdist
does it when a warning crosses from a worker to the controller. A class that
names a domain field first therefore receives its own rendered sentence as that
field, and the template renders around its own output:
Atom name 'Atom name 'Ar' is not recognized.' is not recognized.
Putting message first makes that rebuild land where it belongs. Keeping the
fields keyword-only preserves what a per-field signature is for: a misspelled
field stays a TypeError instead of becoming an extra nobody reads and a
template rendered with holes in it.
Where a class needs to compute its message, render it in a classmethod and
hand the finished text to __init__, so the constructor still stores what it is
given rather than deriving it:
@classmethod
def for_atoms(cls, names):
joined = ", ".join(sorted(names))
rendered, _ = smonitor.resolve(code="MYLIB-W010", extra={"atom_name": joined})
return cls(rendered, atom_name=joined)One residue is not fixable this way: a hint whose template interpolates a field
cannot be re-rendered by a rebuilder that carries only args, since the field is
not there. That is a limitation of the transfer, not of the class, and it is
being addressed upstream in pytest-dev/pytest-xdist#1372.
Use warn(...) rather than warnings.warn(...). It does both jobs: it emits the
structured, catalog-backed event and raises the warning through Python's
warning machinery, so everything your users and your test suite already rely on
keeps working:
# Your users' filters apply as usual.
warnings.filterwarnings("ignore", category=UnknownAtomNameWarning)
warnings.simplefilter("error") # promotes it to an exception
# Your tests assert on it as usual.
with pytest.warns(UnknownAtomNameWarning, match="XXX"):
get_atom_type_from_atom_name("XXX")Calling warnings.warn(MyCatalogWarning(...)) directly instead is the mistake
this replaces. The warning still reaches SMonitor when capture_warnings is on,
but only by way of the py.warnings logger, which delivers it as Python's
formatted text — file path and source line included — with code=None,
source="py.warnings", no category and none of your structured fields. The
catalog entry is bypassed entirely, so the incident is invisible to
events_by_code, to fingerprint summaries and to any QA policy keyed on codes.
A filter that suppresses the warning for the user does not suppress the
SMonitor event: filters govern the console, not your telemetry. And when
simplefilter("error") promotes the warning to an exception, the event has
already been recorded before it is raised.
Do not swallow diagnostics emission errors with except Exception: pass.
If emission fails in non-critical paths:
- fallback to a plain Python warning/log line;
- keep enough context (
caller, signal key, exception text) for debugging.
Silencing emission failures causes loss of traceability and empty/noisy diagnostics in downstream libraries.
Level: Recommended
To enable execution traceability (breadcrumbs), decorate all major API entry points and internal orchestration functions.
from smonitor import signal
@signal(tags=["topology"])
def get_atoms(molecular_system, selection="all"):
...Benefits:
- On error, SMonitor reports the full call chain:
[mylib.api_func] -> [otherlib.internal_logic] -> [ERROR]. - Performance telemetry can be enabled globally without changing the code.
Level: Recommended
Enforce structured data by defining required fields in _smonitor.py:
SIGNALS = {
"mylib.select": {
"extra_required": ["selection"],
}
}Missing fields will trigger warnings or errors in dev and qa profiles, ensuring diagnostic quality.
Level: Recommended
Recent pre-1.0 stabilization work added several profiling and machine-diagnostics capabilities that integrators should use deliberately:
@signal(..., extra_factory=...)can attach structured per-call context without emitting a separate warning or error event.report()now exposestimings_by_tagin addition to timings by function and module.slow_signal_msandslow_signal_levelenable opt-in slow-call events (SMONITOR-SIGNAL-SLOW) for developer and QA workflows.
Recommended usage:
from smonitor import signal
@signal(
tags=["api", "selection"],
extra_factory=lambda args, kwargs: {"selection": kwargs.get("selection")},
)
def get_atoms(molecular_system, selection="all"):
...These features are intended for observability and QA; they should remain opt-in and must not flood end-user output by default.
Level: Recommended
For library-generated diagnostics, prefer smonitor.integrations.context_extra(...) when building repeated extra payloads.
from smonitor.integrations import context_extra, emit_from_catalog
emit_from_catalog(
CATALOG["warnings"]["DownloadWarning"],
extra=context_extra(
caller="mylib.form.file_pdb.download",
resource="181l.pdb",
provider="RCSB",
operation="download",
extra={"attempt": 2, "retries": 5},
),
)Use this helper for stable shared keys such as caller, form, requested_attribute, resource, provider, and operation.
Current canonical additive fields also include:
- retry metadata:
retry_attempt,retry_max,retry_exhausted,retry_delay_s; - causal metadata:
failure_class,last_failure_reason,cause_exception_type,cause_code,causal_chain; - decision metadata:
incident_kind,severity,priority,diagnostic_confidence,recommended_action,next_step,retryable,support_needed; - structured
evidencefor compactexpected/observed/resource/operationfacts.
Compact modern example:
from smonitor.integrations import context_extra
extra = context_extra(
caller="mylib.io.download_structure",
resource="181l.pdb",
provider="RCSB",
operation="download",
retry_attempt=2,
retry_max=5,
retry_exhausted=False,
failure_class="network",
last_failure_reason="timeout",
incident_kind="network",
recommended_action="retry",
next_step="check-network",
evidence={"expected": "download ok", "observed": "timeout"},
)Level: Advanced / CI-support
SMonitor now exposes QA-oriented summaries beyond raw event streams:
- events carry stable
fingerprint,run_id,session_id, and optionalcorrelation_id. - events also carry a stable additive
human_summaryblock for concise human-facing handoff. report()includesevents_by_code,events_by_category,events_by_fingerprint,slow_signals_recent, andcoalesced_warnings.report()also exposes operational triage sections such astop_codes,top_sources,top_fingerprints,most_noisy_resources,most_expensive_entries,blocking_incidents,actionable_incidents, andrecurrent_incidents.- bundle exports mirror those summaries under
triage. - bundle exports also carry a
runtimeblock and can be compared locally withsmonitor compare. JsonHandlerincludes anormalizedpayload section with stable machine-oriented fields for cross-library ingestion, including retry/causal metadata, decision metadata, structuredevidence, and mirroredhuman_summary.
These additions should be treated as the preferred source for automated QA summaries before scanning raw event buffers.
Minimal CI/support flow:
- run tests or the representative workflow;
- export a local bundle;
- inspect
triagefirst, not raw events; - compare against a previous bundle when asking “what changed?”;
- only fall back to raw event streams when the summaries are insufficient.
Level: Advanced / CI-support
Two usability rules now apply:
- Human-readable handlers may truncate very large structured payload fragments for
qa,dev, anddebugprofiles. The underlying event payload is not altered. - Repeated transient warnings can be coalesced with
warning_coalesce_window_s. The first warning is emitted normally; suppressed duplicates are summarized incoalesced_warnings.
Use coalescing only for clearly repeated transient diagnostics such as download retries, not for semantically distinct warnings.
More general duplicate handling is also available through duplicate_policy, keyed by incident fingerprint. The current pre-1.0 supported policies are:
offemit_summaryemit_every_n
Use duplicate policies for genuinely repeated incidents where aggregate counts are more useful than verbatim repetition.
Level: Recommended
SMonitor captures all exceptions by default as ERROR. For functions that perform exploratory checks (e.g., "is this string a unit?"), this creates log noise.
Use exception_level="DEBUG" in the @signal decorator to silence expected failures in normal operation.
@signal(tags=["check"], exception_level="DEBUG")
def is_valid_format(data):
# If this raises, it will be logged as DEBUG, not ERROR
...For functions that must succeed (e.g., "parse this unit"), keep the default ERROR level. If a user provides malformed input where a valid one is expected, it is an error.
- Zero String Hardcoding: If it's a warning or error, it belongs in the catalog.
- Lazy Diagnostics: Do not perform expensive string formatting before calling
emit. Pass raw data inextraand let SMonitor handle the interpolation. - Traceability First: Use
@signalgenerously in orchestration layers but avoid it in high-frequency tight loops. - Template Wiring Integrity: Every emitted catalog code must have a matching template in the active
_smonitor.pyconfiguration. - No Silent Emission Failures: Never hide failed catalog emissions without an explicit fallback diagnostic.
Document created on February 6, 2026, as the authority for SMonitor integration.