What it is · Feature tour · Install · Quickstart · Language matrix · Platform
A log line that only carries the message is a clue. One that carries the whole story is an answer.
@smooai/loggerstamps every entry with the request journey (correlation IDs across services), the AWS runtime around it (Lambda, SQS, API Gateway context), and — where an OpenTelemetry span is active — the real W3C trace and span IDs, so logs join your traces instead of floating beside them. Native ports in five languages — TypeScript, Python, Rust, Go, and .NET — emit the same JSON shape, so a request crossing language boundaries still reads as one story.
Traditional loggers give you the message, but not the story. @smooai/logger records where the log came from, the request journey that led there, and the runtime around it — so a production failure reads like a trace, not a guess.
One structured-logging schema, implemented natively five times. Each port is idiomatic in its own language, but the wire shape — level names, correlationId, http.request/response, user, telemetry, error serialization — is shared, so logs from a TypeScript Lambda, a Go worker, a Python API, a Rust service, and a .NET job land in the same queries.
- TypeScript (
src/) — the original. AWS server logging plus the only browser logger (device/browser detection, fetch correlation). - Python (
python/) — full port, plus Socket.IO and Uvicorn logging adapters. - Rust (
rust/logger/) — serde-based port; Lambda context helpers behind anaws-lambdafeature flag. - Go (
go/) — full port onlog/slog, including Lambda/SQS helpers and OTel span correlation. - .NET (
dotnet/) — full port; integrates withMicrosoft.Extensions.Logging, trace correlation viaSystem.Diagnostics.Activity.
The ports are not all identical — the honest capability matrix is below.
| Capability | Where | |
|---|---|---|
| 🔗 | Correlation across services | All 5 languages |
| ⚡ | AWS context, captured automatically | All 5 languages |
| 🔭 | Logs that join your traces | TS · Python · Rust · Go (+ .NET via Activity) |
| 📍 | Exact caller location | All 5 languages |
| 🎨 | Pretty local output + rotating file logs | All 5 languages |
| 🕶️ | Sensitive-key redaction | All 5 languages |
| 🖥️ | Browser logging | TypeScript only |
A correlation ID set (or extracted from an incoming header, Lambda event, or SQS record) follows the request everywhere, in every language:
// Service A: API Gateway handler (TypeScript)
logger.addLambdaContext(event, context);
logger.info("Request received"); // correlationId: abc-123
// Service B: SQS processor (extracts the ID from the record)
logger.addSQSRecordContext(record);
logger.info("Processing message"); // same correlationId: abc-123// Service C: a Go worker — same schema, same ID
l.AddHTTPRequest(logger.HTTPRequest{
Headers: map[string]string{"X-Correlation-Id": "abc-123"},
})
l.Info("Completing workflow", logger.Map{"orderId": "ord_1"}) // still abc-123%%{init: {'theme':'base','themeVariables':{
'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52',
'lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif',
'clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
flowchart LR
B["Browser<br/>BrowserLogger (TS)"] -->|"X-Correlation-Id: abc-123"| GW["API Gateway → Lambda<br/>AwsServerLogger (TS)"]
GW -->|"SQS message attributes"| Q["SQS worker<br/>(Go port)"]
Q -->|"HTTP header"| API["Internal API<br/>(Python / .NET / Rust port)"]
GW -.-> LOGS[("One query:<br/>correlationId = abc-123")]
Q -.-> LOGS
API -.-> LOGS
B -.-> LOGS
classDef warm fill:#f49f0a,stroke:#ff6b6c,color:#1a0f00;
classDef teal fill:#00a6a6,stroke:#00c2c2,color:#011;
class LOGS warm
class B teal
Hand the logger the Lambda event/context (or SQS record, or HTTP request) once; every subsequent line carries the invocation metadata — function, region, request IDs, HTTP method/path/headers, SQS attributes.
import { AwsServerLogger } from "@smooai/logger/AwsServerLogger";
const logger = new AwsServerLogger({ name: "UserAPI" });
export const handler = async (event, context) => {
logger.addLambdaContext(event, context);
try {
const user = await createUser(event.body);
logger.info("User created successfully", { userId: user.id });
return { statusCode: 201, body: JSON.stringify(user) };
} catch (error) {
logger.error("Failed to create user", error, { body: event.body });
throw error;
}
};The same helpers exist in each port: add_lambda_context (Python), NewLambdaLogger / AddSQSRecordContext (Go), AddLambdaContext (.NET), and Lambda environment/event helpers behind the aws-lambda feature (Rust).
Historically every line's traceId was a fabricated UUID — useless for joining logs to traces. Now, when an OpenTelemetry span is active, TypeScript, Python, Rust, and Go stamp the span's real W3C trace_id and span_id onto the line, matching what your tracing backend recorded. No active span → the UUID fallback, unchanged.
// Go: thread the context and the active span's IDs land on the line
l.InfoContext(ctx, "Order shipped", logger.Map{"orderId": "ord_1"})
// → { "traceId": "4bf92f35…", "spanId": "00f067aa…", … }Each port depends only on the OTel API (no SDK, no exporter). .NET is the one exception: it takes no OpenTelemetry dependency at all and reads the same real W3C IDs from System.Diagnostics.Activity.Current — the API OTel .NET itself builds on — so the output is equivalent.
Every entry includes where in the code it was emitted, in all five languages:
Two shapes are in play, and the difference is deliberate:
| shape | ports | how |
|---|---|---|
callerContext.stack — multiple frames |
TypeScript, Python | walks the runtime stack |
caller: { file, line, function } — one frame |
Go, Rust, .NET | zero-cost compile-time / runtime.Caller |
{ "caller": { "file": "UserService.cs", "line": 42, "function": "CreateUser" } }Rust omits function: #[track_caller] gives file and line for free, but std::panic::Location
carries no symbol name and resolving one would mean capturing a backtrace on every line. .NET uses
[CallerFilePath]/[CallerLineNumber]/[CallerMemberName], which the compiler fills in at each
call site — no StackTrace walk. Both emit the file basename only; the full path is
build-machine noise.
All five ports detect local development and switch from strict JSON lines to ANSI pretty-printing — and write logs to disk under .smooai-logs/ with size/interval-based rotation:
const logger = new AwsServerLogger({
prettyPrint: true, // auto-enabled locally
rotation: { size: "10M", interval: "1d", compress: true },
});Every port scrubs values whose keys match a redaction list (case-insensitive, recursive) before a line is emitted — password, token, authorization, and friends by default, extensible per logger (addRedactKeys / add_redact_keys / DefaultRedactKeys…).
TypeScript only. BrowserLogger captures device type, browser name/version, platform, and user agent, and correlates fetches to your backend logs:
import { BrowserLogger } from "@smooai/logger/browser/BrowserLogger";
const logger = new BrowserLogger({ name: "CheckoutFlow" });
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "X-Correlation-Id": logger.correlationId() },
});
logger.addResponseContext(response);
logger.info("Checkout completed", { orderId: data.id });| Language | Package | Install |
|---|---|---|
| TypeScript | @smooai/logger |
pnpm add @smooai/logger |
| Python | smooai-logger |
pip install smooai-logger (or uv add smooai-logger) |
| Rust | smooai-logger |
cargo add smooai-logger |
| Go | github.com/SmooAI/logger/go/v4 |
go get github.com/SmooAI/logger/go/v4 |
| .NET | SmooAI.Logger |
dotnet add package SmooAI.Logger |
TypeScript (the original port — see AWS context and browser above for fuller examples):
// AWS environments (Lambda, ECS, EC2, …)
import { AwsServerLogger, Level } from "@smooai/logger/AwsServerLogger";
const logger = new AwsServerLogger({ name: "OrderService", level: Level.Info });
logger.addUserContext({ id: "user-123", role: "admin" }); // persists across logs
logger.addTelemetryFields({ duration: 150, operation: "db-query" });
logger.info("Payment processed", { amount: 99.99, currency: "USD" });
try {
await riskyOperation();
} catch (error) {
logger.error("Operation failed", error, { context: "additional-info" });
// → error message, stack trace, error type, and your context
}Six levels in every port — TRACE · DEBUG · INFO · WARN · ERROR · FATAL — plus context presets (MINIMAL / FULL).
Per-language quickstarts, with full API docs:
- Python —
python/README.md - Rust —
rust/logger/README.md - Go —
go/README.md - .NET —
dotnet/src/SmooAI.Logger/README.md
The wire schema is shared; port depth is not identical. Here's the honest status of each surface:
| Capability | TypeScript | Python | Rust | Go | .NET |
|---|---|---|---|---|---|
| Structured JSON, 6 levels | ✅ | ✅ | ✅ | ✅ | ✅ |
| Correlation / request / trace IDs | ✅ | ✅ | ✅ | ✅ | ✅ |
| HTTP request/response context | ✅ | ✅ | ✅ | ✅ | ✅ |
| User context + telemetry fields | ✅ | ✅ | ✅ | ✅ | ✅ |
| Lambda / SQS / API Gateway helpers | ✅ | ✅ | ✅ ¹ | ✅ | ✅ |
| Pretty local output | ✅ | ✅ | ✅ | ✅ | ✅ |
Rotating file logs (.smooai-logs/) |
✅ | ✅ | ✅ | ✅ | ✅ |
| Sensitive-key redaction | ✅ | ✅ | ✅ | ✅ | ✅ |
OTel span → traceId/spanId stamping |
✅ | ✅ | ✅ | ✅ | ➖ ² |
| Per-line caller location ⁴ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Browser logger | ✅ | ❌ | ❌ | ❌ | ❌ |
| Parity corpus enforced in tests ³ | ✅ | ✅ | ✅ | ✅ | ✅ |
¹ Behind the aws-lambda cargo feature; Lambda environment context needs no feature.
² No OTel dependency — equivalent real W3C trace/span IDs read from System.Diagnostics.Activity.Current, which is the API OpenTelemetry .NET itself builds on. Log lines also tee upstream via SmooLoggerOptions.ForwardTo (an ILogger), the hook OTel's .NET log appender attaches to.
³ parity-corpus.json is the cross-language output contract, and all five ports now replay it from that one committed file — TypeScript (src/parity-corpus.spec.ts), Python (python/tests/test_parity_corpus.py), Rust (rust/logger/tests/parity_corpus.rs), Go (go/parity_corpus_test.go), and .NET (dotnet/tests/SmooAI.Logger.Tests/ParityCorpusTests.cs). It covers level mapping, required field names, message shape, correlation-id propagation, and the default redaction key list. Editing a corpus value turns all five suites red.
⁴ Two shapes: TypeScript and Python emit a multi-frame callerContext.stack; Go, Rust and .NET emit a single-frame caller object. Rust's omits function — see Exact caller location.
CI does cover all five languages on every PR (pr-checks.yml typechecks, lints, tests, and builds TS, Python, Rust, Go, and .NET), and release.yml publishes all five: npm → PyPI → crates.io → Go module tag → NuGet.
It moved. The Rust/egui log viewer that used to live here has been rebuilt as SmooAI Observability Studio — a Dioxus native desktop client for SmooAI logs, errors, and metrics — and lives in the SmooAI/observability repo (desktop/), with builds on its releases page. The crate has been deleted from this repo; see log-viewer/DEPRECATED.md for the migration story.
@smooai/logger is built and open-sourced by Smoo AI — the AI-powered business platform with AI built into every product: CRM, customer support, campaigns, field service, observability, and developer tools.
- 🧰 More open source from Smoo AI — smoo.ai/open-source
- 🧩 Sibling packages — @smooai/fetch, @smooai/config, @smooai/observability, smooth
Use them in your stack, or take them as a reference for how we build.
Contributions are welcome. This project uses changesets to manage versions and releases — add one with pnpm changeset, then open a pull request referencing any related issues.
MIT © SmooAI. See LICENSE.
Brent Rager
Smoo GitHub: github.com/SmooAI
Built by Smoo AI — AI built into every product.

{ "callerContext": { "stack": [ "at UserService.createUser (/src/services/UserService.ts:42:16)", "at processRequest (/src/handlers/userHandler.ts:15:23)", ], }, }