Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 19 additions & 3 deletions packages/adapter-opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type OpenTelemetryEvidenceOptions = {

type SpanLike = {
name?: unknown;
kind?: unknown;
attributes?: unknown;
};

Expand All @@ -38,11 +39,19 @@ function normalizeKind(value: string | undefined): ResourceContractKind | undefi
return undefined;
}

function normalizeOperation(kind: ResourceContractKind, value: string | undefined): ResourceAccessMode {
function isServerSpanKind(kind: unknown): boolean {
return kind === 2 || kind === "2" || kind === "SPAN_KIND_SERVER" || kind === "SERVER";
}

function normalizeOperation(kind: ResourceContractKind, value: string | undefined, spanKind?: unknown): ResourceAccessMode {
if (value === "read" || value === "write" || value === "publish" || value === "subscribe" || value === "call" || value === "serve") return value;
if (kind === "database") return value && /insert|update|delete|write|upsert|merge|truncate|drop|create|alter|replace|grant|revoke/i.test(value) ? "write" : "read";
if (kind === "queue") return value && /receive|consume|subscribe/i.test(value) ? "subscribe" : "publish";
if (kind === "http") return value && /server|serve/i.test(value) ? "serve" : "call";
if (kind === "http") {
if (value && /server|serve/i.test(value)) return "serve";
if (isServerSpanKind(spanKind)) return "serve";
return "call";
}
return value && /write/i.test(value) ? "write" : "read";
}

Expand Down Expand Up @@ -99,7 +108,14 @@ function accessFromSpan(span: SpanLike, options: OpenTelemetryEvidenceOptions):
if (!selector) return undefined;
return {
kind: inferredKind,
access: normalizeOperation(inferredKind, stringAttribute(attributes, "cellfence.resource.operation") || stringAttribute(attributes, "db.operation") || stringAttribute(attributes, "messaging.operation") || stringAttribute(attributes, "http.request.method")),
access: normalizeOperation(
inferredKind,
stringAttribute(attributes, "cellfence.resource.operation")
|| stringAttribute(attributes, "db.operation")
|| stringAttribute(attributes, "messaging.operation")
|| stringAttribute(attributes, "http.request.method"),
span.kind,
),
selector,
cellId: stringAttribute(attributes, "cellfence.cell") || stringAttribute(attributes, "cell.id") || stringAttribute(attributes, "service.name") || options.defaultCellId,
observedAt: stringAttribute(attributes, "time") || options.generatedAt,
Expand Down
50 changes: 50 additions & 0 deletions tests/official-plugins.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2465,6 +2465,56 @@ test("opentelemetry adapter ignores non-string wrapped values without throwing",
}]);
});

test("opentelemetry adapter converts HTTP SERVER spans to access: serve", () => {
const evidence = openTelemetryToResourceEvidence({
resourceSpans: [{
scopeSpans: [{
spans: [
{
name: "GET /users",
kind: 2,
attributes: [
{ key: "http.route", value: { stringValue: "/users" } },
{ key: "http.request.method", value: { stringValue: "GET" } },
],
},
{
name: "POST /orders",
kind: "SPAN_KIND_SERVER",
attributes: [
{ key: "http.route", value: { stringValue: "/orders" } },
{ key: "http.request.method", value: { stringValue: "POST" } },
],
},
{
name: "GET /client-users",
kind: 3,
attributes: [
{ key: "http.route", value: { stringValue: "/client-users" } },
{ key: "http.request.method", value: { stringValue: "GET" } },
],
},
{
name: "GET /explicit-override",
kind: 2,
attributes: [
{ key: "http.route", value: { stringValue: "/explicit-override" } },
{ key: "cellfence.resource.operation", value: { stringValue: "call" } },
],
},
],
}],
}],
}, { generatedAt: "2026-01-01T00:00:00.000Z" });

assert.deepEqual(evidence.accesses.map((access) => `${access.kind}:${access.access}:${access.selector}`), [
"http:serve:/users",
"http:serve:/orders",
"http:call:/client-users",
"http:call:/explicit-override",
]);
});

test("declarative call-pattern adapter records dynamic resource arguments as unresolved", () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "cellfence-call-pattern-dynamic-"));
try {
Expand Down
Loading