@uoa/lambda-tracing 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Class extended from OpenTelemetry B3MultiPropagator so that we can propagate & log the X-B3-Info header
3
+ *
4
+ * See {@link https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-propagator-b3/src/B3MultiPropagator.ts}
5
+ */
6
+ import {
7
+ Context,
8
+ isSpanContextValid,
9
+ trace,
10
+ TextMapGetter,
11
+ TextMapPropagator,
12
+ TextMapSetter,
13
+ TraceFlags,
14
+ createContextKey
15
+ } from '@opentelemetry/api';
16
+ import { isTracingSuppressed } from '@opentelemetry/core';
17
+ import {getRequestId} from "./tracing";
18
+
19
+
20
+ export const X_B3_TRACE_ID = 'x-b3-traceid';
21
+ export const X_B3_SPAN_ID = 'x-b3-spanid';
22
+ export const X_B3_SAMPLED = 'x-b3-sampled';
23
+ export const X_B3_PARENT_SPAN_ID = 'x-b3-parentspanid';
24
+ export const X_B3_FLAGS = 'x-b3-flags';
25
+ export const X_B3_INFO = 'x-b3-info';
26
+ export const B3_DEBUG_FLAG_KEY = createContextKey(
27
+ 'B3 Debug Flag'
28
+ );
29
+ export const B3_INFO_KEY = createContextKey(
30
+ 'B3 Info Header'
31
+ )
32
+
33
+ const VALID_SAMPLED_VALUES = new Set([true, 'true', 'True', '1', 1]);
34
+ const VALID_UNSAMPLED_VALUES = new Set([false, 'false', 'False', '0', 0]);
35
+
36
+ function parseHeader(header: unknown) {
37
+ return Array.isArray(header) ? header[0] : header;
38
+ }
39
+
40
+ function getHeaderValue(carrier: unknown, getter: TextMapGetter, key: string) {
41
+ const header = getter.get(carrier, key);
42
+ return parseHeader(header);
43
+ }
44
+
45
+ function getTraceId(carrier: unknown, getter: TextMapGetter): string {
46
+ const traceId = getHeaderValue(carrier, getter, X_B3_TRACE_ID);
47
+ if (typeof traceId === 'string') {
48
+ return traceId.padStart(32, '0');
49
+ }
50
+ return '';
51
+ }
52
+
53
+ function getSpanId(carrier: unknown, getter: TextMapGetter): string {
54
+ const spanId = getHeaderValue(carrier, getter, X_B3_SPAN_ID);
55
+ if (typeof spanId === 'string') {
56
+ return spanId;
57
+ }
58
+ return 'aaaaaaaaaaaaaaaa'; //Valid dummy value as spanId will change when AwsLambdaInstrumentation is initialised
59
+ }
60
+
61
+ function getDebug(carrier: unknown, getter: TextMapGetter): string | undefined {
62
+ const debug = getHeaderValue(carrier, getter, X_B3_FLAGS);
63
+ return debug === '1' ? '1' : undefined;
64
+ }
65
+
66
+ function getTraceFlags(
67
+ carrier: unknown,
68
+ getter: TextMapGetter
69
+ ): TraceFlags | undefined {
70
+ const traceFlags = getHeaderValue(carrier, getter, X_B3_SAMPLED);
71
+ const debug = getDebug(carrier, getter);
72
+ if (debug === '1' || VALID_SAMPLED_VALUES.has(traceFlags)) {
73
+ return TraceFlags.SAMPLED;
74
+ }
75
+ if (traceFlags === undefined || VALID_UNSAMPLED_VALUES.has(traceFlags)) {
76
+ return TraceFlags.NONE;
77
+ }
78
+ // This indicates to isValidSampledValue that this is not valid
79
+ return;
80
+ }
81
+
82
+ function getInfo(carrier: unknown, getter: TextMapGetter): string {
83
+ const info = getHeaderValue(carrier, getter, X_B3_INFO);
84
+ if (typeof info === 'string') {
85
+ return info;
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ export class UoaB3Propagator implements TextMapPropagator {
91
+ inject(context: Context, carrier: any, setter: TextMapSetter<any>): void {
92
+ const spanContext = trace.getSpanContext(context);
93
+ if (
94
+ !spanContext ||
95
+ !isSpanContextValid(spanContext) ||
96
+ isTracingSuppressed(context)
97
+ )
98
+ return;
99
+
100
+ const debug = context.getValue(B3_DEBUG_FLAG_KEY);
101
+ setter.set(carrier, X_B3_TRACE_ID, spanContext.traceId);
102
+ setter.set(carrier, X_B3_SPAN_ID, spanContext.spanId);
103
+ const info = context.getValue(B3_INFO_KEY);
104
+ if (info) {
105
+ setter.set(carrier, X_B3_INFO, info.toString());
106
+ } else if (getRequestId()) {
107
+ setter.set(carrier, X_B3_INFO, `lambdaRequestId:${getRequestId()}`);
108
+ }
109
+ // According to the B3 spec, if the debug flag is set,
110
+ // the sampled flag shouldn't be propagated as well.
111
+ if (debug === '1') {
112
+ setter.set(carrier, X_B3_FLAGS, debug);
113
+ } else if (spanContext.traceFlags !== undefined) {
114
+ // We set the header only if there is an existing sampling decision.
115
+ // Otherwise we will omit it => Absent.
116
+ setter.set(
117
+ carrier,
118
+ X_B3_SAMPLED,
119
+ (TraceFlags.SAMPLED & spanContext.traceFlags) === TraceFlags.SAMPLED
120
+ ? '1'
121
+ : '0'
122
+ );
123
+ }
124
+ }
125
+
126
+ extract(context: Context, carrier: any, getter: TextMapGetter<any>): Context {
127
+ const traceId = getTraceId(carrier, getter);
128
+ const spanId = getSpanId(carrier, getter);
129
+ const traceFlags = getTraceFlags(carrier, getter) as TraceFlags;
130
+ const debug = getDebug(carrier, getter);
131
+ const info = getInfo(carrier, getter);
132
+
133
+ context = context.setValue(B3_DEBUG_FLAG_KEY, debug);
134
+ if (info) {
135
+ context = context.setValue(B3_INFO_KEY, info);
136
+ }
137
+ return trace.setSpanContext(context, {
138
+ traceId,
139
+ spanId,
140
+ isRemote: true,
141
+ traceFlags,
142
+ });
143
+ }
144
+
145
+ fields(): string[] {
146
+ return [
147
+ X_B3_TRACE_ID,
148
+ X_B3_SPAN_ID,
149
+ X_B3_FLAGS,
150
+ X_B3_SAMPLED,
151
+ X_B3_PARENT_SPAN_ID,
152
+ X_B3_INFO
153
+ ];
154
+ }
155
+
156
+ }
package/logging.ts ADDED
@@ -0,0 +1,73 @@
1
+ import {B3_INFO_KEY} from "./UoaB3Propagator";
2
+ import {getRequestId} from "./tracing";
3
+
4
+ const winston = require('winston');
5
+ const moment = require('moment');
6
+ const path = require('path');
7
+ const {trace, context} = require('@opentelemetry/api');
8
+
9
+ const defaultLogPattern = '%date [%thread] %level %class - [[[%traceId,%spanId,%info]]] %message'
10
+ const uoaLogLevels = {
11
+ error: 0,
12
+ warn: 1,
13
+ info: 2,
14
+ debug: 3
15
+ }
16
+
17
+ const uoaLoggingFormat = function(callingModule: NodeModule) {
18
+ return winston.format.printf((logInfo: any) => {
19
+ let logPattern = process.env.loggingPattern ? process.env.loggingPattern : defaultLogPattern;
20
+
21
+ const logReplacements = getLogReplacementValues(callingModule, logInfo);
22
+ logReplacements.forEach((value, key) => {
23
+ logPattern = logPattern.replace(key, value);
24
+ });
25
+
26
+ return logPattern;
27
+ });
28
+ }
29
+
30
+ function getLogReplacementValues(callingModule: NodeModule, logInfo: any): Map<string, string> {
31
+ const parts = callingModule.filename.substring(0, callingModule.filename.lastIndexOf('.js')).split(path.sep);
32
+ const moduleParts = parts.slice(parts.indexOf('src'));
33
+
34
+ let traceId = '-';
35
+ let spanId = '-';
36
+ const currentContext = trace.getSpanContext(context.active());
37
+ if (currentContext) {
38
+ traceId = currentContext.traceId;
39
+ spanId = currentContext.spanId;
40
+ }
41
+
42
+ let info = '-';
43
+ const infoHeader = context.active().getValue(B3_INFO_KEY);
44
+ if (infoHeader) {
45
+ info = infoHeader.toString();
46
+ } else if (getRequestId()) {
47
+ info = `lambdaRequestId:${getRequestId()}`;
48
+ }
49
+
50
+ let logReplacements: Map<string, string> = new Map<string, string>();
51
+ logReplacements.set('%date', moment().toISOString());
52
+ logReplacements.set('%thread', '-');
53
+ logReplacements.set('%level', logInfo.level.toUpperCase());
54
+ logReplacements.set('%class', moduleParts.join('.'));
55
+ logReplacements.set('%traceId', traceId);
56
+ logReplacements.set('%spanId', spanId);
57
+ logReplacements.set('%info', info);
58
+ logReplacements.set('%message', logInfo.message);
59
+
60
+ return logReplacements;
61
+ }
62
+
63
+ module.exports = function (callingModule: NodeModule) {
64
+ return winston.createLogger({
65
+ levels: uoaLogLevels,
66
+ transports: [
67
+ new winston.transports.Console({
68
+ level: process.env.loggingLevel ? process.env.loggingLevel.toLowerCase() : 'info'
69
+ })
70
+ ],
71
+ format: uoaLoggingFormat(callingModule)
72
+ });
73
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@uoa/lambda-tracing",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "Library for logging & distributed tracing in UoA Lambda projects",
5
+ "main": "tracing.ts",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+ssh://git@bitbucket.org/uoa/lambda-tracing.git"
12
+ },
13
+ "author": "Mitchell Faulconbridge <mitchell.faulconbridge@auckland.ac.nz>",
14
+ "license": "MIT",
15
+ "homepage": "https://bitbucket.org/uoa/lambda-tracing#readme",
16
+ "keywords": [
17
+ "uoa",
18
+ "lambda",
19
+ "logging",
20
+ "distributed-tracing"
21
+ ],
22
+ "exports": {
23
+ ".": "./tracing.ts",
24
+ "./uoaHttps": "./uoaHttps.ts",
25
+ "./logging": "./logging.ts"
26
+ },
27
+ "dependencies": {
28
+ "@opentelemetry/api": "^1.1.0",
29
+ "@opentelemetry/core": "^1.2.0",
30
+ "@opentelemetry/instrumentation": "^0.28.0",
31
+ "@opentelemetry/instrumentation-aws-lambda": "^0.31.0",
32
+ "@opentelemetry/sdk-trace-node": "^1.2.0",
33
+ "moment": "^2.29.3",
34
+ "winston": "^3.7.2"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^17.0.35"
38
+ }
39
+ }
package/readme.md ADDED
@@ -0,0 +1,4 @@
1
+ ![npm (scoped)](https://img.shields.io/npm/v/@uoa/lambda-tracing)
2
+ # UOA Lambda Tracing Library
3
+
4
+ This library contains functions to enable distributed tracing & logging in Auckland University AWS Lambda projects.
package/tracing.ts ADDED
@@ -0,0 +1,30 @@
1
+ import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
2
+ import {AwsLambdaInstrumentation} from '@opentelemetry/instrumentation-aws-lambda';
3
+ import {registerInstrumentations} from '@opentelemetry/instrumentation';
4
+ import {UoaB3Propagator} from "./UoaB3Propagator";
5
+
6
+ const provider = new NodeTracerProvider();
7
+ provider.register({
8
+ propagator: new UoaB3Propagator()
9
+ });
10
+ let requestId: string;
11
+
12
+ registerInstrumentations({
13
+ instrumentations: [
14
+ new AwsLambdaInstrumentation({
15
+ requestHook: (span, { event, context }) => {
16
+ span.setAttribute('faas.name', context.functionName);
17
+ requestId = context.awsRequestId;
18
+ },
19
+ responseHook: (span, { err, res }) => {
20
+ if (err instanceof Error) span.setAttribute('faas.error', err.message);
21
+ if (res) span.setAttribute('faas.res', res);
22
+ },
23
+ disableAwsContextPropagation: true
24
+ })
25
+ ],
26
+ });
27
+
28
+ export function getRequestId() {
29
+ return requestId;
30
+ }
package/uoaHttps.ts ADDED
@@ -0,0 +1,34 @@
1
+ import * as https from "https";
2
+ import * as http from "http";
3
+ import { URL } from "url";
4
+ import {context, propagation} from "@opentelemetry/api";
5
+ import {RequestOptions} from "https";
6
+
7
+ function request(options: RequestOptions | string | URL, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
8
+ function request(url: string | URL, options: RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
9
+ function request(...args: any[]): http.ClientRequest {
10
+ if (args[2]) {
11
+ propagation.inject(context.active(), args[1].headers);
12
+ return https.request(args[0], args[1], args[2]);
13
+ } else {
14
+ propagation.inject(context.active(), args[0].headers);
15
+ return https.request(args[0], args[1]);
16
+ }
17
+ }
18
+
19
+ function get(options: RequestOptions | string | URL, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
20
+ function get(url: string | URL, options: RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
21
+ function get(...args: any[]): http.ClientRequest {
22
+ if (args[2]) {
23
+ propagation.inject(context.active(), args[1].headers);
24
+ return https.get(args[0], args[1], args[2]);
25
+ } else {
26
+ propagation.inject(context.active(), args[0].headers);
27
+ return https.get(args[0], args[1]);
28
+ }
29
+ }
30
+
31
+ module.exports = {
32
+ request,
33
+ get
34
+ }