diff --git a/.changeset/format-drift-and-release-tree.md b/.changeset/format-drift-and-release-tree.md new file mode 100644 index 00000000..5694c69d --- /dev/null +++ b/.changeset/format-drift-and-release-tree.md @@ -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`. diff --git a/.claude/settings.json b/.claude/settings.json index b7581fe5..8dadd230 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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 + } } diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 093a6e89..5675b479 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -72,6 +72,9 @@ jobs: - name: Lint run: pnpm lint + - name: Format check + run: pnpm format:check + - name: Test run: pnpm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4585b228..15f93284 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f27dc7..b8b17f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. @@ -63,7 +62,7 @@ ```jsonc { - "caller": { "file": "UserService.cs", "line": 42, "function": "CreateUser" } + "caller": { "file": "UserService.cs", "line": 42, "function": "CreateUser" }, } ``` @@ -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 @@ -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 diff --git a/README.md b/README.md index 78af1050..5a2ce4f4 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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" } } @@ -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 @@ -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" }); @@ -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). diff --git a/package.json b/package.json index 8ebd92f4..29dc796e 100644 --- a/package.json +++ b/package.json @@ -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 .)\")", @@ -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" }, @@ -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", @@ -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" }, @@ -169,9 +174,5 @@ "@smooai/config-typescript", "esbuild" ] - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/api-logs": "^0.55.0" } } diff --git a/python/tests/test_otel_correlation.py b/python/tests/test_otel_correlation.py index d7ade078..c69d413d 100644 --- a/python/tests/test_otel_correlation.py +++ b/python/tests/test_otel_correlation.py @@ -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) diff --git a/rust/logger/README.md b/rust/logger/README.md index 8384a5c6..9673a9ef 100644 --- a/rust/logger/README.md +++ b/rust/logger/README.md @@ -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 diff --git a/src/Logger.otel.spec.ts b/src/Logger.otel.spec.ts index 709cea69..916d22e9 100644 --- a/src/Logger.otel.spec.ts +++ b/src/Logger.otel.spec.ts @@ -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();