@ogcio/fastify-o11y
Version:
Custom fastify plugin for OGCIO Observability
119 lines (103 loc) • 3.65 kB
text/typescript
import { getActiveSpan, getMetric } from "@ogcio/o11y-sdk-node";
import type { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import type { FastifySchemaValidationError } from "fastify/types/schema.js";
import { SpanStatusCode } from "@opentelemetry/api";
export type PluginConfig = {
traceErrors?: boolean; // set to `false` to disable error tracing, defaults to `true`.
};
const ACCESS_CONTROL_EXPOSE_HEADERS = "access-control-expose-headers";
const httpResponsesCounter = getMetric<"counter", { status_code: number }>(
"counter",
{
meterName: "http.server.responses",
metricName: "http.server.responses",
},
);
export const X_TRACE_ID = "x-trace-id";
export default fp(
async (server: FastifyInstance, pluginConfig: PluginConfig = {}) => {
server.addHook("onRequest", (_request, reply, done) => {
try {
const traceId = getActiveSpan()?.spanContext()?.traceId;
if (traceId) {
reply.header(X_TRACE_ID, traceId);
}
} catch (_nonCriticalError) {
} finally {
done();
}
});
server.addHook("onResponse", (_request, reply, done) => {
try {
httpResponsesCounter.add(1, { status_code: reply.statusCode });
if (reply.getHeader(ACCESS_CONTROL_EXPOSE_HEADERS)) {
// x-trace-id header is already exposed, exit
if (
reply
.getHeader(ACCESS_CONTROL_EXPOSE_HEADERS)
?.toString()
.includes(X_TRACE_ID)
) {
return done();
}
reply.header(
ACCESS_CONTROL_EXPOSE_HEADERS,
`${X_TRACE_ID}, ${reply.getHeader(ACCESS_CONTROL_EXPOSE_HEADERS)}`,
);
} else {
reply.header(ACCESS_CONTROL_EXPOSE_HEADERS, X_TRACE_ID);
}
} finally {
done();
}
});
if (pluginConfig.traceErrors !== false) {
server.addHook("onError", async (_request, _reply, error) => {
try {
const span = getActiveSpan();
if (span) {
// Add validation error attributes without PII
span.setAttributes({
"error.type": "error",
"error.code": error.statusCode,
"http.route": _request.routeOptions.url,
"http.method": _request.method,
});
if (error.validation) {
span.setAttributes({
"error.type": "validation_error",
"error.validation.failed": true,
});
const validationDetails = error.validation.map(
(validationError: FastifySchemaValidationError) => ({
field: validationError.instancePath || "root",
rule: validationError.keyword,
schemaPath: validationError.schemaPath,
message: validationError.message ?? error.message,
}),
);
validationDetails.forEach((detail, index) => {
span.addEvent(`validation_error_${index}`, {
"validation.field": detail.field,
"validation.rule": detail.rule,
"validation.message": detail.message,
"validation.schema_path": detail.schemaPath,
});
});
}
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: "Request failed",
});
}
} catch (_nonCriticalError) {}
});
}
},
{
fastify: "4.x",
name: "@ogcio/fastify-o11y",
},
);