Skip to content

Commit

Permalink
feat: add open telemetry instrumentation
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacasonato committed Dec 10, 2024
1 parent f1b6b4c commit f512c24
Show file tree
Hide file tree
Showing 19 changed files with 840 additions and 94 deletions.
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@fresh/plugin-tailwind": "./plugin-tailwindcss/src/mod.ts",
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.0",
"@marvinh-test/fresh-island": "jsr:@marvinh-test/fresh-island@^0.0.1",
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
"@preact/signals": "npm:@preact/signals@^1.2.3",
"@std/async": "jsr:@std/async@1",
"@std/cli": "jsr:@std/cli@1",
Expand Down Expand Up @@ -75,7 +76,7 @@
"ts-morph": "npm:ts-morph@^22.0.0"
},
"compilerOptions": {
"lib": ["dom", "dom.asynciterable", "deno.ns"],
"lib": ["dom", "dom.asynciterable", "deno.ns", "deno.unstable"],
"jsx": "precompile",
"jsxImportSource": "preact",
"jsxPrecompileSkipElements": ["a", "img", "source", "body", "html", "head"]
Expand Down
10 changes: 9 additions & 1 deletion deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 14 additions & 6 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import * as path from "@std/path";
import { type ComponentType, h } from "preact";
import { renderToString } from "preact-render-to-string";
import { trace } from "@opentelemetry/api";

import { DENO_DEPLOYMENT_ID } from "./runtime/build_id.ts";
import * as colors from "@std/fmt/colors";
import { type MiddlewareFn, runMiddlewares } from "./middlewares/mod.ts";
Expand All @@ -14,10 +19,7 @@ import {
type ResolvedFreshConfig,
} from "./config.ts";
import { type BuildCache, ProdBuildCache } from "./build_cache.ts";
import * as path from "@std/path";
import { type ComponentType, h } from "preact";
import type { ServerIslandRegistry } from "./context.ts";
import { renderToString } from "preact-render-to-string";
import { FinishSetup, ForgotBuild } from "./finish_setup.tsx";
import { HttpError } from "./error.ts";

Expand Down Expand Up @@ -140,8 +142,8 @@ export class App<State> {

// Special case when user calls one of these:
// - `app.mountApp("/", otherApp)`
// - `app.mountApp("*", otherApp)`
const isSelf = path === "*" || path === "/";
// - `app.mountApp("/*", otherApp)`
const isSelf = path === "/*" || path === "/";
if (isSelf && middlewares.length > 0) {
this.#router._middlewares.push(...middlewares);
}
Expand Down Expand Up @@ -205,7 +207,7 @@ export class App<State> {
? DEFAULT_NOT_ALLOWED_METHOD
: DEFAULT_NOT_FOUND;

const { params, handlers } = matched;
const { params, handlers, pattern } = matched;
const ctx = new FreshReqContext<State>(
req,
url,
Expand All @@ -217,6 +219,12 @@ export class App<State> {
this.#buildCache!,
);

const span = trace.getActiveSpan();
if (span && pattern) {
span.updateName(`${method} ${pattern}`);
span.setAttribute("http.route", pattern);
}

try {
if (handlers.length === 1 && handlers[0].length === 1) {
return handlers[0][0](ctx);
Expand Down
35 changes: 27 additions & 8 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type VNode,
} from "preact";
import { renderToString } from "preact-render-to-string";
import { SpanStatusCode } from "@opentelemetry/api";
import type { ResolvedFreshConfig } from "./config.ts";
import type { BuildCache } from "./build_cache.ts";
import {
Expand All @@ -15,6 +16,7 @@ import {
} from "./runtime/server/preact_hooks.tsx";
import { DEV_ERROR_OVERLAY_URL } from "./constants.ts";
import { BUILD_ID } from "./runtime/build_id.ts";
import { tracer } from "./otel.ts";

export interface Island {
file: string | URL;
Expand Down Expand Up @@ -184,14 +186,31 @@ export class FreshReqContext<State>
headers.set("X-Fresh-Id", partialId);
}

const html = preactRender(
vnode,
this,
this.#islandRegistry,
this.#buildCache,
partialId,
headers,
);
const html = tracer.startActiveSpan("render", (span) => {
span.setAttribute("fresh.span_type", "render");
try {
return preactRender(
vnode,
this,
this.#islandRegistry,
this.#buildCache,
partialId,
headers,
);
} catch (err) {
if (err instanceof Error) {
span.recordException(err);
} else {
span.setStatus({
code: SpanStatusCode.ERROR,
message: String(err),
});
}
throw err;
} finally {
span.end();
}
});
return new Response(html, responseInit);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/dev/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class Builder implements FreshBuilder {
const devApp = new App<T>(app.config)
.use(liveReload())
.use(devErrorOverlay())
.mountApp("*", app);
.mountApp("/*", app);

devApp.config.mode = "development";

Expand Down
7 changes: 3 additions & 4 deletions src/dev/update_check.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as semver from "@std/semver";
import * as colors from "@std/fmt/colors";
import * as path from "@std/path";
import { CURRENT_FRESH_VERSION } from "../otel.ts";

export interface CheckFile {
last_checked: string;
Expand Down Expand Up @@ -48,10 +49,8 @@ async function fetchLatestVersion() {
throw new Error(`Could not fetch latest version.`);
}

async function readCurrentVersion() {
return (await import("../../deno.json", {
with: { type: "json" },
})).default.version;
function readCurrentVersion() {
return CURRENT_FRESH_VERSION;
}

export async function updateCheck(
Expand Down
114 changes: 65 additions & 49 deletions src/middlewares/static_files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { MiddlewareFn } from "./mod.ts";
import { ASSET_CACHE_BUST_KEY } from "../runtime/shared_internal.tsx";
import { BUILD_ID } from "../runtime/build_id.ts";
import { getBuildCache } from "../context.ts";
import { tracer } from "../otel.ts";

/**
* Fresh middleware to enable file-system based routing.
Expand All @@ -25,6 +26,7 @@ export function staticFiles<T>(): MiddlewareFn<T> {
}

// Fast path bail out
const startTime = performance.now() + performance.timeOrigin;
const file = await buildCache.readFile(pathname);
if (pathname === "/" || file === null) {
// Optimization: Prevent long responses for favicon.ico requests
Expand All @@ -39,60 +41,74 @@ export function staticFiles<T>(): MiddlewareFn<T> {
return new Response("Method Not Allowed", { status: 405 });
}

const cacheKey = url.searchParams.get(ASSET_CACHE_BUST_KEY);
if (cacheKey !== null && BUILD_ID !== cacheKey) {
url.searchParams.delete(ASSET_CACHE_BUST_KEY);
const location = url.pathname + url.search;
file.close();
return new Response(null, {
status: 307,
headers: {
location,
},
});
}
const span = tracer.startSpan("static file", {
attributes: { "fresh.span_type": "static_file" },
startTime,
});

const ext = path.extname(pathname);
const etag = file.hash;
try {
const cacheKey = url.searchParams.get(ASSET_CACHE_BUST_KEY);
if (cacheKey !== null && BUILD_ID !== cacheKey) {
url.searchParams.delete(ASSET_CACHE_BUST_KEY);
const location = url.pathname + url.search;
file.close();
span.setAttribute("fresh.cache", "invalid_bust_key");
span.setAttribute("fresh.cache_key", cacheKey);
return new Response(null, {
status: 307,
headers: {
location,
},
});
}

const contentType = getContentType(ext);
const headers = new Headers({
"Content-Type": contentType ?? "text/plain",
vary: "If-None-Match",
});
const ext = path.extname(pathname);
const etag = file.hash;

const ifNoneMatch = req.headers.get("If-None-Match");
if (
ifNoneMatch !== null &&
(ifNoneMatch === etag || ifNoneMatch === `W/"${etag}"`)
) {
file.close();
return new Response(null, { status: 304, headers });
} else if (etag !== null) {
headers.set("Etag", `W/"${etag}"`);
}
const contentType = getContentType(ext);
const headers = new Headers({
"Content-Type": contentType ?? "text/plain",
vary: "If-None-Match",
});

if (
ctx.config.mode !== "development" &&
(BUILD_ID === cacheKey ||
url.pathname.startsWith(
`${ctx.config.basePath}/_fresh/js/${BUILD_ID}/`,
))
) {
headers.append("Cache-Control", "public, max-age=31536000, immutable");
} else {
headers.append(
"Cache-Control",
"no-cache, no-store, max-age=0, must-revalidate",
);
}
const ifNoneMatch = req.headers.get("If-None-Match");
if (
ifNoneMatch !== null &&
(ifNoneMatch === etag || ifNoneMatch === `W/"${etag}"`)
) {
file.close();
span.setAttribute("fresh.cache", "not_modified");
return new Response(null, { status: 304, headers });
} else if (etag !== null) {
headers.set("Etag", `W/"${etag}"`);
}

headers.set("Content-Length", String(file.size));
if (req.method === "HEAD") {
file.close();
return new Response(null, { status: 200, headers });
}
if (
ctx.config.mode !== "development" &&
(BUILD_ID === cacheKey ||
url.pathname.startsWith(
`${ctx.config.basePath}/_fresh/js/${BUILD_ID}/`,
))
) {
span.setAttribute("fresh.cache", "immutable");
headers.append("Cache-Control", "public, max-age=31536000, immutable");
} else {
span.setAttribute("fresh.cache", "no_cache");
headers.append(
"Cache-Control",
"no-cache, no-store, max-age=0, must-revalidate",
);
}

headers.set("Content-Length", String(file.size));
if (req.method === "HEAD") {
file.close();
return new Response(null, { status: 200, headers });
}

return new Response(file.readable, { headers });
return new Response(file.readable, { headers });
} finally {
span.end();
}
};
}
6 changes: 6 additions & 0 deletions src/otel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { trace } from "@opentelemetry/api";
import denoJson from "../deno.json" with { type: "json" };

export const CURRENT_FRESH_VERSION = denoJson.version;

export const tracer = trace.getTracer("fresh", CURRENT_FRESH_VERSION);
2 changes: 1 addition & 1 deletion src/plugins/fs_routes/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export async function fsRoutes<State>(
errorComponents.push(mod.component);
}
let parent = mod.path.slice(0, -"_error".length);
parent = parent === "/" ? "*" : parent + "*";
parent = parent + "*";

// Add error route as its own route
if (!specialPaths.has(mod.path)) {
Expand Down
Loading

0 comments on commit f512c24

Please sign in to comment.