镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content
Closed
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
45 changes: 45 additions & 0 deletions apps/server/src/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,51 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
}),
);

it.effect("does not create a custom trace directory when tracing is disabled", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-config-tracing-off-" });
const serverTracePath = path.join(baseDir, "custom-traces", "server.trace.ndjson");

const resolved = yield* resolveServerConfig(
{
mode: Option.some("desktop"),
port: Option.some(4888),
host: Option.none(),
baseDir: Option.some(baseDir),
cwd: Option.none(),
devUrl: Option.none(),
noBrowser: Option.none(),
bootstrapFd: Option.none(),
autoBootstrapProjectFromCwd: Option.none(),
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
},
Option.none(),
).pipe(
Effect.provide(
Layer.mergeAll(
ConfigProvider.layer(
ConfigProvider.fromEnv({
env: {
T3CODE_TRACE_FILE: serverTracePath,
T3CODE_TRACE_MIN_LEVEL: "None",
},
}),
),
NetService.layer,
),
),
);

assert.equal(resolved.traceMinLevel, "None");
assert.equal(resolved.serverTracePath, serverTracePath);
expect(yield* fs.exists(path.dirname(serverTracePath))).toBe(false);
}),
);

it.effect("applies flag then env precedence over bootstrap envelope values", () =>
Effect.gen(function* () {
const { join } = yield* Path.Path;
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ export const resolveServerConfig = (
derivedPaths.settingsPath,
);
const serverTracePath = env.traceFile ?? derivedPaths.serverTracePath;
yield* fs.makeDirectory(path.dirname(serverTracePath), { recursive: true });
if (env.traceMinLevel !== "None") {
yield* fs.makeDirectory(path.dirname(serverTracePath), { recursive: true });
}
const startupPresentation = options?.startupPresentation ?? "browser";
const isHeadlessStartup = startupPresentation === "headless";
const noBrowser = Option.getOrElse(
Expand Down
67 changes: 67 additions & 0 deletions apps/server/src/observability/Layers/Observability.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import type { TraceRecord } from "@t3tools/shared/observability";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import { FetchHttpClient } from "effect/unstable/http";

import * as ServerConfig from "../../config.ts";
import * as ResourceAttribution from "../../resourceTelemetry/ResourceAttribution.ts";
import * as BrowserTraceCollector from "../BrowserTraceCollector.ts";
import { ObservabilityLive } from "./Observability.ts";

const browserTraceRecord: TraceRecord = {
type: "otlp-span",
name: "browser.test",
kind: "internal",
traceId: "11111111111111111111111111111111",
spanId: "2222222222222222",
sampled: true,
startTimeUnixNano: "1000000",
endTimeUnixNano: "2000000",
durationMs: 1,
attributes: {},
events: [],
links: [],
resourceAttributes: {
"service.name": "t3-web",
},
scope: {
name: "effect",
attributes: {},
},
};

it.layer(NodeServices.layer)("server observability", (it) => {
it.effect("does not create a trace file for server or browser spans when disabled", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-observability-off-" });
const tracePath = path.join(baseDir, "disabled-traces", "server.trace.ndjson");
const defaultConfig = yield* ServerConfig.ServerConfig.pipe(
Effect.provide(ServerConfig.layerTest(process.cwd(), baseDir)),
);
const config = ServerConfig.make({
...defaultConfig,
traceMinLevel: "None",
serverTracePath: tracePath,
});
const observabilityLayer = ObservabilityLive.pipe(
Layer.provideMerge(ResourceAttribution.layer),
Layer.provide(ServerConfig.layer(config)),
);

yield* Effect.gen(function* () {
const browserTraceCollector = yield* BrowserTraceCollector.BrowserTraceCollector;
yield* Effect.void.pipe(Effect.withSpan("server.test"));
yield* browserTraceCollector.record([browserTraceRecord]);
}).pipe(Effect.provide(observabilityLayer));

assert.isFalse(yield* fs.exists(tracePath));
assert.isFalse(yield* fs.exists(path.dirname(tracePath)));
}).pipe(Effect.provide(FetchHttpClient.layer)),
);
});
40 changes: 25 additions & 15 deletions apps/server/src/observability/Layers/Observability.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability";
import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability";
import { makeLocalFileTracer, makeTraceSink, type TraceSink } from "@t3tools/shared/observability";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as References from "effect/References";
Expand All @@ -16,6 +16,13 @@ import * as BrowserTraceCollector from "../BrowserTraceCollector.ts";

const otlpSerializationLayer = OtlpSerialization.layerJson;

const makeNoopTraceSink = (filePath: string): TraceSink => ({
filePath,
push: () => undefined,
flush: Effect.void,
close: () => Effect.void,
});

export const ObservabilityLive = Layer.unwrap(
Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
Expand All @@ -29,20 +36,23 @@ export const ObservabilityLive = Layer.unwrap(

const tracerLayer = Layer.unwrap(
Effect.gen(function* () {
const sink = yield* makeTraceSink({
filePath: config.serverTracePath,
maxBytes: config.traceMaxBytes,
maxFiles: config.traceMaxFiles,
batchWindowMs: config.traceBatchWindowMs,
onFlush: (stats) =>
attribution.record({
component: "server-trace",
operation: "append",
logicalWriteBytes: stats.logicalWriteBytes,
count: stats.count,
durationMs: stats.durationMs,
}),
});
const sink =
config.traceMinLevel === "None"
? makeNoopTraceSink(config.serverTracePath)
: yield* makeTraceSink({
filePath: config.serverTracePath,
maxBytes: config.traceMaxBytes,
maxFiles: config.traceMaxFiles,
batchWindowMs: config.traceBatchWindowMs,
onFlush: (stats) =>
attribution.record({
component: "server-trace",
operation: "append",
logicalWriteBytes: stats.logicalWriteBytes,
count: stats.count,
durationMs: stats.durationMs,
}),
});
const delegate =
config.otlpTracesUrl === undefined
? undefined
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6153,6 +6153,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {

yield* buildAppUnderTest({
config: {
traceMinLevel: "None",
otlpTracesUrl: "http://localhost:4318/v1/traces",
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
},
Expand Down Expand Up @@ -6185,7 +6186,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
assert.deepEqual(first.config.issues, []);
assert.deepEqual(first.config.providers, providers);
assert.equal(path.basename(first.config.observability.logsDirectoryPath), "logs");
assert.equal(first.config.observability.localTracingEnabled, true);
assert.equal(first.config.observability.localTracingEnabled, false);
assert.equal(first.config.observability.otlpTracesUrl, "http://localhost:4318/v1/traces");
assert.equal(first.config.observability.otlpTracesEnabled, true);
assert.equal(first.config.observability.otlpMetricsUrl, "http://localhost:4318/v1/metrics");
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ const makeWsRpcLayer = (
),
observability: {
logsDirectoryPath: config.logsDir,
localTracingEnabled: true,
localTracingEnabled: config.traceMinLevel !== "None",
...(config.otlpTracesUrl !== undefined
? { otlpTracesUrl: config.otlpTracesUrl }
: {}),
Expand Down
24 changes: 16 additions & 8 deletions docs/operations/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
T3 Code has one server-side observability model:

- pretty logs go to stdout for humans
- completed spans go to a local NDJSON trace file
- completed spans go to a local NDJSON trace file by default
- traces and metrics can also be exported over OTLP to a real backend like Grafana LGTM

The local trace file is the persisted source of truth for normal local launches. Those launches do not
write a separate server log file, but SSH-managed launches also persist the remote process's
stdout/stderr at `~/.t3/ssh-launch/<state>/server.log`.
When enabled, the local trace file is the persisted source of truth for normal local launches. Those
launches do not write a separate server log file, but SSH-managed launches also persist the remote
process's stdout/stderr at `~/.t3/ssh-launch/<state>/server.log`.

## Where To Find Things

Expand All @@ -27,8 +27,8 @@ If you want a log message to show up in the trace file, emit it inside an active

### Traces

Completed spans are written as NDJSON records to `serverTracePath`. The default depends on how the
server starts: production and explicitly configured homes use
When local tracing is enabled, completed spans are written as NDJSON records to `serverTracePath`.
The default depends on how the server starts: production and explicitly configured homes use
`<home>/userdata/logs/server.trace.ndjson` (so `~/.t3/userdata/...` by default, or
`/custom/path/userdata/...` with `--home-dir /custom/path`), a linked worktree dev run uses
`<worktree>/.t3/userdata/logs/server.trace.ndjson`, and an implicit dev run outside a linked
Expand All @@ -46,6 +46,12 @@ Important fields common to both record types:
`effect-span` records also contain `exit` with `Success`, `Failure`, or `Interrupted`. `otlp-span`
records instead carry OTLP resource, scope, and optional status fields.

Set `T3CODE_TRACE_MIN_LEVEL=None` before launch to disable local tracing. In this mode, neither
server spans nor browser-submitted spans create or write the local trace file. `None` is also the
server's global minimum span level, so server spans are not exported over OTLP either. Browser
trace payloads are still forwarded when `T3CODE_OTLP_TRACES_URL` is configured, and OTLP metrics
are unaffected.

The `TraceRecord`, `EffectTraceRecord`, and `OtlpTraceRecord` schemas live in
`packages/shared/src/observability.ts`.

Expand Down Expand Up @@ -76,7 +82,8 @@ There are two useful modes:
- local-only: stdout + local `server.trace.ndjson`
- full local observability: stdout + local trace file + OTLP export to Grafana/Tempo/Prometheus

The local trace file is always on. OTLP export is opt-in.
The local trace file is on by default. Set `T3CODE_TRACE_MIN_LEVEL=None` to turn it off. OTLP export
is opt-in.

### Option 1: Local Traces Only

Expand Down Expand Up @@ -519,7 +526,8 @@ Local trace file:
- `T3CODE_TRACE_MAX_BYTES`: per-file rotation size, default `10485760`
- `T3CODE_TRACE_MAX_FILES`: rotated file count, default `10`
- `T3CODE_TRACE_BATCH_WINDOW_MS`: flush window, default `200`
- `T3CODE_TRACE_MIN_LEVEL`: minimum trace level, default `Info`
- `T3CODE_TRACE_MIN_LEVEL`: minimum trace level, default `Info`; use `None` to disable local tracing
and server span creation
- `T3CODE_TRACE_TIMING_ENABLED`: enable timing metadata, default `true`

OTLP export:
Expand Down
Loading