Skip to content

Commit

Permalink
feat: add OpenTelemetry support
Browse files Browse the repository at this point in the history
  • Loading branch information
tschoffelen committed Dec 23, 2024
1 parent 239db11 commit e2e4da3
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 29 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,9 @@
"lambda-sample-events": "^1.0.1",
"prettier": "^3.2.5",
"semantic-release": "^23.0.8"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.28.0"
}
}
9 changes: 8 additions & 1 deletion src/lib/ApiAdapter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Response = require("./Response");
const Request = require("./Request");
const OpenTelemetry = require("./OpenTelemetry");

class ApiAdapter {
constructor(app, policies, errorConverter) {
Expand All @@ -15,6 +16,8 @@ class ApiAdapter {
return "Lambda is warm";
}

OpenTelemetry.addSpanRequestAttributes(event);

try {
let input = event;

Expand Down Expand Up @@ -60,9 +63,13 @@ class ApiAdapter {
output = output.body;
}

return new Response(output, this.statusCode, this.additionalHeaders);
const res = new Response(output, this.statusCode, this.additionalHeaders);
OpenTelemetry.addSpanResponseAttributes(event, res);

return res;
} catch (error) {
console.error(error);
OpenTelemetry.addSpanErrorAttributes(event, error);
return this.errorConverter.convert(error);
}
}
Expand Down
171 changes: 171 additions & 0 deletions src/lib/OpenTelemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const { trace } = require("@opentelemetry/api");
const {
ATTR_HTTP_ROUTE,
ATTR_URL_FULL,
ATTR_USER_AGENT_ORIGINAL,
ATTR_HTTP_REQUEST_METHOD,
ATTR_NETWORK_PROTOCOL_NAME,
ATTR_NETWORK_PROTOCOL_VERSION,
} = require("@opentelemetry/semantic-conventions");
const {
ATTR_HTTP_USER_AGENT,
ATTR_HTTP_FLAVOR,
} = require("@opentelemetry/semantic-conventions/incubating");

const isApiGwEvent = (event) => {
return (
event?.requestContext?.domainName != null &&
event?.requestContext?.requestId != null
);
};

const isSnsEvent = (event) => {
return event?.Records?.[0]?.EventSource === "aws:sns";
};

const isSqsEvent = (event) => {
return event?.Records?.[0]?.eventSource === "aws:sqs";
};

const isS3Event = (event) => {
return event?.Records?.[0]?.eventSource === "aws:s3";
};

const isDDBEvent = (event) => {
return event?.Records?.[0]?.eventSource === "aws:dynamodb";
};

const isCloudfrontEvent = (event) => {
return event?.Records?.[0]?.cf?.config?.distributionId != null;
};

const getFullUrl = (event) => {
if (!event.headers) return undefined;
function findAny(event, key1, key2) {
return event.headers[key1] ?? event.headers[key2];
}
const host = findAny(event, "host", "Host");
const proto = findAny(event, "x-forwarded-proto", "X-Forwarded-Proto");
const port = findAny(event, "x-forwarded-port", "X-Forwarded-Port");
if (!(proto && host && (event.path || event.rawPath))) {
return undefined;
}
let answer = proto + "://" + host;
if (port) {
answer += ":" + port;
}
answer += event.path ?? event.rawPath;
if (event.queryStringParameters) {
let first = true;
for (const key in event.queryStringParameters) {
answer += first ? "?" : "&";
answer += encodeURIComponent(key);
answer += "=";
answer += encodeURIComponent(event.queryStringParameters[key]);
first = false;
}
}
return answer;
};

class OpenTelemetry {
static _getSpan() {
return trace.getActiveSpan();
}

static setSpanAttribute(key, value) {
const span = this._getSpan();
if (span) span.setAttribute(key, value);
}

static addSpanRequestAttributes(event) {
try {
const span = this._getSpan();
if (!span) return;

if (isApiGwEvent(event)) {
const fullUrl = getFullUrl(event);
span.setAttribute(ATTR_HTTP_ROUTE, event.routeKey?.split(" ")[1]);
fullUrl && span.setAttribute(ATTR_URL_FULL, fullUrl);
span.setAttribute(
ATTR_HTTP_REQUEST_METHOD,
event.requestContext?.http?.method,
);
span.setAttribute(
ATTR_USER_AGENT_ORIGINAL,
event.requestContext?.http?.userAgent,
);
span.setAttribute(ATTR_NETWORK_PROTOCOL_NAME, "http");
span.setAttribute(
ATTR_NETWORK_PROTOCOL_VERSION,
event.requestContext?.http?.protocol?.split("/")?.[1],
);
span.setAttribute("http.request.id", event.requestContext?.requestId);
span.setAttribute(
"http.request.header.content-type",
event.headers?.["content-type"],
);
span.setAttribute("http.request.body_size", event.body?.length || 0);
span.setAttribute("url.path", event.rawPath);
span.setAttribute("url.query", event.rawQueryString);
if (event.requestContext?.authorizer?.jwt?.claims) {
const { claims } = event.requestContext.authorizer.jwt;
span.setAttribute("user.id", claims.sub || claims.id);
span.setAttribute("user.auth_method", "jwt");
span.setAttribute(
"user.role",
claims.role || claims["cognito:groups"],
);
if (claims.event_id?.includes("Parent=")) {
const parentTraceId = claims.event_id
.split("Parent=")?.[1]
?.split(";")?.[0];
span.setAttribute("user.auth_parent_trace_id", parentTraceId);
}
}
}

// TODO: deal with other event types (S3, SQS, SNS, etc.)
} catch (e) {
console.debug("Error in addSpanRequestAttributes", e);
}
}

static addSpanResponseAttributes(event, response) {
try {
const span = this._getSpan();
if (!span) return;

if (isApiGwEvent(event)) {
span.setAttribute("http.response.status_code", response.statusCode);
span.setAttribute(
"http.response.header.content-type",
response.headers?.["content-type"] ||
response.headers?.["Content-Type"],
);
span.setAttribute(
"http.response.body_size",
response.body?.length || 0,
);
}
} catch (e) {
console.debug("Error in addSpanResponseAttributes", e);
}
}

static addSpanErrorAttributes(event, error) {
try {
const span = this._getSpan();
if (!span) return;

span.setAttribute("error", true);
span.setAttribute("exception.message", error.message);
span.setAttribute("exception.type", error.name);
span.setAttribute("exception.stacktrace", error.stack);
} catch (e) {
console.debug("Error in addSpanErrorAttributes", e);
}
}
}

module.exports = OpenTelemetry;
41 changes: 13 additions & 28 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,16 @@
dependencies:
"@octokit/openapi-types" "^22.2.0"

"@opentelemetry/api@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==

"@opentelemetry/semantic-conventions@^1.28.0":
version "1.28.0"
resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6"
integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==

"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
Expand Down Expand Up @@ -4947,7 +4957,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"

"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand All @@ -4965,15 +4975,6 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"

string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
Expand Down Expand Up @@ -5018,7 +5019,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand All @@ -5032,13 +5033,6 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"

strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
Expand Down Expand Up @@ -5475,16 +5469,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand Down

0 comments on commit e2e4da3

Please sign in to comment.