Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/format-drift-and-release-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@smooai/logger": patch
---

Commit the repo's formatting and stop the release pipeline from dirtying its own working tree.

Dropping `cargo publish --allow-dirty` surfaced what the flag had been hiding: `release.yml`'s
own `Format` step ran `pnpm format`, which **rewrites** files and never commits them, so
`cargo publish` a few steps later saw a dirty tree. Every crate published from this repo silently
carried uncommitted reformatting.

`main` was format-drifted across seven files with nothing checking — PR checks ran `oxlint` but
never a formatter, and `pnpm format:check` did not exist.

- The formatting is committed; `pnpm format` is now a no-op on `main`.
- New `format:check` (oxfmt + ruff + `cargo fmt` + `gofmt`) runs in PR checks.
- `release.yml`'s `Format` becomes `Format check` — check, never rewrite.
- The changesets `version` lifecycle now ends with `oxfmt --write CHANGELOG.md package.json`,
because `changeset version` emits both in a shape oxfmt disagrees with; without it the next
release PR would land unformatted and break `cargo publish --locked` again.

This release also re-publishes to crates.io, NuGet, and the Go module tag, which stalled at
`4.5.0` while npm and PyPI went to `4.5.2`.
22 changes: 11 additions & 11 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"extraKnownMarketplaces": {
"smooth": {
"source": {
"source": "github",
"repo": "SmooAI/smooth"
}
}
},
"enabledPlugins": {
"smooth-agent@smooth": true
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"extraKnownMarketplaces": {
"smooth": {
"source": {
"source": "github",
"repo": "SmooAI/smooth"
}
}
},
"enabledPlugins": {
"smooth-agent@smooth": true
}
}
3 changes: 3 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ jobs:
- name: Lint
run: pnpm lint

- name: Format check
run: pnpm format:check

- name: Test
run: pnpm test

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,11 @@ jobs:
- name: Build
run: pnpm build

- name: Format
run: pnpm format
# Check, never rewrite. `pnpm format` here mutated the working tree without
# ever committing the result, which left `cargo publish` staring at a dirty
# tree — the reason `--allow-dirty` was needed in the first place.
- name: Format check
run: pnpm format:check

- name: Version Update 🦋
id: changesets
Expand Down
8 changes: 1 addition & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
of them the published one.

Every consumer of a non-npm port was reading a version constant from a different release.

- The sync moves into the changesets `version` lifecycle (`"version": "changeset version && node scripts/sync-versions.mjs"`, with `version: pnpm run version` on the action), so the bumped manifests are **committed** with the version bump.
- `cargo publish` drops `--allow-dirty` and gains `--locked`.
- `python/uv.lock` joins the synced set. It was missed before and is not cosmetic: `poe install-dev` runs `uv sync --locked`, which errors when the lock disagrees with `pyproject.toml`.
Expand Down Expand Up @@ -63,7 +62,7 @@

```jsonc
{
"caller": { "file": "UserService.cs", "line": 42, "function": "CreateUser" }
"caller": { "file": "UserService.cs", "line": 42, "function": "CreateUser" },
}
```

