Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 67 additions & 2 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import Mime from "@effect/platform-node/Mime";
import { Effect, FileSystem, Option, Path } from "effect";
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
import { Effect, FileSystem, Layer, Option, Path } from "effect";
import {
HttpBody,
HttpClient,
HttpClientResponse,
HttpRouter,
HttpServerResponse,
HttpServerRequest,
} from "effect/unstable/http";

import {
ATTACHMENTS_ROUTE_PREFIX,
Expand All @@ -9,10 +16,68 @@ import {
} from "./attachmentPaths";
import { resolveAttachmentPathById } from "./attachmentStore";
import { ServerConfig } from "./config";
import { decodeOtlpTraceRecords, OtlpTracePayloadSchema } from "./observability/TraceRecord.ts";
import { BrowserTraceCollector } from "./observability/Services/BrowserTraceCollector.ts";
import { ProjectFaviconResolver } from "./project/Services/ProjectFaviconResolver";

const PROJECT_FAVICON_CACHE_CONTROL = "public, max-age=3600";
const FALLBACK_PROJECT_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#6b728080" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-fallback="project-favicon"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2Z"/></svg>`;
const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";

export const otlpTracesProxyRouteLayer = HttpRouter.add(
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
"POST",
OTLP_TRACES_PROXY_PATH,
Effect.gen(function* () {
const config = yield* ServerConfig;
const otlpTracesUrl = config.otlpTracesUrl;
const browserTraceCollector = yield* BrowserTraceCollector;
const httpClient = yield* HttpClient.HttpClient;

// Get raw body
const body = yield* Effect.service(HttpServerRequest.HttpServerRequest).pipe(
Effect.flatMap((request) => request.arrayBuffer),
Effect.map((buffer) => new Uint8Array(buffer)),
);

// Collect traces to local trace sink
yield* HttpServerRequest.schemaBodyJson(OtlpTracePayloadSchema).pipe(
Effect.map(decodeOtlpTraceRecords),
Effect.flatMap(browserTraceCollector.record),
Effect.catch((cause) => Effect.logWarning("Failed to record browser OTLP traces", { cause })),
);
Comment thread
cursor[bot] marked this conversation as resolved.

Comment thread
macroscopeapp[bot] marked this conversation as resolved.
if (otlpTracesUrl === undefined) {
return HttpServerResponse.empty({ status: 204 });
}

// Forward request to remote OTLP traces endpoint
return yield* httpClient
.post(otlpTracesUrl, {
body: HttpBody.uint8Array(body),
})
.pipe(
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.as(HttpServerResponse.empty({ status: 204 })),
Effect.tapError((cause) =>
Effect.logWarning("Failed to export browser OTLP traces", {
cause,
otlpTracesUrl,
}),
),
Effect.catch(() =>
Effect.succeed(HttpServerResponse.text("Trace export failed.", { status: 502 })),
),
);
}),
).pipe(
Layer.provide(
HttpRouter.cors({
allowedMethods: ["POST", "OPTIONS"],
allowedHeaders: ["content-type"],
maxAge: 600,
}),
),
);

export const attachmentsRouteLayer = HttpRouter.add(
"GET",
Expand Down
26 changes: 23 additions & 3 deletions apps/server/src/observability/Layers/Observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/obse
import { ServerConfig } from "../../config.ts";
import { ServerLoggerLive } from "../../serverLogger.ts";
import { makeLocalFileTracer } from "../LocalFileTracer.ts";
import { BrowserTraceCollector } from "../Services/BrowserTraceCollector.ts";
import { makeTraceSink } from "../TraceSink.ts";

const otlpSerializationLayer = OtlpSerialization.layerJson;

Expand All @@ -16,9 +18,14 @@ export const ObservabilityLive = Layer.unwrap(
Layer.succeed(References.TracerTimingEnabled, config.traceTimingEnabled),
);

const tracerLayer = Layer.effect(
Tracer.Tracer,
const tracerLayer = Layer.unwrap(
Effect.gen(function* () {
const sink = yield* makeTraceSink({
filePath: config.serverTracePath,
maxBytes: config.traceMaxBytes,
maxFiles: config.traceMaxFiles,
batchWindowMs: config.traceBatchWindowMs,
});
const delegate =
config.otlpTracesUrl === undefined
? undefined
Expand All @@ -34,13 +41,26 @@ export const ObservabilityLive = Layer.unwrap(
},
});

return yield* makeLocalFileTracer({
const tracer = yield* makeLocalFileTracer({
filePath: config.serverTracePath,
maxBytes: config.traceMaxBytes,
maxFiles: config.traceMaxFiles,
batchWindowMs: config.traceBatchWindowMs,
sink,
...(delegate ? { delegate } : {}),
});

return Layer.mergeAll(
Layer.succeed(Tracer.Tracer, tracer),
Layer.succeed(BrowserTraceCollector, {
record: (records) =>
Effect.sync(() => {
for (const record of records) {
sink.push(record);
}
}),
}),
);
}),
).pipe(Layer.provideMerge(otlpSerializationLayer));

Expand Down
6 changes: 3 additions & 3 deletions apps/server/src/observability/LocalFileTracer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from "node:path";
import { assert, describe, it } from "@effect/vitest";
import { Effect, Layer, Logger, References, Tracer } from "effect";

import type { TraceRecord } from "./TraceRecord.ts";
import type { EffectTraceRecord } from "./TraceRecord.ts";
import { makeLocalFileTracer } from "./LocalFileTracer.ts";

const makeTestLayer = (tracePath: string) =>
Expand All @@ -23,13 +23,13 @@ const makeTestLayer = (tracePath: string) =>
Layer.succeed(References.MinimumLogLevel, "Info"),
);

const readTraceRecords = (tracePath: string): Array<TraceRecord> =>
const readTraceRecords = (tracePath: string): Array<EffectTraceRecord> =>
fs
.readFileSync(tracePath, "utf8")
.trim()
.split("\n")
.filter((line) => line.length > 0)
.map((line) => JSON.parse(line) as TraceRecord);
.map((line) => JSON.parse(line) as EffectTraceRecord);

describe("LocalFileTracer", () => {
it.effect("writes nested spans to disk and captures log messages as span events", () =>
Expand Down
17 changes: 10 additions & 7 deletions apps/server/src/observability/LocalFileTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type * as Exit from "effect/Exit";
import { Effect, Option, Tracer } from "effect";

import { spanToTraceRecord } from "./TraceRecord.ts";
import { makeTraceSink } from "./TraceSink.ts";
import { makeTraceSink, type TraceSink } from "./TraceSink.ts";

export interface LocalFileTracerOptions {
readonly filePath: string;
readonly maxBytes: number;
readonly maxFiles: number;
readonly batchWindowMs: number;
readonly delegate?: Tracer.Tracer;
readonly sink?: TraceSink;
}

class LocalFileSpan implements Tracer.Span {
Expand Down Expand Up @@ -82,12 +83,14 @@ class LocalFileSpan implements Tracer.Span {
export const makeLocalFileTracer = Effect.fn("makeLocalFileTracer")(function* (
options: LocalFileTracerOptions,
) {
const sink = yield* makeTraceSink({
filePath: options.filePath,
maxBytes: options.maxBytes,
maxFiles: options.maxFiles,
batchWindowMs: options.batchWindowMs,
});
const sink =
options.sink ??
(yield* makeTraceSink({
filePath: options.filePath,
maxBytes: options.maxBytes,
maxFiles: options.maxFiles,
batchWindowMs: options.batchWindowMs,
}));

const delegate =
options.delegate ??
Expand Down
13 changes: 13 additions & 0 deletions apps/server/src/observability/Services/BrowserTraceCollector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ServiceMap } from "effect";
import type { Effect } from "effect";

import type { TraceRecord } from "../TraceRecord.ts";

export interface BrowserTraceCollectorShape {
readonly record: (records: ReadonlyArray<TraceRecord>) => Effect.Effect<void>;
}

export class BrowserTraceCollector extends ServiceMap.Service<
BrowserTraceCollector,
BrowserTraceCollectorShape
>()("t3/observability/Services/BrowserTraceCollector") {}
Loading
Loading