Skip to content

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.md has been removed. This document is the canonical reference for both C# and Python logging.

Contents


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‑effort PersistMinimumLevel
  • 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?), and Logger.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 Activity
  • Logger.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 scenarios
  • Logger.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 ↔ Serilog Level
  • Canonical message ↔ Serilog MessageTemplate
  • Canonical renderedMessage ↔ Serilog RenderedMessage
  • Canonical props ↔ Serilog Properties
  • Canonical exception.demystifiedStack ↔ Demystified Exception string (stack) in Serilog’s Exception field
  • Canonical traceId/spanId/parentSpanId ↔ Properties enriched by ActivityEnricher
  • Canonical tags ↔ properties prefixed by ActivityTagPrefix (default Tag.)

Notes on robustness

  • Writes are synchronous by default. Async buffering is available via MaxQueueSize and 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, set ScrubPII = false temporarily.

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

  • SmrtLoggingOptions provides 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.json includes 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 Activity is 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 Activity is active
  • Flags are set to 01 when the activity is recorded/sampled, else 00

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 Activity from ActivitySource, 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 with ScrubPII=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 to LoggerTests and LoggingExtensionsTimingTests.
  • Files:

    • SmrtApps/src/Smrt.Logging.Tests/TestCollections.cs
    • SmrtApps/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.ReadWrite so readers don’t collide with active writers.
  • Tests reading logs use a shared-read helper that opens with FileShare.ReadWrite and retries briefly on IOException.
  • Files:

    • SmrtApps/src/Smrt.Logging.Tests/TestSinkExtensionsLocal.cs (FileStream appends with FileShare.ReadWrite)
    • SmrtApps/src/Smrt.Logging.Tests/LoggerTests.cs (ReadAllTextShared helper used in file-based assertions)
  • Reduce timing flakiness for operation scope logs

  • LoggingExtensionsTimingTests.LogOperation_Emits_Completed_With_Timing asserts 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, optional ParentSpanId
  • Activity tags emitted with a configurable prefix; default Tag. (via activity_tag_prefix)
  • Component normalization: when a component is passed to log helpers, it overwrites Properties.Component with a kebab‑cased value
  • Exception capture: Python traceback text in Exception field
  • 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 and Tag.* 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> via StorageGuardPaths so LocalSystem and interactive accounts read the exact same artifacts.
  • StorageGuardPaths.GetSystemInfoDirectory() automatically migrates legacy %LocalAppData%/SmrtHub/Logs/system-info folders 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 optional python-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 levelLevel
  • Canonical messageMessageTemplate
  • Canonical renderedMessageRenderedMessage
  • Canonical propsProperties (includes Component, Machine, User, ProcessId, correlation/activity fields and tags)
  • Canonical exceptionException (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.Serviceorders-service)
  • Exception capture produces an Error event 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.html beside the JSON log.
  • Unified HTML (all components): Logger.ExportUnifiedHtmlLogs(autoOpen?, maxEntriesPerComponent?) writes SmrtHub-logs.html in 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 (respects SMRTHUB_COMMON_APPDATA_OVERRIDE). Newline-delimited JSON records with { component, stage, recordedAtUtc, encounter, files?, error? }.
  • Stage encounter-created is emitted by ClipboardMonitor when it mints a batch; includes per-fragment SHA-256 digests plus Storage Guard snapshot/signature metadata + verification status.
  • Stage encounter-persisted is 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 files collection enumerates every artifact so Support Bundles can correlate evidence without leaving .encounter.json clutter 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’s error field 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 ScrubPII enabled unless there’s a strong reason otherwise
  • Use structured properties for variables
  • Use BeginActivity and tags for cross‑component tracing
  • Set ActivityTagPrefix if 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; prefer log_exception in error paths to populate the Exception field

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.cs
  • Logger (core API): Initialize, ConfigureFrom, Info/Warning/Error/Debug/Verbose/Fatal, BeginScope, BeginActivity, ExportHtmlLog, GetMetrics
  • ActivityEnricher: populates TraceId/SpanId/ParentSpanId, baggage, and tag properties (prefixed via ActivityTagPrefix)
  • InternalWrite(...): centralized write path, applies scrubbing and exception demystification
  • TryDemystify(Exception): reflection bridge to Ben.Demystifier if available
  • SmrtApps/src/Smrt.Logging.Tests
  • TestSinkExtensionsLocal: in‑memory sink + safe file writes for deterministic tests
  • TestLogger: constructs a single Serilog instance, wires static Logger, avoids reconfigure pitfalls
  • LoggerTests.cs, LoggingExtensionsTimingTests.cs: coverage for scrubbing, trace, tags, timing, export, levels
  • SmrtApps/src/Smrt.Config/Logging
  • SmrtLoggingOptions: typed options for configuration binding
  • appsettings.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 → Serilog Level
  • Canonical message → Serilog MessageTemplate
  • Canonical renderedMessage → Serilog RenderedMessage
  • Canonical props → Serilog Properties
  • Canonical exception.demystifiedStack → appears within Serilog Exception string (demystified when enabled)
  • Canonical traceId/spanId/parentSpanId → enriched properties via ActivityEnricher
  • Canonical Activity tags → properties prefixed with ActivityTagPrefix (default Tag.)

Python quick links

  • SmrtApps/PythonApp/python_core/utils/logger.py – per‑component logger factory and structured logging helpers
  • SmrtApps/PythonApp/python_core/tests/test_logger.py – Python logging tests (shape, scrubbing, mapping)