Expand Down Expand Up @@ -119,7 +118,6 @@
[SmooAI/observability](https://github.com/SmooAI/observability) (`desktop/`).

Removed:

- the `postinstall` hook and both `bundle-log-viewer` scripts (Node + Python)
- the `smooai-log-viewer` bin (npm) and console script (PyPI) and their wrappers
- the `log-viewer/` crate source and the `build-log-viewer.yml` release workflow
Expand Down Expand Up @@ -372,26 +370,22 @@
This release transforms `@smooai/logger` into a comprehensive multi-language logging ecosystem:

### 🐍 Python Package (`smooai-logger`)

- Available on PyPI as `smooai-logger`
- Full Python implementation with identical API to TypeScript version
- Synchronized versioning with npm package

### 🦀 Rust Crate (`smooai-logger`)

- Available on crates.io as `smooai-logger`
- Native Rust logging implementation
- Synchronized versioning with npm package

### 📊 Log Viewer CLI (`smooai-log-viewer`)

- Interactive GUI application for viewing `.smooai-logs` files
- Available as CLI command when installing npm package: `smooai-log-viewer`
- Cross-platform native binaries bundled with package
- Features filtering, searching, JSON expansion, and context viewing

### 🔄 Automated Publishing Pipeline

- Single changesets release now publishes to npm, PyPI, and crates.io
- Automatic version synchronization across all packages
- Enhanced CI/CD workflow for multi-language support
Expand Down
94 changes: 47 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

---

> **A log line that only carries the message is a clue. One that carries the whole story is an answer.** `@smooai/logger` stamps 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.
> **A log line that only carries the message is a clue. One that carries the whole story is an answer.** `@smooai/logger` stamps 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.

Expand All @@ -60,15 +60,15 @@ The ports are **not** all identical — the honest capability matrix is [below](

## Feature tour

| | Capability | Where |
| --- | --- | --- |
| 🔗 | [**Correlation across services**](#-correlation-across-services) | All 5 languages |
| ⚡ | [**AWS context, captured automatically**](#-aws-context-captured-automatically) | All 5 languages |
| 🔭 | [**Logs that join your traces**](#-logs-that-join-your-traces) | TS · Python · Rust · Go (+ .NET via `Activity`) |
| 📍 | [**Exact caller location**](#-exact-caller-location) | All 5 languages |
| 🎨 | [**Pretty local output + rotating file logs**](#-pretty-local-output--rotating-file-logs) | All 5 languages |
| 🕶️ | [**Sensitive-key redaction**](#-sensitive-key-redaction) | All 5 languages |
| 🖥️ | [**Browser logging**](#-browser-logging) | TypeScript only |
| | Capability | Where |
| --- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
| 🔗 | [**Correlation across services**](#-correlation-across-services) | All 5 languages |
| ⚡ | [**AWS context, captured automatically**](#-aws-context-captured-automatically) | All 5 languages |
| 🔭 | [**Logs that join your traces**](#-logs-that-join-your-traces) | TS · Python · Rust · Go (+ .NET via `Activity`) |
| 📍 | [**Exact caller location**](#-exact-caller-location) | All 5 languages |
| 🎨 | [**Pretty local output + rotating file logs**](#-pretty-local-output--rotating-file-logs) | All 5 languages |
| 🕶️ | [**Sensitive-key redaction**](#-sensitive-key-redaction) | All 5 languages |
| 🖥️ | [**Browser logging**](#-browser-logging) | TypeScript only |

### 🔗 Correlation across services

Expand Down Expand Up @@ -157,18 +157,18 @@ Every entry includes where in the code it was emitted, in all five languages:
"callerContext": {
"stack": [
"at UserService.createUser (/src/services/UserService.ts:42:16)",
"at processRequest (/src/handlers/userHandler.ts:15:23)"
]
}
"at processRequest (/src/handlers/userHandler.ts:15:23)",
],
},
}
```

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` |
| 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` |

```jsonc
{ "caller": { "file": "UserService.cs", "line": 42, "function": "CreateUser" } }
Expand Down Expand Up @@ -200,29 +200,29 @@ Every port scrubs values whose keys match a redaction list (case-insensitive, re
**TypeScript only.** `BrowserLogger` captures device type, browser name/version, platform, and user agent, and correlates fetches to your backend logs:

```typescript
import { BrowserLogger } from '@smooai/logger/browser/BrowserLogger';
import { BrowserLogger } from "@smooai/logger/browser/BrowserLogger";

const logger = new BrowserLogger({ name: 'CheckoutFlow' });
const logger = new BrowserLogger({ name: "CheckoutFlow" });

const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'X-Correlation-Id': logger.correlationId() },
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "X-Correlation-Id": logger.correlationId() },
});
logger.addResponseContext(response);
logger.info('Checkout completed', { orderId: data.id });
logger.info("Checkout completed", { orderId: data.id });
```

---

## 📦 Install

| Language | Package | Install |
| --- | --- | --- |
| **TypeScript** | [`@smooai/logger`](https://www.npmjs.com/package/@smooai/logger) | `pnpm add @smooai/logger` |
| **Python** | [`smooai-logger`](https://pypi.org/project/smooai-logger/) | `pip install smooai-logger` (or `uv add smooai-logger`) |
| **Rust** | [`smooai-logger`](https://crates.io/crates/smooai-logger) | `cargo add smooai-logger` |
| **Go** | [`github.com/SmooAI/logger/go/v4`](https://pkg.go.dev/github.com/SmooAI/logger/go/v4) | `go get github.com/SmooAI/logger/go/v4` |
| **.NET** | [`SmooAI.Logger`](https://www.nuget.org/packages/SmooAI.Logger) | `dotnet add package SmooAI.Logger` |
| Language | Package | Install |
| -------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **TypeScript** | [`@smooai/logger`](https://www.npmjs.com/package/@smooai/logger) | `pnpm add @smooai/logger` |
| **Python** | [`smooai-logger`](https://pypi.org/project/smooai-logger/) | `pip install smooai-logger` (or `uv add smooai-logger`) |
| **Rust** | [`smooai-logger`](https://crates.io/crates/smooai-logger) | `cargo add smooai-logger` |
| **Go** | [`github.com/SmooAI/logger/go/v4`](https://pkg.go.dev/github.com/SmooAI/logger/go/v4) | `go get github.com/SmooAI/logger/go/v4` |
| **.NET** | [`SmooAI.Logger`](https://www.nuget.org/packages/SmooAI.Logger) | `dotnet add package SmooAI.Logger` |

## 🚀 Quickstart

Expand All @@ -234,7 +234,7 @@ 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.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" });

Expand All @@ -261,22 +261,22 @@ Per-language quickstarts, with full API docs:

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.
| 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`](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`](src/parity-corpus.spec.ts)), Python ([`python/tests/test_parity_corpus.py`](python/tests/test_parity_corpus.py)), Rust ([`rust/logger/tests/parity_corpus.rs`](rust/logger/tests/parity_corpus.rs)), Go ([`go/parity_corpus_test.go`](go/parity_corpus_test.go)), and .NET ([`dotnet/tests/SmooAI.Logger.Tests/ParityCorpusTests.cs`](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](#-exact-caller-location).
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"check:versions": "node scripts/check-versions.mjs",
"ci:publish": "node scripts/ci-publish.mjs",
"format": "oxfmt --write . && pnpm run python:format && pnpm run rust:fmt && pnpm run go:fmt",
"format:check": "oxfmt --check . && (cd python && uv run ruff format --check) && pnpm run rust:fmt:check && pnpm run go:fmt:check",
"go:build": "(cd go && go build ./...)",
"go:fmt": "(cd go && gofmt -w .)",
"go:fmt:check": "(cd go && test -z \"$(gofmt -l .)\")",
Expand All @@ -113,7 +114,7 @@
"rust:test": "(cd rust/logger && cargo test)",
"test": "vitest run --passWithNoTests && pnpm python:test && pnpm rust:test && pnpm go:test",
"typecheck": "tsc --noEmit --skipLibCheck && pnpm run python:typecheck:errors && pnpm run rust:check && pnpm run go:build",
"version": "changeset version && node scripts/sync-versions.mjs",
"version": "changeset version && node scripts/sync-versions.mjs && oxfmt --write CHANGELOG.md package.json",
"version:sync": "node scripts/sync-versions.mjs",
"watch": "tsdown --watch"
},
Expand All @@ -134,6 +135,8 @@
},
"devDependencies": {
"@changesets/cli": "^2.28.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.55.0",
"@opentelemetry/context-async-hooks": "^1.30.0",
"@opentelemetry/sdk-logs": "^0.55.0",
"@opentelemetry/sdk-trace-base": "^1.30.0",
Expand All @@ -153,7 +156,9 @@
"vite": "^6.2.4",
"vite-node": "^3.1.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.1",
"vitest": "^3.1.1"
},
"peerDependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.55.0"
},
Expand All @@ -169,9 +174,5 @@
"@smooai/config-typescript",
"esbuild"
]
},
"peerDependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.55.0"
}
}
3 changes: 1 addition & 2 deletions python/tests/test_otel_correlation.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ def test_no_otel_consumer_means_no_bridged_record(monkeypatch):
try:
Logger().info("hello")
assert app_handler.records == [], (
"a plain root handler received a bridged record, so every consumer "
"with logging.basicConfig() double-prints every line"
"a plain root handler received a bridged record, so every consumer with logging.basicConfig() double-prints every line"
)
finally:
root.removeHandler(app_handler)
Expand Down
2 changes: 1 addition & 1 deletion rust/logger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ cargo add smooai-logger
> frame inside this crate. Only the file basename is emitted. `function` is absent: `std::panic::Location`
> carries no symbol name and resolving one would mean capturing a backtrace on every line — Go and .NET
> do include it. TypeScript and Python emit a multi-frame `callerContext.stack` instead of this
> single-frame shape. Full stack traces are still serialized for logged *errors*.
> single-frame shape. Full stack traces are still serialized for logged _errors_.

### Track Requests Across Services

Expand Down
5 changes: 3 additions & 2 deletions src/Logger.otel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ describe("Logger OTel correlation", () => {
logger.setCorrelationId("11111111-2222-3333-4444-555555555555");
const span = tracerProvider.getTracer("test").startSpan("valid");

const built = context.with(trace.setSpan(context.active(), span), () =>
(logger as any).buildLogObject(Level.Info, ["hello"])[0],
const built = context.with(
trace.setSpan(context.active(), span),
() => (logger as any).buildLogObject(Level.Info, ["hello"])[0],
);
span.end();

Expand Down
Loading