@momentumcms/plugins-otel 0.0.1 → 0.1.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 +12 -0
- package/index.js +327 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.1.2 (2026-02-16)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- **release:** centralize manifestRootsToUpdate to update both source and dist ([2b8f832](https://github.com/DonaldMurillo/momentum-cms/commit/2b8f832))
|
|
6
|
+
- **create-app:** fix Angular SSR, Analog builds, and CJS/ESM compatibility ([28d4d0a](https://github.com/DonaldMurillo/momentum-cms/commit/28d4d0a))
|
|
7
|
+
|
|
8
|
+
### ❤️ Thank You
|
|
9
|
+
|
|
10
|
+
- Claude Opus 4.6
|
|
11
|
+
- Donald Murillo @DonaldMurillo
|
|
12
|
+
|
|
1
13
|
## 0.1.1 (2026-02-16)
|
|
2
14
|
|
|
3
15
|
This was a version bump only for plugins-otel to align it with other projects, there were no code changes.
|
package/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// libs/plugins/otel/src/lib/otel-plugin.ts
|
|
2
|
+
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
3
|
+
|
|
4
|
+
// libs/logger/src/lib/log-level.ts
|
|
5
|
+
var LOG_LEVEL_VALUES = {
|
|
6
|
+
debug: 0,
|
|
7
|
+
info: 1,
|
|
8
|
+
warn: 2,
|
|
9
|
+
error: 3,
|
|
10
|
+
fatal: 4,
|
|
11
|
+
silent: 5
|
|
12
|
+
};
|
|
13
|
+
function shouldLog(messageLevel, configuredLevel) {
|
|
14
|
+
return LOG_LEVEL_VALUES[messageLevel] >= LOG_LEVEL_VALUES[configuredLevel];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// libs/logger/src/lib/ansi-colors.ts
|
|
18
|
+
var ANSI = {
|
|
19
|
+
reset: "\x1B[0m",
|
|
20
|
+
bold: "\x1B[1m",
|
|
21
|
+
dim: "\x1B[2m",
|
|
22
|
+
// Foreground colors
|
|
23
|
+
red: "\x1B[31m",
|
|
24
|
+
green: "\x1B[32m",
|
|
25
|
+
yellow: "\x1B[33m",
|
|
26
|
+
blue: "\x1B[34m",
|
|
27
|
+
magenta: "\x1B[35m",
|
|
28
|
+
cyan: "\x1B[36m",
|
|
29
|
+
white: "\x1B[37m",
|
|
30
|
+
gray: "\x1B[90m",
|
|
31
|
+
// Background colors
|
|
32
|
+
bgRed: "\x1B[41m",
|
|
33
|
+
bgYellow: "\x1B[43m"
|
|
34
|
+
};
|
|
35
|
+
function colorize(text, ...codes) {
|
|
36
|
+
if (codes.length === 0)
|
|
37
|
+
return text;
|
|
38
|
+
return `${codes.join("")}${text}${ANSI.reset}`;
|
|
39
|
+
}
|
|
40
|
+
function supportsColor() {
|
|
41
|
+
if (process.env["FORCE_COLOR"] === "1")
|
|
42
|
+
return true;
|
|
43
|
+
if (process.env["NO_COLOR"] !== void 0)
|
|
44
|
+
return false;
|
|
45
|
+
if (process.env["TERM"] === "dumb")
|
|
46
|
+
return false;
|
|
47
|
+
return process.stdout.isTTY === true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// libs/logger/src/lib/formatters.ts
|
|
51
|
+
var LEVEL_COLORS = {
|
|
52
|
+
debug: [ANSI.dim, ANSI.gray],
|
|
53
|
+
info: [ANSI.cyan],
|
|
54
|
+
warn: [ANSI.yellow],
|
|
55
|
+
error: [ANSI.red],
|
|
56
|
+
fatal: [ANSI.bold, ANSI.white, ANSI.bgRed]
|
|
57
|
+
};
|
|
58
|
+
function padLevel(level) {
|
|
59
|
+
return level.toUpperCase().padEnd(5);
|
|
60
|
+
}
|
|
61
|
+
function formatTimestamp(date) {
|
|
62
|
+
const y = date.getFullYear();
|
|
63
|
+
const mo = String(date.getMonth() + 1).padStart(2, "0");
|
|
64
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
65
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
66
|
+
const mi = String(date.getMinutes()).padStart(2, "0");
|
|
67
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
68
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
69
|
+
return `${y}-${mo}-${d} ${h}:${mi}:${s}.${ms}`;
|
|
70
|
+
}
|
|
71
|
+
function formatData(data) {
|
|
72
|
+
const entries = Object.entries(data);
|
|
73
|
+
if (entries.length === 0)
|
|
74
|
+
return "";
|
|
75
|
+
return " " + entries.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
|
|
76
|
+
}
|
|
77
|
+
function prettyFormatter(entry) {
|
|
78
|
+
const useColor = supportsColor();
|
|
79
|
+
const level = entry.level;
|
|
80
|
+
const ts = formatTimestamp(entry.timestamp);
|
|
81
|
+
const levelStr = padLevel(entry.level);
|
|
82
|
+
const ctx = `[${entry.context}]`;
|
|
83
|
+
const msg = entry.message;
|
|
84
|
+
const enrichmentStr = entry.enrichments ? formatData(entry.enrichments) : "";
|
|
85
|
+
const dataStr = entry.data ? formatData(entry.data) : "";
|
|
86
|
+
const extra = `${enrichmentStr}${dataStr}`;
|
|
87
|
+
if (useColor) {
|
|
88
|
+
const colors = LEVEL_COLORS[level];
|
|
89
|
+
const coloredLevel = colorize(levelStr, ...colors);
|
|
90
|
+
const coloredCtx = colorize(ctx, ANSI.magenta);
|
|
91
|
+
const coloredTs = colorize(ts, ANSI.gray);
|
|
92
|
+
return `${coloredTs} ${coloredLevel} ${coloredCtx} ${msg}${extra}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
return `${ts} ${levelStr} ${ctx} ${msg}${extra}
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
function jsonFormatter(entry) {
|
|
99
|
+
const output = {
|
|
100
|
+
timestamp: entry.timestamp.toISOString(),
|
|
101
|
+
level: entry.level,
|
|
102
|
+
context: entry.context,
|
|
103
|
+
message: entry.message
|
|
104
|
+
};
|
|
105
|
+
if (entry.enrichments && Object.keys(entry.enrichments).length > 0) {
|
|
106
|
+
Object.assign(output, entry.enrichments);
|
|
107
|
+
}
|
|
108
|
+
if (entry.data && Object.keys(entry.data).length > 0) {
|
|
109
|
+
output["data"] = entry.data;
|
|
110
|
+
}
|
|
111
|
+
return JSON.stringify(output) + "\n";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// libs/logger/src/lib/logger-config.types.ts
|
|
115
|
+
function resolveLoggingConfig(config) {
|
|
116
|
+
return {
|
|
117
|
+
level: config?.level ?? "info",
|
|
118
|
+
format: config?.format ?? "pretty",
|
|
119
|
+
timestamps: config?.timestamps ?? true,
|
|
120
|
+
output: config?.output ?? ((msg) => {
|
|
121
|
+
process.stdout.write(msg);
|
|
122
|
+
}),
|
|
123
|
+
errorOutput: config?.errorOutput ?? ((msg) => {
|
|
124
|
+
process.stderr.write(msg);
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// libs/logger/src/lib/logger.ts
|
|
130
|
+
var ERROR_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
|
|
131
|
+
var MomentumLogger = class _MomentumLogger {
|
|
132
|
+
static {
|
|
133
|
+
this.enrichers = [];
|
|
134
|
+
}
|
|
135
|
+
constructor(context, config) {
|
|
136
|
+
this.context = context;
|
|
137
|
+
this.config = isResolvedConfig(config) ? config : resolveLoggingConfig(config);
|
|
138
|
+
this.formatter = this.config.format === "json" ? jsonFormatter : prettyFormatter;
|
|
139
|
+
}
|
|
140
|
+
debug(message, data) {
|
|
141
|
+
this.log("debug", message, data);
|
|
142
|
+
}
|
|
143
|
+
info(message, data) {
|
|
144
|
+
this.log("info", message, data);
|
|
145
|
+
}
|
|
146
|
+
warn(message, data) {
|
|
147
|
+
this.log("warn", message, data);
|
|
148
|
+
}
|
|
149
|
+
error(message, data) {
|
|
150
|
+
this.log("error", message, data);
|
|
151
|
+
}
|
|
152
|
+
fatal(message, data) {
|
|
153
|
+
this.log("fatal", message, data);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Creates a child logger with a sub-context.
|
|
157
|
+
* e.g., `Momentum:DB` → `Momentum:DB:Migrate`
|
|
158
|
+
*/
|
|
159
|
+
child(subContext) {
|
|
160
|
+
return new _MomentumLogger(`${this.context}:${subContext}`, this.config);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Registers a global enricher that adds extra fields to all log entries.
|
|
164
|
+
*/
|
|
165
|
+
static registerEnricher(enricher) {
|
|
166
|
+
_MomentumLogger.enrichers.push(enricher);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Removes a previously registered enricher.
|
|
170
|
+
*/
|
|
171
|
+
static removeEnricher(enricher) {
|
|
172
|
+
const index = _MomentumLogger.enrichers.indexOf(enricher);
|
|
173
|
+
if (index >= 0) {
|
|
174
|
+
_MomentumLogger.enrichers.splice(index, 1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Clears all registered enrichers. Primarily for testing.
|
|
179
|
+
*/
|
|
180
|
+
static clearEnrichers() {
|
|
181
|
+
_MomentumLogger.enrichers.length = 0;
|
|
182
|
+
}
|
|
183
|
+
log(level, message, data) {
|
|
184
|
+
if (!shouldLog(level, this.config.level))
|
|
185
|
+
return;
|
|
186
|
+
const enrichments = this.collectEnrichments();
|
|
187
|
+
const entry = {
|
|
188
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
189
|
+
level,
|
|
190
|
+
context: this.context,
|
|
191
|
+
message,
|
|
192
|
+
data,
|
|
193
|
+
enrichments: Object.keys(enrichments).length > 0 ? enrichments : void 0
|
|
194
|
+
};
|
|
195
|
+
const formatted = this.formatter(entry);
|
|
196
|
+
if (ERROR_LEVELS.has(level)) {
|
|
197
|
+
this.config.errorOutput(formatted);
|
|
198
|
+
} else {
|
|
199
|
+
this.config.output(formatted);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
collectEnrichments() {
|
|
203
|
+
const result = {};
|
|
204
|
+
for (const enricher of _MomentumLogger.enrichers) {
|
|
205
|
+
Object.assign(result, enricher.enrich());
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function isResolvedConfig(config) {
|
|
211
|
+
if (!config)
|
|
212
|
+
return false;
|
|
213
|
+
return typeof config.level === "string" && typeof config.format === "string" && typeof config.timestamps === "boolean" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
|
|
214
|
+
typeof config.output === "function" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
|
|
215
|
+
typeof config.errorOutput === "function";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// libs/plugins/otel/src/lib/otel-plugin.ts
|
|
219
|
+
var OtelLogEnricher = class {
|
|
220
|
+
enrich() {
|
|
221
|
+
const span = trace.getActiveSpan();
|
|
222
|
+
if (!span) {
|
|
223
|
+
return {};
|
|
224
|
+
}
|
|
225
|
+
const spanContext = span.spanContext();
|
|
226
|
+
return {
|
|
227
|
+
traceId: spanContext.traceId,
|
|
228
|
+
spanId: spanContext.spanId
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
function otelPlugin(config = {}) {
|
|
233
|
+
const { serviceName = "momentum-cms", enrichLogs = true, attributes = {}, operations } = config;
|
|
234
|
+
let tracer;
|
|
235
|
+
let enricher = null;
|
|
236
|
+
return {
|
|
237
|
+
name: "otel",
|
|
238
|
+
onInit({ collections, logger }) {
|
|
239
|
+
tracer = trace.getTracer(serviceName);
|
|
240
|
+
logger.info(`OpenTelemetry tracing enabled (service: ${serviceName})`);
|
|
241
|
+
if (enrichLogs) {
|
|
242
|
+
enricher = new OtelLogEnricher();
|
|
243
|
+
MomentumLogger.registerEnricher(enricher);
|
|
244
|
+
logger.info("Log enricher registered for trace/span IDs");
|
|
245
|
+
}
|
|
246
|
+
for (const collection of collections) {
|
|
247
|
+
injectTracingHooks(collection, tracer, attributes, operations);
|
|
248
|
+
}
|
|
249
|
+
logger.info(`Tracing hooks injected into ${collections.length} collections`);
|
|
250
|
+
},
|
|
251
|
+
onShutdown({ logger }) {
|
|
252
|
+
if (enricher) {
|
|
253
|
+
MomentumLogger.removeEnricher(enricher);
|
|
254
|
+
enricher = null;
|
|
255
|
+
}
|
|
256
|
+
logger.info("OpenTelemetry plugin shut down");
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function injectTracingHooks(collection, tracer, attributes, operationFilter) {
|
|
261
|
+
collection.hooks = collection.hooks ?? {};
|
|
262
|
+
const beforeChangeHook = (args) => {
|
|
263
|
+
const operation = args.operation ?? "create";
|
|
264
|
+
if (operationFilter && !operationFilter.includes(operation)) {
|
|
265
|
+
return args.data;
|
|
266
|
+
}
|
|
267
|
+
const span = tracer.startSpan(`${collection.slug}.${operation}`, {
|
|
268
|
+
attributes: {
|
|
269
|
+
"momentum.collection": collection.slug,
|
|
270
|
+
"momentum.operation": operation,
|
|
271
|
+
...attributes
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
if (args.data) {
|
|
275
|
+
args.data["__otelSpan"] = span;
|
|
276
|
+
}
|
|
277
|
+
return args.data;
|
|
278
|
+
};
|
|
279
|
+
const afterChangeHook = (args) => {
|
|
280
|
+
const doc = args.doc ?? args.data ?? {};
|
|
281
|
+
const span = doc["__otelSpan"];
|
|
282
|
+
if (span && typeof span === "object" && "end" in span) {
|
|
283
|
+
const typedSpan = span;
|
|
284
|
+
typedSpan.setStatus({ code: SpanStatusCode.OK });
|
|
285
|
+
typedSpan.end();
|
|
286
|
+
}
|
|
287
|
+
if (args.doc) {
|
|
288
|
+
delete args.doc["__otelSpan"];
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const beforeDeleteHook = (args) => {
|
|
292
|
+
if (operationFilter && !operationFilter.includes("delete")) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const span = tracer.startSpan(`${collection.slug}.delete`, {
|
|
296
|
+
attributes: {
|
|
297
|
+
"momentum.collection": collection.slug,
|
|
298
|
+
"momentum.operation": "delete",
|
|
299
|
+
"momentum.documentId": args.doc?.["id"] ? String(args.doc["id"]) : "unknown",
|
|
300
|
+
...attributes
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
if (args.doc) {
|
|
304
|
+
args.doc["__otelSpan"] = span;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const afterDeleteHook = (args) => {
|
|
308
|
+
const doc = args.doc ?? {};
|
|
309
|
+
const span = doc["__otelSpan"];
|
|
310
|
+
if (span && typeof span === "object" && "end" in span) {
|
|
311
|
+
const typedSpan = span;
|
|
312
|
+
typedSpan.setStatus({ code: SpanStatusCode.OK });
|
|
313
|
+
typedSpan.end();
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
const existingBeforeChange = collection.hooks.beforeChange ?? [];
|
|
317
|
+
collection.hooks.beforeChange = [beforeChangeHook, ...existingBeforeChange];
|
|
318
|
+
const existingAfterChange = collection.hooks.afterChange ?? [];
|
|
319
|
+
collection.hooks.afterChange = [...existingAfterChange, afterChangeHook];
|
|
320
|
+
const existingBeforeDelete = collection.hooks.beforeDelete ?? [];
|
|
321
|
+
collection.hooks.beforeDelete = [beforeDeleteHook, ...existingBeforeDelete];
|
|
322
|
+
const existingAfterDelete = collection.hooks.afterDelete ?? [];
|
|
323
|
+
collection.hooks.afterDelete = [...existingAfterDelete, afterDeleteHook];
|
|
324
|
+
}
|
|
325
|
+
export {
|
|
326
|
+
otelPlugin
|
|
327
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momentumcms/plugins-otel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenTelemetry observability plugin for Momentum CMS",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Momentum CMS Contributors",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=18"
|
|
26
26
|
},
|
|
27
|
-
"type": "commonjs",
|
|
28
27
|
"main": "./index.cjs",
|
|
29
28
|
"types": "./src/index.d.ts",
|
|
30
29
|
"peerDependencies": {
|
|
31
|
-
"@momentumcms/core": "0.
|
|
32
|
-
"@momentumcms/logger": "0.
|
|
33
|
-
"@momentumcms/plugins-core": "0.
|
|
30
|
+
"@momentumcms/core": "0.1.2",
|
|
31
|
+
"@momentumcms/logger": "0.1.2",
|
|
32
|
+
"@momentumcms/plugins-core": "0.1.2",
|
|
34
33
|
"@opentelemetry/api": "^1.0.0"
|
|
35
34
|
},
|
|
36
|
-
"dependencies": {}
|
|
35
|
+
"dependencies": {},
|
|
36
|
+
"module": "./index.js"
|
|
37
37
|
}
|