SmrtHub Logging – Canonical Guide (C# + Python)¶
This is the canonical, top‑level reference for SmrtHub logging across C# and Python. It describes the target (canonical) design, the unified behaviors implemented in both runtimes, configuration, HTML export and Support Bundle, and how everything is validated via tests. Treat this as the single source of truth for all things logging in SmrtHub.
Note: The older
Python_Core.Logging.README.mdhas been removed. This document is the canonical reference for both C# and Python logging.
Contents¶
- SmrtHub Logging – Canonical Guide (C# + Python)
- Contents
- Goals and Scope
- Canonical Log Event Schema (target)
- C# implementation:
Smrt.Logging - Windows Event Log sink behavior (C#)
- Configuration:
Smrt.Config/Logging - W3C Trace Context Propagation
- Tests:
Smrt.Logging.Tests - Python implementation:
python_core.utils.logger - HTML export and Support Bundle
- Adoption checklist
- Status summary
- Implementation appendix (for maintainers)
Goals and Scope¶
- Enterprise‑grade logging with structured JSON lines and readable text
- Privacy by default: recursive PII/sensitive data scrubbing
- First‑class traceability with W3C Activity context and correlation IDs
- Clear exception reporting with demystified stacks (C# demystified; Python captures traceback)
- Deterministic tests validating real behavior (C# and Python)
- Cross‑runtime parity: fields and semantics align so logs can be triaged together (HTML viewer, Support Bundle)
Where current implementations diverge from the canonical design, we note a mapping and status.
Canonical Log Event Schema (target)¶
Example JSON line (abbreviated):
{ "ts": "2025-10-19T20:09:31.502Z", "level": "Information", "message": "Order {OrderId} processing…", "renderedMessage": "Order 12345 processing…", "props": { "OrderId": "12345" }, "exception": { "type": "System.Exception", "message": "…", "hresult": -2146233088, "demystifiedStack": "…", "inner": { /recursive/ } },
Status: the C# implementation emits Serilog’s shape (Level, MessageTemplate, RenderedMessage, etc.) with one‑to‑one mappings documented below. The Python implementation emits a Serilog‑shaped NDJSON with matching fields and semantics.
C# implementation: Smrt.Logging¶
Path: SmrtApps/src/Smrt.Logging
Key capabilities
- Dual sinks: JSONL (machine) + text (human)
- Windows Event Log sink is conditionally enabled: Production environment + elevated process only
- PII scrubbing: recursive across strings, dicts, lists, and settable object properties
- Activity enrichment: TraceId, SpanId, ParentSpanId, baggage, and Activity tags
- Tags are emitted with a configurable prefix (default
Tag.) - Correlation scopes:
BeginCorrelation()sets an ambient CorrelationId (also mirrored into an Activity tag) - Exception demystification: applied at log time when enabled
- Dynamic level switching via
SetMinimumLevel, with a best‑effortPersistMinimumLevel - Optional async buffering with bounded queue and backpressure metrics/soft circuit
- Drop-on-pressure policy or blocking producer mode
- Circuit temporarily drops < Warning during sustained pressure windows
- HTML export: escaped, paginated, optional auto‑open; deterministic path for tests
- Unified HTML viewer: single file enumerates all component logs with in‑browser filters; supports optional per‑entry Component filter when multiple components are present within a single aggregated JSONL
- Metrics: counts by level, enqueued/written/dropped totals, last error time
- File naming: per-component (default) or per-process (
-pid{n}) to avoid cross-process interleaving - Hardened Windows ACLs (optional) for log directory
- Optional OTEL-friendly duplicate fields (
trace_id,span_id,parent_span_id) - Test mode: deterministic sinks that avoid Serilog reconfiguration pitfalls
Important APIs
Logger.Initialize(component, level?, options?)Logger.ConfigureFrom(IConfiguration, component, sectionPath?)Logger.Info/Warning/Error/Debug/Verbose/Fatal(…)(+ Async variants)Logger.BeginScope(name, props?),Logger.BeginActivity(name, kind?, tags?), andLogger.BeginCorrelation(corrId?, createActivityIfMissing?, activityName?)Logger.GetTraceContext()– returns(traceId, spanId, parentSpanId)for the current Activity (empty strings if none)Logger.MakeTraceparent()– returns a W3C traceparent header string (e.g.,00-<traceId>-<spanId>-01) or empty if no ActivityLogger.ExportHtmlLog(autoOpen?, maxEntries?)– per‑component HTML viewer (single component)Logger.ExportUnifiedHtmlLogs(autoOpen?, maxEntriesPerComponent?)– unified HTML viewer across all components with component selector, level/text filters, pagination, and optional per‑entry Component filter for single‑file aggregation scenariosLogger.GetMetrics()
Options (LoggerOptions)
- LogDirectory, RetainedFileCountLimit, FileSizeLimitBytes, RollOnFileSizeLimit
- ScrubPII, RedactionToken
- EnableConsole, EnableTraceContext
- EnableWindowsEventLog (honored only when environment is Production and process is elevated)
- AutoOpenHtmlExport
- EnrichWithDemystifiedExceptions (default true)
- ActivityTagPrefix (default
"Tag.") - MaxQueueSize (null or <= 0 disables async buffer), OnDropPolicy (
"drop-newest"|"block") - BackpressureWindowSeconds, BackpressureThresholdDrops, BackpressureTripWindows, BackpressureCircuitHoldSeconds, BackpressureFlipToWarning
- FileNameMode (
"shared"|"per-process"), SecureLogAcls (Windows only) - EnableStructuredRedaction (default true), EnableOtelFields (default false)
- WriterThrottleMs (advanced/testing): optional sleep in background writer to induce pressure in tests
Serilog JSON mapping
- Canonical
level↔ SerilogLevel - Canonical
message↔ SerilogMessageTemplate - Canonical
renderedMessage↔ SerilogRenderedMessage - Canonical
props↔ SerilogProperties - Canonical
exception.demystifiedStack↔ DemystifiedExceptionstring (stack) in Serilog’sExceptionfield - Canonical
traceId/spanId/parentSpanId↔ Properties enriched byActivityEnricher - Canonical
tags↔ properties prefixed byActivityTagPrefix(defaultTag.)
Notes on robustness
- Writes are synchronous by default. Async buffering is available via
MaxQueueSizeand adds drop metrics and a soft circuit for sustained pressure. - Per‑process file naming is off by default; enable if cross‑process interleaving arises (
FileNameMode = "per-process"). - With PII scrubbing enabled (default), token‑ish strings (24–64 chars, alnum/
-/_) are redacted; this includes GUID‑like values. For tests or scenarios that must surface raw CorrelationId, setScrubPII = falsetemporarily.
Windows Event Log sink behavior (C#)¶
- The Event Log sink is gated to avoid developer friction and noisy local event logs.
- Enabled when:
DOTNET_ENVIRONMENT=Production(or equivalent) AND the process has administrative privileges. - Disabled otherwise, even if configured, with a clear INFO log noting the reason.
- The file sinks (text + JSONL) are always enabled unless explicitly turned off in options.
- The logging subsystem ensures the target log directory exists; if creation fails, initialization logs an error to console with remediation guidance.
Configuration: Smrt.Config/Logging¶
Path: SmrtApps/src/Smrt.Config/Logging
SmrtLoggingOptionsprovides typed options:- MinimumLevel, LogDirectory, RetainedFileCountLimit, FileSizeLimitBytes, RollOnFileSizeLimit
- ScrubPII, RedactionToken
- EnableConsole, EnableTraceContext, AutoOpenHtmlExport
- EnrichWithDemystifiedExceptions, ActivityTagPrefix
- MaxQueueSize, OnDropPolicy, BackpressureWindowSeconds, BackpressureThresholdDrops, BackpressureTripWindows, BackpressureCircuitHoldSeconds, BackpressureFlipToWarning
- FileNameMode, SecureLogAcls
- EnableStructuredRedaction, EnableOtelFields
- WriterThrottleMs
- Embedded defaults:
appsettings.logging.jsonincludes a baseline that apps can merge/override. - Preferred wiring:
Logger.ConfigureFrom(configuration, component, "Logging:SmrtHub").
W3C Trace Context Propagation¶
When making outbound HTTP calls or passing context across service boundaries, use the built-in W3C traceparent helpers:
Logger.GetTraceContext()
- Returns a tuple:
(string traceId, string spanId, string parentSpanId) - All values are empty strings if no
Activityis active - Use this to manually construct headers or pass context to other systems
Logger.MakeTraceparent()
- Returns a W3C traceparent header string:
00-<32hex traceId>-<16hex spanId>-<flags> - Returns empty string if no
Activityis active - Flags are set to
01when the activity is recorded/sampled, else00
Example: Outbound HTTP with traceparent
using var activity = new Activity("outbound-call").Start();
var req = new HttpRequestMessage(HttpMethod.Get, url);
var traceparent = Logger.MakeTraceparent();
if (!string.IsNullOrEmpty(traceparent))
{
req.Headers.TryAddWithoutValidation("traceparent", traceparent);
}
var resp = await httpClient.SendAsync(req, cancellationToken);
Notes:
- If you already have an
ActivityfromActivitySource, OpenTelemetry, or diagnostics, the helpers use it automatically - Safe no-op when no activity is present (returns empty string)
- Compatible with distributed tracing systems that consume W3C trace context
Tests: Smrt.Logging.Tests¶
Path: SmrtApps/src/Smrt.Logging.Tests
What's validated
- Event capture through Serilog pipeline (stable in test mode)
- Recursive PII scrubbing
- Correlation and Activity (TraceId/SpanId/ParentSpanId)
- Correlation ID push/pull via
BeginCorrelation(),SetCorrelationId(),GetCorrelationId()(test withScrubPII=false) - Activity tag prefixing (
Tag.default) - Async buffering with backpressure metrics and soft circuit drop logic
- Operation timing messages via
LoggingExtensions.LogOperation - Dynamic level switching behavior
- HTML export escaping and pagination
How tests achieve determinism
- A local in‑memory test sink captures events reliably
- A single logger instance is reused; Serilog’s reconfiguration constraints are avoided
- File sinks are configured with sharing and created up‑front in a temp directory
How to run
- From the repo root or the test project folder, run:
PowerShell (pwsh):
dotnet test C:\AppProjects\SmrtHub.App\SmrtApps\src\Smrt.Logging.Tests\Smrt.Logging.Tests.csproj -c Debug -v minimal
- Expected: 20 passed, 0 failed. Duration ~3–6s on a dev machine.
- Logs for tests are written to a temp directory (prefixed with
SmrtHub-Logger-Tests-). - The suite uses an in-memory sink and pre-created files for determinism.
Test stability notes (Windows file sharing and Logger globals)¶
To keep the logging tests deterministic and green on Windows, we apply a few pragmatic patterns:
- Disable parallelization for logger tests that mutate global/static state
- A shared xUnit collection named
"Serial"is defined and applied toLoggerTestsandLoggingExtensionsTimingTests. -
Files:
SmrtApps/src/Smrt.Logging.Tests/TestCollections.csSmrtApps/src/Smrt.Logging.Tests/LoggerTests.cs(annotated with[Collection("Serial")])SmrtApps/src/Smrt.Logging.Tests/LoggingExtensionsTimingTests.cs(annotated with[Collection("Serial")])
-
Use cooperative file sharing for test log I/O
- The local test sink writes with
FileShare.ReadWriteso readers don’t collide with active writers. - Tests reading logs use a shared-read helper that opens with
FileShare.ReadWriteand retries briefly onIOException. -
Files:
SmrtApps/src/Smrt.Logging.Tests/TestSinkExtensionsLocal.cs(FileStream appends withFileShare.ReadWrite)SmrtApps/src/Smrt.Logging.Tests/LoggerTests.cs(ReadAllTextSharedhelper used in file-based assertions)
-
Reduce timing flakiness for operation scope logs
LoggingExtensionsTimingTests.LogOperation_Emits_Completed_With_Timingasserts via metrics instead of sink inspection to avoid event timing uncertainty.- File:
SmrtApps/src/Smrt.Logging.Tests/LoggingExtensionsTimingTests.cs(metrics-based assertion)
These adjustments are test-only and do not affect production behavior; they document how we keep the suite reliable on developer machines and CI.
Python implementation: python_core.utils.logger¶
Path: SmrtApps/PythonApp/python_core/utils/logger.py
Key capabilities (aligned with C#)
- Dual outputs: rotating text log + NDJSON structured log
- Serilog‑shaped JSON fields:
Timestamp,Level,MessageTemplate,RenderedMessage,Properties,Exception - Privacy by default: PII scrubbing for emails, SSN‑like, credit‑card‑like, and token‑ish values (applied to message text and recursively across dict/list values)
- Correlation and Activity context (W3C‑like):
CorrelationId,TraceId,SpanId, optionalParentSpanId - Activity tags emitted with a configurable prefix; default
Tag.(viaactivity_tag_prefix) - Component normalization: when a
componentis passed to log helpers, it overwritesProperties.Componentwith a kebab‑cased value - Exception capture: Python traceback text in
Exceptionfield - Robust file handling on Windows: rotating handler with delayed open to avoid transient file locks
Convenience API
- Factory:
get_logger_for(component_slug)returns a per‑component logger with methods: log_info,log_warning,log_error,log_debug,log_fatal,log_json,log_exception- Context manager:
begin_activity(name, kind?="Internal", tags?=None)to add trace IDs andTag.*properties
Quick usage
from python_core.utils.logger import get_logger_for
_log = get_logger_for("smrtdetect")
_log.log_info("hello")
_log.log_warning("heads up")
_log.log_error("uh oh")
_log.log_debug("details")
try:
raise RuntimeError("boom")
except Exception:
_log.log_exception("failed")
Where logs go (Windows per-user)
- Text logs: %LOCALAPPDATA%/SmrtHub/Logs/
/ -log.txt - Structured logs: %LOCALAPPDATA%/SmrtHub/Logs/
/ -log.json - Example slugs in use:
python-core,python-runtime,python-net,smrtdetect,smrtspace,flow-enhance
Shared logs for LocalSystem / ProgramData scenarios
- Windows services that must share evidence with user-scoped tooling (currently
Smrt.StorageGuard.ServiceHost) write to%ProgramData%/SmrtHub/Logs/<slug>viaStorageGuardPathsso LocalSystem and interactive accounts read the exact same artifacts. StorageGuardPaths.GetSystemInfoDirectory()automatically migrates legacy%LocalAppData%/SmrtHub/Logs/system-infofolders into ProgramData on first write and only falls back to LocalAppData when ProgramData is unavailable (e.g., developer profile lacking elevation).- The Privacy/Security checklist (
Tools/Privacy-Security/Invoke-PrivacySecurityChecklist.ps1) explicitly opens the ProgramData snapshot/signature/service logs and warns if they are missing, ensuring services never drift back to stray directories.
Configuration (Python)
- Files:
SmrtApps/PythonApp/python_core/config/python-core-logging.defaults.json(baseline) and optionalpython-core-logging.json(overrides) - Keys:
level,text_log,structured_log,max_bytes,backup_count,console,structured,component,scrub_pii,redaction_token,enable_trace_context,activity_tag_prefix
Serilog mapping (Python)
- Canonical
level→Level - Canonical
message→MessageTemplate - Canonical
renderedMessage→RenderedMessage - Canonical
props→Properties(includesComponent,Machine,User,ProcessId, correlation/activity fields and tags) - Canonical
exception→Exception(traceback text)
Run the Python tests
# From repo root; ensure PythonApp is on PYTHONPATH or activate the venv
python -m unittest -v python_core.tests.test_logger
What the tests validate
- Structured shape, PII scrubbing, correlation/activity fields,
Tag.prefix behavior - Component override normalization (
Orders.Service→orders-service) - Exception capture produces an
Errorevent with traceback text
HTML export and Support Bundle¶
HTML export
- Hardened viewer (escaped, paginated, client‑side filters). Designed for local inspection.
- Two modes:
- Per‑component HTML:
Logger.ExportHtmlLog(autoOpen?, maxEntries?)writes<component>-log.htmlbeside the JSON log. - Unified HTML (all components):
Logger.ExportUnifiedHtmlLogs(autoOpen?, maxEntriesPerComponent?)writesSmrtHub-logs.htmlin the logs root with:- Component selector (dataset switch)
- Level filter, free‑text search, and pagination
- Optional per‑entry Component filter when multiple components exist within a single aggregated JSONL file
- How to trigger:
- HubWindow tray: right-click → “SmrtSupport Logs” (headless; opens browser if enabled)
- From code: call the methods above after logger initialization
- Notes: The viewer is sanitized for PII/tokens; the raw JSON/text logs remain the source of truth
Support Bundle (Smrt.SupportBundle)
- Aggregates logs and diagnostics with redaction and caps; includes raw JSON/text logs and can optionally include HTML viewers for convenience
- How to trigger:
- HubWindow tray: right‑click → “Generate Support Bundle (24h)” (headless; outputs to Desktop\SmrtHub-Bundles by default)
- From code:
new Smrt.SupportBundle.SupportBundleExporter().ExportAsync(options) - What’s included (typically):
- Raw logs (JSONL + text) for selected modules/time window/levels
- Optional HTML viewers
- Environment/system info and config snapshots
- A review HTML report and a JSON manifest (v1.1) with SHA-256 hashes plus a retentionEvidence summary when retention artifacts are staged
Encounter evidence logging¶
- ProgramData log:
%ProgramData%/SmrtHub/Logs/encounter-log/encounter-log-YYYYMMDD.ndjson(respectsSMRTHUB_COMMON_APPDATA_OVERRIDE). Newline-delimited JSON records with{ component, stage, recordedAtUtc, encounter, files?, error? }. - Stage
encounter-createdis emitted byClipboardMonitorwhen it mints a batch; includes per-fragment SHA-256 digests plus Storage Guard snapshot/signature metadata + verification status. - Stage
encounter-persistedis emitted by the Python runtime after each save. It mirrors the encounter envelope, lists every saved artifact path (SmrtSpace + SmrtArx + optional HTML copies), and reports any persistence errors. - SmrtSpace artifacts stay clean: Encounter metadata is no longer written next to user-visible files. Instead, the ProgramData log’s
filescollection enumerates every artifact so Support Bundles can correlate evidence without leaving.encounter.jsonclutter in SmrtSpace or SmrtArx. - Storage Guard provenance: The encounter envelope references the latest signed snapshot and signature under
SmrtHub/Logs/system-info. Verification errors (missing snapshot, invalid HMAC, etc.) are captured in the log’serrorfield for audit chains. - Downstream consumers: The NDJSON log is the canonical evidence set for privacy/compliance exports or retention workflows. Support Bundles include the log files directly, so there is no reliance on per-file sidecars, and the log must be treated as an immutable record tied to each clipboard-driven save.
Adoption checklist¶
- Initialize Logger early in app startup
- Keep
ScrubPIIenabled unless there’s a strong reason otherwise - Use structured properties for variables
- Use
BeginActivityand tags for cross‑component tracing - Set
ActivityTagPrefixif you need raw keys (set to empty) or different namespacing - Consider async buffering only if throughput requires it
- For Python modules, switch to
get_logger_for(<component>)and use the structured methods; preferlog_exceptionin error paths to populate theExceptionfield
Status summary¶
- C#: robust and test‑validated; demystification and activity enrichment enabled; configurable tag prefix; unified HTML viewer with per‑entry Component filter
- Config: centralized defaults with per‑app override support
- Python: per‑component logging with structured output; parity with C# field shapes; tests validating shape, PII scrubbing, activity, and exceptions
Implementation appendix (for maintainers)¶
Key source files and types
SmrtApps/src/Smrt.Logging/Logger.csLogger(core API): Initialize, ConfigureFrom, Info/Warning/Error/Debug/Verbose/Fatal, BeginScope, BeginActivity, ExportHtmlLog, GetMetricsActivityEnricher: populates TraceId/SpanId/ParentSpanId, baggage, and tag properties (prefixed via ActivityTagPrefix)InternalWrite(...): centralized write path, applies scrubbing and exception demystificationTryDemystify(Exception): reflection bridge toBen.Demystifierif availableSmrtApps/src/Smrt.Logging.TestsTestSinkExtensionsLocal: in‑memory sink + safe file writes for deterministic testsTestLogger: constructs a single Serilog instance, wires staticLogger, avoids reconfigure pitfallsLoggerTests.cs,LoggingExtensionsTimingTests.cs: coverage for scrubbing, trace, tags, timing, export, levelsSmrtApps/src/Smrt.Config/LoggingSmrtLoggingOptions: typed options for configuration bindingappsettings.logging.json: embedded defaults; apps can override in their own appsettings
Minimal initialization examples (C#)
- Preferred (config‑driven):
using Microsoft.Extensions.Configuration;
var cfg = new ConfigurationBuilder()
.AddSmrtHubLoggingDefaults() // from Smrt.Config
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
SmrtHub.Logging.Logger.ConfigureFrom(cfg, componentName: "trigger-manager", sectionPath: "Logging:SmrtHub");
SmrtHub.Logging.Logger.Info("Application started");
- Simple (direct):
SmrtHub.Logging.Logger.Initialize("trigger-manager");
SmrtHub.Logging.Logger.Info("Application started");
Canonical ↔ Serilog field mapping (top fields)
- Canonical
level→ SerilogLevel - Canonical
message→ SerilogMessageTemplate - Canonical
renderedMessage→ SerilogRenderedMessage - Canonical
props→ SerilogProperties - Canonical
exception.demystifiedStack→ appears within SerilogExceptionstring (demystified when enabled) - Canonical
traceId/spanId/parentSpanId→ enriched properties viaActivityEnricher - Canonical Activity tags → properties prefixed with
ActivityTagPrefix(defaultTag.)
Python quick links
SmrtApps/PythonApp/python_core/utils/logger.py– per‑component logger factory and structured logging helpersSmrtApps/PythonApp/python_core/tests/test_logger.py– Python logging tests (shape, scrubbing, mapping)