@contrail/telemetry 1.1.0-alpha-frontend-logging-2 → 1.1.0-alpha-refactor-telemetry-2
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.
- package/CHANGELOG.md +8 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/logger/flatten-object.d.ts +4 -0
- package/lib/logger/flatten-object.js +60 -0
- package/lib/logger/index.d.ts +18 -2
- package/lib/logger/index.js +258 -13
- package/lib/logger/logger-config.d.ts +3 -0
- package/lib/logger/logger-config.js +20 -0
- package/lib/logger/parse-otel-resource-attributes.d.ts +8 -0
- package/lib/logger/parse-otel-resource-attributes.js +27 -0
- package/lib/logger/semantic-conventions.d.ts +11 -0
- package/lib/logger/semantic-conventions.js +14 -0
- package/lib/semantic-conventions.d.ts +32 -0
- package/lib/semantic-conventions.js +41 -0
- package/package.json +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,10 +7,16 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.2] - 2026-03-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Shared logger for libraries** — Any package can now `import { logger } from '@contrail/telemetry'` and get structured logging with request context, automatically enhanced when running inside a host service.
|
|
15
|
+
- **Browser safety** — Safe to import in browser environments (no `process is not defined` errors).
|
|
16
|
+
|
|
10
17
|
### Changed
|
|
11
18
|
|
|
12
|
-
-
|
|
13
|
-
- Libraries that previously had no logging can now `import { logger } from '@contrail/telemetry'` regardless of runtime environment.
|
|
19
|
+
- Logger singleton now survives npm module duplication — no `overrides` needed for `@contrail/telemetry`.
|
|
14
20
|
|
|
15
21
|
## [1.0.0] - (initial changelog entry)
|
|
16
22
|
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flattenObject = flattenObject;
|
|
4
|
+
const MAX_DEPTH = 10;
|
|
5
|
+
function flattenObject(obj) {
|
|
6
|
+
const seen = new WeakSet();
|
|
7
|
+
return flattenRecursive(obj, '', seen, 0);
|
|
8
|
+
}
|
|
9
|
+
function flattenRecursive(obj, prefix, ancestors, depth) {
|
|
10
|
+
const result = {};
|
|
11
|
+
if (depth > MAX_DEPTH) {
|
|
12
|
+
result[prefix || 'value'] = '[max depth exceeded]';
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
ancestors.add(obj);
|
|
16
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
17
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
18
|
+
if (value === null || value === undefined) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'object') {
|
|
22
|
+
if (ancestors.has(value)) {
|
|
23
|
+
result[fullKey] = '[circular reference]';
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
result[fullKey] = JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
else if (value instanceof Date) {
|
|
30
|
+
result[fullKey] = value.toISOString();
|
|
31
|
+
}
|
|
32
|
+
else if (value instanceof Error) {
|
|
33
|
+
result[fullKey] = value.message;
|
|
34
|
+
if (value.stack) {
|
|
35
|
+
result[`${fullKey}.stack`] = value.stack;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
Object.assign(result, flattenRecursive(value, fullKey, ancestors, depth + 1));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
43
|
+
result[fullKey] = value;
|
|
44
|
+
}
|
|
45
|
+
else if (typeof value === 'bigint') {
|
|
46
|
+
result[fullKey] = value.toString();
|
|
47
|
+
}
|
|
48
|
+
else if (typeof value === 'symbol') {
|
|
49
|
+
result[fullKey] = value.toString();
|
|
50
|
+
}
|
|
51
|
+
else if (typeof value === 'function') {
|
|
52
|
+
result[fullKey] = '[function]';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
result[fullKey] = String(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
ancestors.delete(obj);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
package/lib/logger/index.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
1
|
import pino from 'pino';
|
|
2
|
-
export
|
|
3
|
-
export
|
|
2
|
+
export { parseOtelResourceAttributes } from './parse-otel-resource-attributes';
|
|
3
|
+
export { PINO_LEVEL_TO_OTEL_SEVERITY, PINO_LEVEL_TO_NAME } from './logger-config';
|
|
4
|
+
export { ATTR_LOG_MESSAGE, ATTR_LOG_PAYLOAD } from './semantic-conventions';
|
|
5
|
+
export declare const baseLogger: pino.Logger<never, boolean>;
|
|
6
|
+
export declare const logger: pino.Logger<never, boolean>;
|
|
7
|
+
/**
|
|
8
|
+
* Add a stream destination to the logger's multistream.
|
|
9
|
+
* Use this to wire additional log sinks (e.g. InMemoryLogStream in app-framework).
|
|
10
|
+
*/
|
|
11
|
+
export declare function addStream(streamEntry: pino.StreamEntry): void;
|
|
12
|
+
/**
|
|
13
|
+
* Flushes all pending logs to the OTLP collector.
|
|
14
|
+
* Call this at the end of Lambda handlers to ensure logs are delivered
|
|
15
|
+
* before the execution environment freezes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function flushLogs(options?: {
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}): Promise<void>;
|
package/lib/logger/index.js
CHANGED
|
@@ -2,20 +2,265 @@
|
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
|
-
var _a;
|
|
5
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.logger = void 0;
|
|
8
|
-
exports.
|
|
7
|
+
exports.logger = exports.baseLogger = exports.ATTR_LOG_PAYLOAD = exports.ATTR_LOG_MESSAGE = exports.PINO_LEVEL_TO_NAME = exports.PINO_LEVEL_TO_OTEL_SEVERITY = exports.parseOtelResourceAttributes = void 0;
|
|
8
|
+
exports.addStream = addStream;
|
|
9
|
+
exports.flushLogs = flushLogs;
|
|
9
10
|
const pino_1 = __importDefault(require("pino"));
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
12
|
+
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
13
|
+
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
14
|
+
const core_1 = require("@opentelemetry/core");
|
|
15
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
16
|
+
const api_1 = require("@opentelemetry/api");
|
|
17
|
+
const stream_1 = require("stream");
|
|
18
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
19
|
+
const flatten_object_1 = require("./flatten-object");
|
|
20
|
+
const logger_config_1 = require("./logger-config");
|
|
21
|
+
const parse_otel_resource_attributes_1 = require("./parse-otel-resource-attributes");
|
|
22
|
+
const semantic_conventions_2 = require("./semantic-conventions");
|
|
23
|
+
const serverless_express_1 = require("@codegenie/serverless-express");
|
|
24
|
+
var parse_otel_resource_attributes_2 = require("./parse-otel-resource-attributes");
|
|
25
|
+
Object.defineProperty(exports, "parseOtelResourceAttributes", { enumerable: true, get: function () { return parse_otel_resource_attributes_2.parseOtelResourceAttributes; } });
|
|
26
|
+
var logger_config_2 = require("./logger-config");
|
|
27
|
+
Object.defineProperty(exports, "PINO_LEVEL_TO_OTEL_SEVERITY", { enumerable: true, get: function () { return logger_config_2.PINO_LEVEL_TO_OTEL_SEVERITY; } });
|
|
28
|
+
Object.defineProperty(exports, "PINO_LEVEL_TO_NAME", { enumerable: true, get: function () { return logger_config_2.PINO_LEVEL_TO_NAME; } });
|
|
29
|
+
var semantic_conventions_3 = require("./semantic-conventions");
|
|
30
|
+
Object.defineProperty(exports, "ATTR_LOG_MESSAGE", { enumerable: true, get: function () { return semantic_conventions_3.ATTR_LOG_MESSAGE; } });
|
|
31
|
+
Object.defineProperty(exports, "ATTR_LOG_PAYLOAD", { enumerable: true, get: function () { return semantic_conventions_3.ATTR_LOG_PAYLOAD; } });
|
|
32
|
+
// --- Environment & Resource Setup ---
|
|
33
|
+
const semantic_conventions_4 = require("../semantic-conventions");
|
|
34
|
+
const LAMBDA_ENV_OTEL_ATTRIBUTES = getLambdaEnvAttributes();
|
|
35
|
+
const isLocalEnvironment = process.env.LOCAL === 'true';
|
|
36
|
+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
37
|
+
const shouldSendToOtlpCollector = Boolean(otlpEndpoint);
|
|
38
|
+
const resourceAttributes = (0, parse_otel_resource_attributes_1.parseOtelResourceAttributes)();
|
|
39
|
+
const resource = (0, resources_1.resourceFromAttributes)(resourceAttributes);
|
|
40
|
+
const serviceName = (_b = (_a = resourceAttributes[semantic_conventions_1.ATTR_SERVICE_NAME]) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'unknown-service';
|
|
41
|
+
const deploymentEnvironment = (_d = (_c = resourceAttributes[semantic_conventions_4.ATTR_DEPLOYMENT_ENVIRONMENT_NAME]) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : 'local';
|
|
42
|
+
const serviceVersion = (_f = (_e = resourceAttributes[semantic_conventions_1.ATTR_SERVICE_VERSION]) === null || _e === void 0 ? void 0 : _e.toString()) !== null && _f !== void 0 ? _f : 'unknown';
|
|
43
|
+
// --- OTel Log Provider ---
|
|
44
|
+
const otelLoggerProvider = new sdk_logs_1.LoggerProvider({
|
|
45
|
+
resource,
|
|
46
|
+
processors: shouldSendToOtlpCollector ? [new sdk_logs_1.BatchLogRecordProcessor(new exporter_logs_otlp_http_1.OTLPLogExporter())] : [],
|
|
47
|
+
});
|
|
48
|
+
// Prevent OTel-internal errors (e.g. "Concurrent export limit reached") from routing back
|
|
49
|
+
// through console.error → pino → OTel, which creates an unbounded async feedback loop.
|
|
50
|
+
// Writing directly to process.stderr bypasses the hijacked console entirely.
|
|
51
|
+
(0, core_1.setGlobalErrorHandler)((error) => {
|
|
52
|
+
var _a;
|
|
53
|
+
process.stderr.write(`[OTel Error] ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : String(error)}\n`);
|
|
18
54
|
});
|
|
19
|
-
|
|
20
|
-
|
|
55
|
+
const otelLogger = otelLoggerProvider.getLogger(serviceName);
|
|
56
|
+
// --- Pino Stream Destinations ---
|
|
57
|
+
function isPinoPrettyInstalled() {
|
|
58
|
+
try {
|
|
59
|
+
require.resolve('pino-pretty');
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function createStdoutStream() {
|
|
67
|
+
const shouldUsePrettyPrinting = isLocalEnvironment && isPinoPrettyInstalled();
|
|
68
|
+
if (shouldUsePrettyPrinting) {
|
|
69
|
+
// Use pino-pretty as a synchronous stream rather than a worker thread transport.
|
|
70
|
+
// pino.transport() hijacks the process stdout fd, which silently swallows
|
|
71
|
+
// console.log and output from other Pino instances.
|
|
72
|
+
const PinoPretty = require('pino-pretty');
|
|
73
|
+
return {
|
|
74
|
+
level: 'trace',
|
|
75
|
+
stream: PinoPretty({ colorize: true, sync: true }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
level: 'trace',
|
|
80
|
+
stream: new stream_1.Writable({
|
|
81
|
+
write(chunk, _encoding, callback) {
|
|
82
|
+
var _a, _b, _c, _d;
|
|
83
|
+
let line;
|
|
84
|
+
try {
|
|
85
|
+
const logRecord = JSON.parse(chunk.toString());
|
|
86
|
+
const time = (_a = logRecord.time) !== null && _a !== void 0 ? _a : new Date().toISOString();
|
|
87
|
+
const level = (_b = logger_config_1.PINO_LEVEL_TO_NAME[logRecord.level]) !== null && _b !== void 0 ? _b : 'INFO';
|
|
88
|
+
const message = (_c = logRecord[semantic_conventions_2.ATTR_LOG_MESSAGE]) !== null && _c !== void 0 ? _c : '';
|
|
89
|
+
const requestId = (_d = getLambdaRequestId()) !== null && _d !== void 0 ? _d : '-';
|
|
90
|
+
const payload = logRecord[semantic_conventions_2.ATTR_LOG_PAYLOAD];
|
|
91
|
+
const payloadLine = payload && typeof payload === 'object' && Object.keys(payload).length > 0
|
|
92
|
+
? `${JSON.stringify(payload, null, 2)}\n`
|
|
93
|
+
: '';
|
|
94
|
+
line = `${time}\t${requestId}\t${level}\t${message}\n${payloadLine}`;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
line = chunk.toString();
|
|
98
|
+
}
|
|
99
|
+
process.stdout.write(line);
|
|
100
|
+
callback();
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function createOtelStream() {
|
|
106
|
+
return new stream_1.Writable({
|
|
107
|
+
write(chunk, _encoding, callback) {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
try {
|
|
110
|
+
const logRecord = JSON.parse(chunk.toString());
|
|
111
|
+
const { level, ...attributes } = logRecord;
|
|
112
|
+
const activeSpan = api_1.trace.getSpan(api_1.context.active());
|
|
113
|
+
const spanContext = activeSpan === null || activeSpan === void 0 ? void 0 : activeSpan.spanContext();
|
|
114
|
+
const spanAttributes = getSpanAttributes(activeSpan);
|
|
115
|
+
const lambdaEnvAttributes = LAMBDA_ENV_OTEL_ATTRIBUTES;
|
|
116
|
+
let safeAttributes;
|
|
117
|
+
try {
|
|
118
|
+
safeAttributes = (0, flatten_object_1.flattenObject)(attributes);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Fallback in case flattening fails somehow. This is not expected.
|
|
122
|
+
// If this happens, we will detect it by seeing structured objects
|
|
123
|
+
// in our observability tooling instead of flat keys.
|
|
124
|
+
safeAttributes = attributes;
|
|
125
|
+
}
|
|
126
|
+
otelLogger.emit({
|
|
127
|
+
severityNumber: (_a = logger_config_1.PINO_LEVEL_TO_OTEL_SEVERITY[level]) !== null && _a !== void 0 ? _a : api_logs_1.SeverityNumber.INFO,
|
|
128
|
+
severityText: (_b = logger_config_1.PINO_LEVEL_TO_NAME[level]) !== null && _b !== void 0 ? _b : 'INFO',
|
|
129
|
+
body: attributes[semantic_conventions_2.ATTR_LOG_MESSAGE],
|
|
130
|
+
// Attribute precedence (last-spread-wins):
|
|
131
|
+
// 1. safeAttributes — flattened user/log-context attributes (lowest priority)
|
|
132
|
+
// 2. lambdaEnvAttributes — static Lambda env attributes (override user attrs)
|
|
133
|
+
// 3. spanAttributes — per-invocation span attributes like faas.*, cloud.*, aws.* (highest priority)
|
|
134
|
+
// This is intentional: system-level attributes should always reflect the true runtime
|
|
135
|
+
// environment, even if user code accidentally sets a conflicting key.
|
|
136
|
+
attributes: {
|
|
137
|
+
...safeAttributes,
|
|
138
|
+
...lambdaEnvAttributes,
|
|
139
|
+
...spanAttributes,
|
|
140
|
+
},
|
|
141
|
+
...(spanContext && {
|
|
142
|
+
traceId: spanContext.traceId,
|
|
143
|
+
spanId: spanContext.spanId,
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
otelLogger.emit({
|
|
149
|
+
severityNumber: api_logs_1.SeverityNumber.ERROR,
|
|
150
|
+
severityText: 'ERROR',
|
|
151
|
+
body: 'Failed to parse log record',
|
|
152
|
+
attributes: {
|
|
153
|
+
'log.raw': chunk.toString(),
|
|
154
|
+
'error.message': error instanceof Error ? error.message : String(error),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
callback();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function buildPinoStreams() {
|
|
163
|
+
const streams = [createStdoutStream()];
|
|
164
|
+
const shouldSendLogsToOtel = !isLocalEnvironment || otlpEndpoint;
|
|
165
|
+
if (shouldSendLogsToOtel) {
|
|
166
|
+
streams.push({ level: 'trace', stream: createOtelStream() });
|
|
167
|
+
}
|
|
168
|
+
return streams;
|
|
169
|
+
}
|
|
170
|
+
// --- Lambda Environment Attributes ---
|
|
171
|
+
// Static attributes from Lambda environment variables (don't change per-invocation)
|
|
172
|
+
function getLambdaEnvAttributes() {
|
|
173
|
+
const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
|
|
174
|
+
if (!functionName)
|
|
175
|
+
return {};
|
|
176
|
+
const attributes = {
|
|
177
|
+
'faas.name': functionName,
|
|
178
|
+
'cloud.provider': 'aws',
|
|
179
|
+
};
|
|
180
|
+
if (process.env.AWS_LAMBDA_FUNCTION_VERSION) {
|
|
181
|
+
attributes['faas.version'] = process.env.AWS_LAMBDA_FUNCTION_VERSION;
|
|
182
|
+
}
|
|
183
|
+
if (process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE) {
|
|
184
|
+
attributes['faas.max_memory'] = parseInt(process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, 10) * 1024 * 1024;
|
|
185
|
+
}
|
|
186
|
+
if (process.env.AWS_REGION) {
|
|
187
|
+
attributes['cloud.region'] = process.env.AWS_REGION;
|
|
188
|
+
}
|
|
189
|
+
if (process.env.AWS_LAMBDA_LOG_STREAM_NAME) {
|
|
190
|
+
attributes['faas.instance'] = process.env.AWS_LAMBDA_LOG_STREAM_NAME;
|
|
191
|
+
}
|
|
192
|
+
return attributes;
|
|
193
|
+
}
|
|
194
|
+
// --- Span Attributes ---
|
|
195
|
+
function isReadableSpan(span) {
|
|
196
|
+
return 'attributes' in span && typeof span.attributes === 'object';
|
|
197
|
+
}
|
|
198
|
+
function getSpanAttributes(span) {
|
|
199
|
+
if (!span || !span.isRecording())
|
|
200
|
+
return {};
|
|
201
|
+
if (!isReadableSpan(span))
|
|
202
|
+
return {};
|
|
203
|
+
const relevantPrefixes = ['faas.', 'cloud.', 'aws.'];
|
|
204
|
+
const result = {};
|
|
205
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
206
|
+
if (value !== undefined && relevantPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
207
|
+
result[key] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
function getLambdaRequestId() {
|
|
213
|
+
try {
|
|
214
|
+
const { context: lambdaContext } = (0, serverless_express_1.getCurrentInvoke)();
|
|
215
|
+
return lambdaContext === null || lambdaContext === void 0 ? void 0 : lambdaContext.awsRequestId;
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// --- Base Pino Logger ---
|
|
222
|
+
exports.baseLogger = (0, pino_1.default)({
|
|
223
|
+
level: (_g = process.env.LOG_LEVEL) !== null && _g !== void 0 ? _g : 'debug',
|
|
224
|
+
messageKey: semantic_conventions_2.ATTR_LOG_MESSAGE,
|
|
225
|
+
timestamp: pino_1.default.stdTimeFunctions.isoTime,
|
|
226
|
+
nestedKey: semantic_conventions_2.ATTR_LOG_PAYLOAD,
|
|
227
|
+
base: {
|
|
228
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: serviceName,
|
|
229
|
+
[semantic_conventions_4.ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: deploymentEnvironment,
|
|
230
|
+
[semantic_conventions_1.ATTR_SERVICE_VERSION]: serviceVersion,
|
|
231
|
+
},
|
|
232
|
+
}, pino_1.default.multistream(buildPinoStreams()));
|
|
233
|
+
exports.logger = exports.baseLogger;
|
|
234
|
+
// --- Public API ---
|
|
235
|
+
/**
|
|
236
|
+
* Add a stream destination to the logger's multistream.
|
|
237
|
+
* Use this to wire additional log sinks (e.g. InMemoryLogStream in app-framework).
|
|
238
|
+
*/
|
|
239
|
+
function addStream(streamEntry) {
|
|
240
|
+
const ms = exports.baseLogger.stream;
|
|
241
|
+
if (ms && typeof ms.add === 'function') {
|
|
242
|
+
ms.add(streamEntry);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Flushes all pending logs to the OTLP collector.
|
|
247
|
+
* Call this at the end of Lambda handlers to ensure logs are delivered
|
|
248
|
+
* before the execution environment freezes.
|
|
249
|
+
*/
|
|
250
|
+
async function flushLogs(options) {
|
|
251
|
+
var _a;
|
|
252
|
+
const timeoutMs = (_a = options === null || options === void 0 ? void 0 : options.timeoutMs) !== null && _a !== void 0 ? _a : 200;
|
|
253
|
+
try {
|
|
254
|
+
await Promise.race([
|
|
255
|
+
otelLoggerProvider.forceFlush(),
|
|
256
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Log flush timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
console.error('[OTel Log Flush Failed]', JSON.stringify({
|
|
261
|
+
error: error instanceof Error ? error.message : String(error),
|
|
262
|
+
timeoutMs,
|
|
263
|
+
timestamp: new Date().toISOString(),
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
21
266
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PINO_LEVEL_TO_NAME = exports.PINO_LEVEL_TO_OTEL_SEVERITY = void 0;
|
|
4
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
5
|
+
exports.PINO_LEVEL_TO_OTEL_SEVERITY = {
|
|
6
|
+
10: api_logs_1.SeverityNumber.TRACE,
|
|
7
|
+
20: api_logs_1.SeverityNumber.DEBUG,
|
|
8
|
+
30: api_logs_1.SeverityNumber.INFO,
|
|
9
|
+
40: api_logs_1.SeverityNumber.WARN,
|
|
10
|
+
50: api_logs_1.SeverityNumber.ERROR,
|
|
11
|
+
60: api_logs_1.SeverityNumber.FATAL,
|
|
12
|
+
};
|
|
13
|
+
exports.PINO_LEVEL_TO_NAME = {
|
|
14
|
+
10: 'TRACE',
|
|
15
|
+
20: 'DEBUG',
|
|
16
|
+
30: 'INFO',
|
|
17
|
+
40: 'WARN',
|
|
18
|
+
50: 'ERROR',
|
|
19
|
+
60: 'FATAL',
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DetectedResourceAttributes } from '@opentelemetry/resources';
|
|
2
|
+
/**
|
|
3
|
+
* Parse OTEL_RESOURCE_ATTRIBUTES environment variable manually since detectResources is async.
|
|
4
|
+
* Format: key1=value1,key2=value2
|
|
5
|
+
*
|
|
6
|
+
* Also checks OTEL_SERVICE_NAME which can be set separately.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseOtelResourceAttributes(resourceAttributesEnv?: string | undefined, serviceNameEnv?: string | undefined): DetectedResourceAttributes;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseOtelResourceAttributes = parseOtelResourceAttributes;
|
|
4
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
5
|
+
/**
|
|
6
|
+
* Parse OTEL_RESOURCE_ATTRIBUTES environment variable manually since detectResources is async.
|
|
7
|
+
* Format: key1=value1,key2=value2
|
|
8
|
+
*
|
|
9
|
+
* Also checks OTEL_SERVICE_NAME which can be set separately.
|
|
10
|
+
*/
|
|
11
|
+
function parseOtelResourceAttributes(resourceAttributesEnv = process.env.OTEL_RESOURCE_ATTRIBUTES, serviceNameEnv = process.env.OTEL_SERVICE_NAME) {
|
|
12
|
+
const rawAttributes = resourceAttributesEnv || '';
|
|
13
|
+
const attributes = {};
|
|
14
|
+
if (rawAttributes) {
|
|
15
|
+
rawAttributes.split(',').forEach((pair) => {
|
|
16
|
+
const [key, ...valueParts] = pair.split('=');
|
|
17
|
+
if (key && valueParts.length > 0) {
|
|
18
|
+
attributes[key.trim()] = valueParts.join('=').trim();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Also check OTEL_SERVICE_NAME which can be set separately
|
|
23
|
+
if (serviceNameEnv) {
|
|
24
|
+
attributes[semantic_conventions_1.ATTR_SERVICE_NAME] = serviceNameEnv;
|
|
25
|
+
}
|
|
26
|
+
return attributes;
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log-specific semantic conventions.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Attribute key for the log message body.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ATTR_LOG_MESSAGE: "contrail.message";
|
|
8
|
+
/**
|
|
9
|
+
* Attribute key for user-provided log payload objects.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ATTR_LOG_PAYLOAD: "contrail.payload";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Log-specific semantic conventions.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ATTR_LOG_PAYLOAD = exports.ATTR_LOG_MESSAGE = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Attribute key for the log message body.
|
|
9
|
+
*/
|
|
10
|
+
exports.ATTR_LOG_MESSAGE = 'contrail.message';
|
|
11
|
+
/**
|
|
12
|
+
* Attribute key for user-provided log payload objects.
|
|
13
|
+
*/
|
|
14
|
+
exports.ATTR_LOG_PAYLOAD = 'contrail.payload';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic conventions for Contrail telemetry (logging + tracing).
|
|
3
|
+
*
|
|
4
|
+
* For attributes that ARE in the OpenTelemetry SDK (e.g. ATTR_CLIENT_ADDRESS, ATTR_HTTP_ROUTE),
|
|
5
|
+
* import directly from `@opentelemetry/semantic-conventions`.
|
|
6
|
+
*
|
|
7
|
+
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @see https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/
|
|
11
|
+
*/
|
|
12
|
+
export declare const ATTR_USER_ID: "user.id";
|
|
13
|
+
/**
|
|
14
|
+
* @see https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/
|
|
15
|
+
*/
|
|
16
|
+
export declare const ATTR_USER_EMAIL: "user.email";
|
|
17
|
+
/**
|
|
18
|
+
* @see https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/
|
|
19
|
+
*/
|
|
20
|
+
export declare const ATTR_USER_ROLES: "user.roles";
|
|
21
|
+
/**
|
|
22
|
+
* @see https://opentelemetry.io/docs/specs/semconv/resource/deployment-environment/
|
|
23
|
+
*/
|
|
24
|
+
export declare const ATTR_DEPLOYMENT_ENVIRONMENT_NAME: "deployment.environment.name";
|
|
25
|
+
/**
|
|
26
|
+
* Namespace for Contrail platform context.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ATTR_CONTRAIL_SYSTEM: "contrail.system";
|
|
29
|
+
/**
|
|
30
|
+
* Namespace for Contrail-custom user attributes (not in OTel spec).
|
|
31
|
+
*/
|
|
32
|
+
export declare const ATTR_CONTRAIL_USER: "contrail.user";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Semantic conventions for Contrail telemetry (logging + tracing).
|
|
4
|
+
*
|
|
5
|
+
* For attributes that ARE in the OpenTelemetry SDK (e.g. ATTR_CLIENT_ADDRESS, ATTR_HTTP_ROUTE),
|
|
6
|
+
* import directly from `@opentelemetry/semantic-conventions`.
|
|
7
|
+
*
|
|
8
|
+
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ATTR_CONTRAIL_USER = exports.ATTR_CONTRAIL_SYSTEM = exports.ATTR_DEPLOYMENT_ENVIRONMENT_NAME = exports.ATTR_USER_ROLES = exports.ATTR_USER_EMAIL = exports.ATTR_USER_ID = void 0;
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// OTel-spec attributes not yet in the SDK
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* @see https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/
|
|
17
|
+
*/
|
|
18
|
+
exports.ATTR_USER_ID = 'user.id';
|
|
19
|
+
/**
|
|
20
|
+
* @see https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/
|
|
21
|
+
*/
|
|
22
|
+
exports.ATTR_USER_EMAIL = 'user.email';
|
|
23
|
+
/**
|
|
24
|
+
* @see https://opentelemetry.io/docs/specs/semconv/attributes-registry/user/
|
|
25
|
+
*/
|
|
26
|
+
exports.ATTR_USER_ROLES = 'user.roles';
|
|
27
|
+
/**
|
|
28
|
+
* @see https://opentelemetry.io/docs/specs/semconv/resource/deployment-environment/
|
|
29
|
+
*/
|
|
30
|
+
exports.ATTR_DEPLOYMENT_ENVIRONMENT_NAME = 'deployment.environment.name';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Contrail-custom attributes
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Namespace for Contrail platform context.
|
|
36
|
+
*/
|
|
37
|
+
exports.ATTR_CONTRAIL_SYSTEM = 'contrail.system';
|
|
38
|
+
/**
|
|
39
|
+
* Namespace for Contrail-custom user attributes (not in OTel spec).
|
|
40
|
+
*/
|
|
41
|
+
exports.ATTR_CONTRAIL_USER = 'contrail.user';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrail/telemetry",
|
|
3
|
-
"version": "1.1.0-alpha-
|
|
3
|
+
"version": "1.1.0-alpha-refactor-telemetry-2",
|
|
4
4
|
"description": "Telemetry and monitoring utilities for contrail services",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -14,9 +14,27 @@
|
|
|
14
14
|
"author": "",
|
|
15
15
|
"license": "ISC",
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@codegenie/serverless-express": "^4.16.0",
|
|
18
|
+
"@opentelemetry/api": "^1.9.0",
|
|
19
|
+
"@opentelemetry/api-logs": "^0.211.0",
|
|
20
|
+
"@opentelemetry/core": "^2.5.0",
|
|
21
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.211.0",
|
|
22
|
+
"@opentelemetry/resources": "^2.5.0",
|
|
23
|
+
"@opentelemetry/sdk-logs": "^0.211.0",
|
|
24
|
+
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
|
25
|
+
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
17
26
|
"pino": "^10.1.0"
|
|
18
27
|
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"pino-pretty": "^13.1.3"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"pino-pretty": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
19
36
|
"devDependencies": {
|
|
37
|
+
"@types/aws-lambda": "^8.10.161",
|
|
20
38
|
"@types/jest": "^29.5.2",
|
|
21
39
|
"@types/node": "^20.0.0",
|
|
22
40
|
"jest": "^29.5.0",
|