mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-09 23:55:12 +00:00
# Which Problems Are Solved All http 2 based connect-es clients caused memory leaks in our testing using the login v2 application as the host. # How the Problems Are Solved Switch to connect-rpc based http 1.1 transport for the login v2 # Additional Changes Improved the otel interceptor implementation and tests. # Additional Context - connect-rpc is only enabled for v2 API's so we have to make sure to not use any v1 apis in the login but this should be the case anyways. - Discussion #10562 --------- Co-authored-by: Max Peintner <[email protected]>
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
/**
|
|
* OpenTelemetry gRPC/Connect Interceptor
|
|
*
|
|
* Provides distributed tracing for Connect/gRPC calls. Creates client spans
|
|
* with RPC semantic conventions, propagates W3C trace context headers
|
|
* (traceparent/tracestate) and baggage, and records errors with gRPC status
|
|
* codes.
|
|
*
|
|
* @see https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* import { otelGrpcInterceptor } from "@/lib/grpc/interceptors/otel";
|
|
*
|
|
* const transport = createTransport({
|
|
* baseUrl: "https://api.example.com",
|
|
* interceptors: [otelGrpcInterceptor],
|
|
* });
|
|
* ```
|
|
*/
|
|
|
|
import { ConnectError, Interceptor } from "@connectrpc/connect";
|
|
import { context, propagation, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
|
|
const TRACER_NAME = "zitadel-login-grpc" as const;
|
|
|
|
/**
|
|
* Extracts the hostname from a URL string. Returns "unknown" if the URL is
|
|
* undefined or cannot be parsed.
|
|
*/
|
|
function parseHostname(url: string): string {
|
|
try {
|
|
return new URL(url).hostname;
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* OpenTelemetry interceptor for Connect/gRPC calls. Wraps each RPC call in a
|
|
* client span, injects trace context headers for distributed tracing, and
|
|
* records any errors that occur during the call.
|
|
*/
|
|
export const otelGrpcInterceptor: Interceptor = (next) =>
|
|
async function tracedCall(req) {
|
|
const serviceName: string = req.service?.typeName ?? "unknown";
|
|
const methodName: string = req.method?.name ?? "unknown";
|
|
|
|
return trace.getTracer(TRACER_NAME).startActiveSpan(
|
|
`${serviceName}/${methodName}`,
|
|
{
|
|
kind: SpanKind.CLIENT,
|
|
attributes: {
|
|
"rpc.system": "grpc",
|
|
"rpc.service": serviceName,
|
|
"rpc.method": methodName,
|
|
"server.address": parseHostname(req.url),
|
|
},
|
|
},
|
|
async (span) => {
|
|
const ctx = trace.setSpan(context.active(), span);
|
|
propagation.inject(ctx, req.header, {
|
|
set: (carrier, key, value) => carrier.set(key, value),
|
|
});
|
|
|
|
try {
|
|
const response = await next(req);
|
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
return response;
|
|
} catch (err) {
|
|
const exception = err instanceof Error ? err : new Error(String(err));
|
|
span.recordException(exception);
|
|
|
|
if (exception instanceof ConnectError) {
|
|
span.setAttribute("rpc.grpc.status_code", exception.code);
|
|
}
|
|
span.setStatus({
|
|
code: SpanStatusCode.ERROR,
|
|
message: exception.message,
|
|
});
|
|
throw err;
|
|
} finally {
|
|
span.end();
|
|
}
|
|
},
|
|
);
|
|
};
|