@cerbos/opentelemetry
Version:
OpenTelemetry instrumentation for the @cerbos/grpc and @cerbos/http client libraries
246 lines (218 loc) • 5.83 kB
text/typescript
import type { Attributes, SpanStatus, Tracer } from "@opentelemetry/api";
import {
SpanKind,
SpanStatusCode,
context,
propagation,
trace,
} from "@opentelemetry/api";
import {
ATTR_RPC_GRPC_STATUS_CODE,
ATTR_RPC_METHOD,
ATTR_RPC_SERVICE,
ATTR_RPC_SYSTEM,
} from "@opentelemetry/semantic-conventions/incubating";
import type {
_AbortHandler,
_Method,
_MethodKind,
_Request,
_Response,
_Service,
_Transport,
} from "@cerbos/core";
import { NotOK, Status } from "@cerbos/core";
import type { CerbosInstrumentation } from "./instrumentation";
import type { Instruments } from "./instruments";
type TransportMethod<
Service extends _Service,
MethodKind extends _MethodKind,
Method extends _Method<Service, MethodKind>,
> = (
service: Service,
method: Method,
request: _Request<Service, MethodKind, Method>,
headers: Headers,
abortHandler: _AbortHandler,
) => TransportReturn<Service, MethodKind, Method>;
type TransportReturn<
Service extends _Service,
MethodKind extends _MethodKind,
Method extends _Method<Service, MethodKind>,
> = MethodKind extends "unary"
? Promise<_Response<Service, MethodKind, Method>>
: MethodKind extends "serverStream"
? AsyncGenerator<_Response<Service, MethodKind, Method>, void, undefined>
: never;
const serviceNames: Record<_Service, string> = {
admin: "cerbos.svc.v1.CerbosAdminService",
cerbos: "cerbos.svc.v1.CerbosService",
health: "grpc.health.v1.Health",
};
export class Transport implements _Transport {
private readonly transport: {
[MethodKind in _MethodKind]: _Transport[MethodKind];
};
public constructor(
private readonly instrumentation: CerbosInstrumentation,
transport: _Transport,
) {
this.transport = {
unary: transport.unary.bind(transport),
serverStream: transport.serverStream.bind(transport),
};
}
public async unary<
Service extends _Service,
Method extends _Method<Service, "unary">,
>(
service: Service,
method: Method,
request: _Request<Service, "unary", Method>,
headers: Headers,
abortHandler: _AbortHandler,
): Promise<_Response<Service, "unary", Method>> {
const call = this.call(
service,
"unary",
method,
request,
headers,
abortHandler,
);
try {
const response = await call.result;
call.succeeded();
return response;
} catch (error) {
call.failed(error);
throw error;
}
}
public async *serverStream<
Service extends _Service,
Method extends _Method<Service, "serverStream">,
>(
service: Service,
method: Method,
request: _Request<Service, "serverStream", Method>,
headers: Headers,
abortHandler: _AbortHandler,
): AsyncGenerator<
_Response<Service, "serverStream", Method>,
void,
undefined
> {
const call = this.call(
service,
"serverStream",
method,
request,
headers,
abortHandler,
);
let done = false;
try {
yield* call.result;
done = true;
call.succeeded();
} catch (error) {
done = true;
call.failed(error);
throw error;
} finally {
if (!done) {
call.failed(abortHandler.error());
}
}
}
private call<
Service extends _Service,
MethodKind extends _MethodKind,
Method extends _Method<Service, MethodKind>,
>(
service: Service,
methodKind: MethodKind,
method: Method,
request: _Request<Service, MethodKind, Method>,
headers: Headers,
abortHandler: _AbortHandler,
): {
result: TransportReturn<Service, MethodKind, Method>;
succeeded: () => void;
failed: (error: unknown) => void;
} {
const startTime = performance.now();
const serviceName = serviceNames[service];
const methodName = `${method.charAt(0).toUpperCase()}${method.slice(1)}`;
const status: SpanStatus = { code: SpanStatusCode.UNSET };
const attributes: Attributes = {
[ATTR_RPC_SYSTEM]: "grpc",
[ATTR_RPC_SERVICE]: serviceName,
[ATTR_RPC_METHOD]: methodName,
};
const span = this.tracer.startSpan(`${serviceName}/${methodName}`, {
kind: SpanKind.CLIENT,
startTime,
});
const activeContext = trace.setSpan(context.active(), span);
propagation.inject(activeContext, headers, {
set(carrier, key, value) {
carrier.set(key, value);
},
});
const finish = (): void => {
const endTime = performance.now();
span.setStatus(status);
span.setAttributes(attributes);
span.end(endTime);
this.instruments.duration.record(endTime - startTime, attributes);
};
const call = {
succeeded: (): void => {
attributes[ATTR_RPC_GRPC_STATUS_CODE] = Status.OK;
finish();
},
failed: (error: unknown): void => {
status.code = SpanStatusCode.ERROR;
if (error instanceof Error) {
status.message = error.message;
attributes["cerbos.error"] = error.message;
if (error instanceof NotOK) {
attributes[ATTR_RPC_GRPC_STATUS_CODE] = error.code;
}
}
finish();
},
};
const transport = this.transport[methodKind] as TransportMethod<
Service,
MethodKind,
Method
>;
try {
return {
result: context.with(
activeContext,
transport,
undefined,
service,
method,
request,
headers,
abortHandler,
),
...call,
};
} catch (error) {
call.failed(error);
throw error;
}
}
private get instruments(): Instruments {
return this.instrumentation._instruments;
}
private get tracer(): Tracer {
return this.instrumentation._tracer;
}
}