@frontmcp/observability 1.1.2 → 1.2.1
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/esm/index.mjs +214 -45
- package/esm/package.json +3 -3
- package/index.d.ts +2 -1
- package/index.js +235 -54
- package/package.json +3 -3
- package/telemetry/index.d.ts +5 -2
- package/telemetry/telemetry.accessor.d.ts +13 -0
- package/telemetry/telemetry.counters.d.ts +139 -0
- package/telemetry/telemetry.factory.d.ts +51 -0
- package/telemetry/telemetry.tokens.d.ts +11 -0
package/esm/index.mjs
CHANGED
|
@@ -319,7 +319,7 @@ var init_otlp_sink = __esm({
|
|
|
319
319
|
});
|
|
320
320
|
|
|
321
321
|
// libs/observability/src/plugin/observability.plugin.ts
|
|
322
|
-
import { trace as
|
|
322
|
+
import { trace as trace6 } from "@opentelemetry/api";
|
|
323
323
|
import {
|
|
324
324
|
AgentCallHook,
|
|
325
325
|
DynamicPlugin,
|
|
@@ -770,43 +770,6 @@ import {
|
|
|
770
770
|
trace as trace4
|
|
771
771
|
} from "@opentelemetry/api";
|
|
772
772
|
|
|
773
|
-
// libs/observability/src/otel/trace-context-bridge.ts
|
|
774
|
-
import {
|
|
775
|
-
TraceFlags,
|
|
776
|
-
trace,
|
|
777
|
-
context as otelContext
|
|
778
|
-
} from "@opentelemetry/api";
|
|
779
|
-
function frontmcpToOTelSpanContext(tc) {
|
|
780
|
-
return {
|
|
781
|
-
traceId: tc.traceId,
|
|
782
|
-
spanId: tc.parentId,
|
|
783
|
-
traceFlags: tc.traceFlags & TraceFlags.SAMPLED ? TraceFlags.SAMPLED : TraceFlags.NONE,
|
|
784
|
-
isRemote: true
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
function otelToFrontmcpContext(sc) {
|
|
788
|
-
const flags = sc.traceFlags.toString(16).padStart(2, "0");
|
|
789
|
-
return {
|
|
790
|
-
traceId: sc.traceId,
|
|
791
|
-
parentId: sc.spanId,
|
|
792
|
-
traceFlags: sc.traceFlags,
|
|
793
|
-
raw: `00-${sc.traceId}-${sc.spanId}-${flags}`
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
function createOTelContextFromTrace(tc) {
|
|
797
|
-
const spanContext = frontmcpToOTelSpanContext(tc);
|
|
798
|
-
const span = trace.wrapSpanContext(spanContext);
|
|
799
|
-
return trace.setSpan(otelContext.active(), span);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// libs/observability/src/plugin/observability.hooks.ts
|
|
803
|
-
import {
|
|
804
|
-
context as otelContext3,
|
|
805
|
-
SpanKind as SpanKind11,
|
|
806
|
-
trace as trace3
|
|
807
|
-
} from "@opentelemetry/api";
|
|
808
|
-
import { sha256Hex } from "@frontmcp/utils";
|
|
809
|
-
|
|
810
773
|
// libs/observability/src/otel/otel.types.ts
|
|
811
774
|
var McpAttributes = {
|
|
812
775
|
/** MCP protocol method name (e.g., "tools/call", "resources/read") */
|
|
@@ -920,6 +883,43 @@ var EnduserAttributes = {
|
|
|
920
883
|
SCOPE: "enduser.scope"
|
|
921
884
|
};
|
|
922
885
|
|
|
886
|
+
// libs/observability/src/otel/trace-context-bridge.ts
|
|
887
|
+
import {
|
|
888
|
+
TraceFlags,
|
|
889
|
+
trace,
|
|
890
|
+
context as otelContext
|
|
891
|
+
} from "@opentelemetry/api";
|
|
892
|
+
function frontmcpToOTelSpanContext(tc) {
|
|
893
|
+
return {
|
|
894
|
+
traceId: tc.traceId,
|
|
895
|
+
spanId: tc.parentId,
|
|
896
|
+
traceFlags: tc.traceFlags & TraceFlags.SAMPLED ? TraceFlags.SAMPLED : TraceFlags.NONE,
|
|
897
|
+
isRemote: true
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function otelToFrontmcpContext(sc) {
|
|
901
|
+
const flags = sc.traceFlags.toString(16).padStart(2, "0");
|
|
902
|
+
return {
|
|
903
|
+
traceId: sc.traceId,
|
|
904
|
+
parentId: sc.spanId,
|
|
905
|
+
traceFlags: sc.traceFlags,
|
|
906
|
+
raw: `00-${sc.traceId}-${sc.spanId}-${flags}`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function createOTelContextFromTrace(tc) {
|
|
910
|
+
const spanContext = frontmcpToOTelSpanContext(tc);
|
|
911
|
+
const span = trace.wrapSpanContext(spanContext);
|
|
912
|
+
return trace.setSpan(otelContext.active(), span);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// libs/observability/src/plugin/observability.hooks.ts
|
|
916
|
+
import {
|
|
917
|
+
context as otelContext3,
|
|
918
|
+
SpanKind as SpanKind11,
|
|
919
|
+
trace as trace3
|
|
920
|
+
} from "@opentelemetry/api";
|
|
921
|
+
import { sha256Hex } from "@frontmcp/utils";
|
|
922
|
+
|
|
923
923
|
// libs/observability/src/otel/spans/auth.span.ts
|
|
924
924
|
import { SpanKind as SpanKind2 } from "@opentelemetry/api";
|
|
925
925
|
|
|
@@ -1698,6 +1698,97 @@ function reportStartup(data) {
|
|
|
1698
1698
|
emitStartupReport(tracer, data);
|
|
1699
1699
|
}
|
|
1700
1700
|
|
|
1701
|
+
// libs/observability/src/telemetry/telemetry.counters.ts
|
|
1702
|
+
import { metrics } from "@opentelemetry/api";
|
|
1703
|
+
function snapshotKey(name, attributes) {
|
|
1704
|
+
const keys = Object.keys(attributes).sort();
|
|
1705
|
+
if (keys.length === 0) return name;
|
|
1706
|
+
const tail = keys.map((k) => `${k}=${attributes[k]}`).join(",");
|
|
1707
|
+
return `${name}{${tail}}`;
|
|
1708
|
+
}
|
|
1709
|
+
var snapshotStore = /* @__PURE__ */ new Map();
|
|
1710
|
+
var TelemetryCounterImpl = class {
|
|
1711
|
+
constructor(name, otelCounter) {
|
|
1712
|
+
this.name = name;
|
|
1713
|
+
this.otelCounter = otelCounter;
|
|
1714
|
+
}
|
|
1715
|
+
inc(by = 1, attributes = {}) {
|
|
1716
|
+
if (!Number.isFinite(by) || by < 0) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
try {
|
|
1720
|
+
this.otelCounter.add(by, attributes);
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
const key = snapshotKey(this.name, attributes);
|
|
1724
|
+
const existing = snapshotStore.get(key);
|
|
1725
|
+
if (existing) {
|
|
1726
|
+
existing.count += by;
|
|
1727
|
+
} else {
|
|
1728
|
+
snapshotStore.set(key, { name: this.name, count: by, attributes: { ...attributes } });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
var METER_NAME = "@frontmcp/observability";
|
|
1733
|
+
var counterCache = /* @__PURE__ */ new Map();
|
|
1734
|
+
function createCounter(name, description) {
|
|
1735
|
+
const existing = counterCache.get(name);
|
|
1736
|
+
if (existing) return existing;
|
|
1737
|
+
const meter = metrics.getMeter(METER_NAME);
|
|
1738
|
+
const otelCounter = meter.createCounter(name, description ? { description } : void 0);
|
|
1739
|
+
const counter = new TelemetryCounterImpl(name, otelCounter);
|
|
1740
|
+
counterCache.set(name, counter);
|
|
1741
|
+
return counter;
|
|
1742
|
+
}
|
|
1743
|
+
function getMetricSnapshot() {
|
|
1744
|
+
return Array.from(snapshotStore.values()).map((entry) => ({
|
|
1745
|
+
name: entry.name,
|
|
1746
|
+
count: entry.count,
|
|
1747
|
+
attributes: { ...entry.attributes }
|
|
1748
|
+
}));
|
|
1749
|
+
}
|
|
1750
|
+
function getCounterTotal(name) {
|
|
1751
|
+
let sum = 0;
|
|
1752
|
+
for (const entry of snapshotStore.values()) {
|
|
1753
|
+
if (entry.name === name) sum += entry.count;
|
|
1754
|
+
}
|
|
1755
|
+
return sum;
|
|
1756
|
+
}
|
|
1757
|
+
function resetMetricSnapshot() {
|
|
1758
|
+
snapshotStore.clear();
|
|
1759
|
+
}
|
|
1760
|
+
function resetTelemetrySnapshotForTesting() {
|
|
1761
|
+
snapshotStore.clear();
|
|
1762
|
+
}
|
|
1763
|
+
function resetCounterCacheForTesting() {
|
|
1764
|
+
counterCache.clear();
|
|
1765
|
+
}
|
|
1766
|
+
var KNOWN_BUNDLE_SOURCES = ["static", "npm", "saas-pull", "webhook", "filesystem", "unknown"];
|
|
1767
|
+
function normalizeBundleSource(value) {
|
|
1768
|
+
if (typeof value !== "string") return "unknown";
|
|
1769
|
+
return KNOWN_BUNDLE_SOURCES.includes(value) ? value : "unknown";
|
|
1770
|
+
}
|
|
1771
|
+
var KNOWN_ERROR_REASONS = ["network", "timeout", "invalid_response", "parse_error", "unknown"];
|
|
1772
|
+
function normalizeErrorReason(err) {
|
|
1773
|
+
if (!err) return "unknown";
|
|
1774
|
+
const name = err instanceof Error ? err.name : "";
|
|
1775
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
1776
|
+
const haystack = `${name} ${message}`.toLowerCase();
|
|
1777
|
+
if (name === "TimeoutError" || haystack.includes("timeout") || haystack.includes("timed out")) {
|
|
1778
|
+
return "timeout";
|
|
1779
|
+
}
|
|
1780
|
+
if (name === "NetworkError" || name === "FetchError" || haystack.includes("network") || haystack.includes("econnrefused") || haystack.includes("econnreset") || haystack.includes("enotfound") || haystack.includes("socket") || haystack.includes("dns")) {
|
|
1781
|
+
return "network";
|
|
1782
|
+
}
|
|
1783
|
+
if (name === "SyntaxError" || haystack.includes("parse") || haystack.includes("json")) {
|
|
1784
|
+
return "parse_error";
|
|
1785
|
+
}
|
|
1786
|
+
if (name === "TypeError" || name === "RangeError" || haystack.includes("invalid") || haystack.includes("malformed") || haystack.includes("schema")) {
|
|
1787
|
+
return "invalid_response";
|
|
1788
|
+
}
|
|
1789
|
+
return "unknown";
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1701
1792
|
// libs/observability/src/telemetry/telemetry.accessor.ts
|
|
1702
1793
|
var TelemetrySpan = class {
|
|
1703
1794
|
constructor(span) {
|
|
@@ -1855,6 +1946,20 @@ var TelemetryAccessor = class {
|
|
|
1855
1946
|
activeSpan.setAttributes(attrs);
|
|
1856
1947
|
}
|
|
1857
1948
|
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Create (or retrieve cached) a named counter.
|
|
1951
|
+
*
|
|
1952
|
+
* Counter increments are exported through the global OTel MeterProvider
|
|
1953
|
+
* when one is configured, and are always recorded in an in-memory snapshot
|
|
1954
|
+
* store (`getMetricSnapshot()` from `@frontmcp/observability`) so tests can
|
|
1955
|
+
* assert behavior without standing up a full OTel pipeline.
|
|
1956
|
+
*
|
|
1957
|
+
* @param name - OTel-style snake_case name, e.g. `frontmcp_skills_bundle_pulls_total`
|
|
1958
|
+
* @param description - human-readable description
|
|
1959
|
+
*/
|
|
1960
|
+
createCounter(name, description) {
|
|
1961
|
+
return createCounter(name, description);
|
|
1962
|
+
}
|
|
1858
1963
|
/**
|
|
1859
1964
|
* Get the trace ID of the current request.
|
|
1860
1965
|
* Useful for including in external API calls or logs.
|
|
@@ -1870,8 +1975,55 @@ var TelemetryAccessor = class {
|
|
|
1870
1975
|
}
|
|
1871
1976
|
};
|
|
1872
1977
|
|
|
1978
|
+
// libs/observability/src/telemetry/telemetry.factory.ts
|
|
1979
|
+
import { SpanStatusCode as SpanStatusCode3, trace as trace5 } from "@opentelemetry/api";
|
|
1980
|
+
var TelemetryFactory = class {
|
|
1981
|
+
constructor(tracerName = "@frontmcp/observability") {
|
|
1982
|
+
this.tracer = trace5.getTracer(tracerName);
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Create (or retrieve cached) a named counter. Increments propagate to
|
|
1986
|
+
* the OTel meter (when configured) and to the in-memory snapshot store.
|
|
1987
|
+
*
|
|
1988
|
+
* @param name - OTel-style snake_case name with `_total` suffix.
|
|
1989
|
+
* @param description - human-readable description (deduped: subsequent
|
|
1990
|
+
* calls with a different description return the
|
|
1991
|
+
* cached counter unchanged).
|
|
1992
|
+
*/
|
|
1993
|
+
createCounter(name, description) {
|
|
1994
|
+
return createCounter(name, description);
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Start a span. Caller is responsible for calling `end()` /
|
|
1998
|
+
* `endWithError()`. The span parents to the currently-active OTel
|
|
1999
|
+
* context; if none is active it becomes a root span.
|
|
2000
|
+
*/
|
|
2001
|
+
startSpan(name, attributes, parent) {
|
|
2002
|
+
const opts = attributes ? { attributes } : void 0;
|
|
2003
|
+
const span = parent ? this.tracer.startSpan(name, opts, parent) : this.tracer.startSpan(name, opts);
|
|
2004
|
+
return new TelemetrySpan(span);
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Convenience wrapper — start a span, run `fn` with it, end on success
|
|
2008
|
+
* or error. Same lifecycle as `TelemetryAccessor.withSpan` but without
|
|
2009
|
+
* the per-request base attributes.
|
|
2010
|
+
*/
|
|
2011
|
+
async withSpan(name, fn, attributes) {
|
|
2012
|
+
const span = this.startSpan(name, attributes);
|
|
2013
|
+
try {
|
|
2014
|
+
const result = await fn(span);
|
|
2015
|
+
span.end();
|
|
2016
|
+
return result;
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
span.endWithError(err instanceof Error ? err : String(err));
|
|
2019
|
+
throw err;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
|
|
1873
2024
|
// libs/observability/src/telemetry/telemetry.tokens.ts
|
|
1874
2025
|
var TELEMETRY_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-accessor");
|
|
2026
|
+
var TELEMETRY_FACTORY = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-factory");
|
|
1875
2027
|
|
|
1876
2028
|
// libs/observability/src/plugin/observability.plugin.ts
|
|
1877
2029
|
var PromptHook = FlowHooksOf("prompts:get-prompt");
|
|
@@ -2305,7 +2457,7 @@ ObservabilityPlugin.dynamicProviders = (input) => {
|
|
|
2305
2457
|
providers.push({
|
|
2306
2458
|
name: "observability:tracer",
|
|
2307
2459
|
provide: OTEL_TRACER,
|
|
2308
|
-
useValue:
|
|
2460
|
+
useValue: trace6.getTracer("@frontmcp/observability")
|
|
2309
2461
|
});
|
|
2310
2462
|
providers.push({
|
|
2311
2463
|
name: "observability:config",
|
|
@@ -2319,6 +2471,11 @@ ObservabilityPlugin.dynamicProviders = (input) => {
|
|
|
2319
2471
|
inject: () => [FRONTMCP_CONTEXT],
|
|
2320
2472
|
useFactory: (ctx) => new TelemetryAccessor(ctx)
|
|
2321
2473
|
});
|
|
2474
|
+
providers.push({
|
|
2475
|
+
name: "observability:telemetry-factory",
|
|
2476
|
+
provide: TELEMETRY_FACTORY,
|
|
2477
|
+
useValue: new TelemetryFactory()
|
|
2478
|
+
});
|
|
2322
2479
|
}
|
|
2323
2480
|
if (options.logging !== false) {
|
|
2324
2481
|
const loggingOpts = options.logging;
|
|
@@ -2756,12 +2913,12 @@ function resolveRequestLogOptions(input) {
|
|
|
2756
2913
|
|
|
2757
2914
|
// libs/observability/src/otel/context-propagator.ts
|
|
2758
2915
|
import {
|
|
2759
|
-
trace as
|
|
2916
|
+
trace as trace7
|
|
2760
2917
|
} from "@opentelemetry/api";
|
|
2761
2918
|
var TRACEPARENT_HEADER = "traceparent";
|
|
2762
2919
|
var FrontMcpPropagator = class {
|
|
2763
2920
|
inject(context, carrier, setter) {
|
|
2764
|
-
const span =
|
|
2921
|
+
const span = trace7.getSpan(context);
|
|
2765
2922
|
if (!span) return;
|
|
2766
2923
|
const sc = span.spanContext();
|
|
2767
2924
|
if (!sc || !isValidSpanContext(sc)) return;
|
|
@@ -2781,8 +2938,8 @@ var FrontMcpPropagator = class {
|
|
|
2781
2938
|
traceFlags: parsed.traceFlags,
|
|
2782
2939
|
isRemote: true
|
|
2783
2940
|
};
|
|
2784
|
-
const span =
|
|
2785
|
-
return
|
|
2941
|
+
const span = trace7.wrapSpanContext(spanContext);
|
|
2942
|
+
return trace7.setSpan(context, span);
|
|
2786
2943
|
}
|
|
2787
2944
|
fields() {
|
|
2788
2945
|
return [TRACEPARENT_HEADER];
|
|
@@ -2877,7 +3034,7 @@ function startHookSpan(tracer, options) {
|
|
|
2877
3034
|
}
|
|
2878
3035
|
|
|
2879
3036
|
// libs/observability/src/otel/spans/pretty-span-exporter.ts
|
|
2880
|
-
import { SpanKind as SpanKind14, SpanStatusCode as
|
|
3037
|
+
import { SpanKind as SpanKind14, SpanStatusCode as SpanStatusCode4 } from "@opentelemetry/api";
|
|
2881
3038
|
var RESET = "\x1B[0m";
|
|
2882
3039
|
var DIM = "\x1B[2m";
|
|
2883
3040
|
var BOLD = "\x1B[1m";
|
|
@@ -2929,7 +3086,7 @@ var PrettySpanExporter = class {
|
|
|
2929
3086
|
const traceShort = span.spanContext().traceId.slice(0, 8);
|
|
2930
3087
|
const durationMs = (span.duration[0] * 1e3 + span.duration[1] / 1e6).toFixed(1);
|
|
2931
3088
|
const kind = KIND_LABELS[span.kind] ?? "UNKNOWN";
|
|
2932
|
-
const statusOk = span.status.code !==
|
|
3089
|
+
const statusOk = span.status.code !== SpanStatusCode4.ERROR;
|
|
2933
3090
|
const attrs = Object.entries(span.attributes).filter(([k]) => SHOW_ATTRS.has(k)).map(([k, v]) => {
|
|
2934
3091
|
const short = k.replace("frontmcp.", "").replace("mcp.", "");
|
|
2935
3092
|
return `${short}=${v}`;
|
|
@@ -3013,6 +3170,8 @@ export {
|
|
|
3013
3170
|
FrontMcpAttributes,
|
|
3014
3171
|
FrontMcpPropagator,
|
|
3015
3172
|
HttpAttributes,
|
|
3173
|
+
KNOWN_BUNDLE_SOURCES,
|
|
3174
|
+
KNOWN_ERROR_REASONS,
|
|
3016
3175
|
LOG_LEVEL_TO_OTEL_SEVERITY,
|
|
3017
3176
|
McpAttributes,
|
|
3018
3177
|
OTEL_CONFIG,
|
|
@@ -3027,11 +3186,14 @@ export {
|
|
|
3027
3186
|
StdoutSink,
|
|
3028
3187
|
StructuredLogTransport,
|
|
3029
3188
|
TELEMETRY_ACCESSOR,
|
|
3189
|
+
TELEMETRY_FACTORY,
|
|
3030
3190
|
TelemetryAccessor,
|
|
3191
|
+
TelemetryFactory,
|
|
3031
3192
|
TelemetrySpan,
|
|
3032
3193
|
WinstonSink,
|
|
3033
3194
|
assertSpanAttribute,
|
|
3034
3195
|
assertSpanExists,
|
|
3196
|
+
createCounter,
|
|
3035
3197
|
createOTelContextFromTrace,
|
|
3036
3198
|
createSink,
|
|
3037
3199
|
createSinks,
|
|
@@ -3042,11 +3204,18 @@ export {
|
|
|
3042
3204
|
findSpan,
|
|
3043
3205
|
findSpansByAttribute,
|
|
3044
3206
|
frontmcpToOTelSpanContext,
|
|
3207
|
+
getCounterTotal,
|
|
3045
3208
|
getFinishedSpans,
|
|
3209
|
+
getMetricSnapshot,
|
|
3210
|
+
normalizeBundleSource,
|
|
3211
|
+
normalizeErrorReason,
|
|
3046
3212
|
otelToFrontmcpContext,
|
|
3047
3213
|
recordHookEvent,
|
|
3048
3214
|
redactFields,
|
|
3049
3215
|
reportStartup,
|
|
3216
|
+
resetCounterCacheForTesting,
|
|
3217
|
+
resetMetricSnapshot,
|
|
3218
|
+
resetTelemetrySnapshotForTesting,
|
|
3050
3219
|
sessionTracingId,
|
|
3051
3220
|
setAuthMode,
|
|
3052
3221
|
setAuthResult,
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/observability",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "OpenTelemetry instrumentation, structured JSON logging, and request log objects for FrontMCP",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@opentelemetry/api": "^1.9.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@frontmcp/sdk": "1.1
|
|
55
|
-
"@frontmcp/utils": "1.1
|
|
54
|
+
"@frontmcp/sdk": "1.2.1",
|
|
55
|
+
"@frontmcp/utils": "1.2.1",
|
|
56
56
|
"@opentelemetry/sdk-trace-base": "^1.25.0",
|
|
57
57
|
"@opentelemetry/sdk-node": "^0.52.0",
|
|
58
58
|
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
package/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { McpAttributes, FrontMcpAttributes, HttpAttributes, RpcAttributes, Endus
|
|
|
4
4
|
export type { TracingOptions, OTelSetupOptions, TraceContextLike, StartSpanOptions, HttpServerSpanOptions, RpcSpanOptions, ToolSpanOptions, ResourceSpanOptions, PromptSpanOptions, HookSpanOptions, FetchSpanOptions, TransportSpanOptions, AuthSpanOptions, StartupTelemetryData, } from './otel';
|
|
5
5
|
export { StructuredLogTransport, LOG_LEVEL_TO_OTEL_SEVERITY, StdoutSink, ConsoleSink, WinstonSink, PinoSink, CallbackSink, OtlpSink, createSink, createSinks, redactFields, } from './logging';
|
|
6
6
|
export type { StructuredLogEntry, StructuredLogError, StructuredLogTransportOptions, LogSink, SinkConfig, StdoutSinkConfig, ConsoleSinkConfig, WinstonSinkConfig, PinoSinkConfig, CallbackSinkConfig, OtlpSinkConfig, OtlpSinkOptions, WinstonLike, PinoLike, ContextAccessor, ContextSnapshot, } from './logging';
|
|
7
|
-
export { TelemetryAccessor, TelemetrySpan, TELEMETRY_ACCESSOR } from './telemetry';
|
|
7
|
+
export { TelemetryAccessor, TelemetrySpan, TelemetryFactory, TELEMETRY_ACCESSOR, TELEMETRY_FACTORY, createCounter, getMetricSnapshot, getCounterTotal, resetMetricSnapshot, resetTelemetrySnapshotForTesting, resetCounterCacheForTesting, normalizeBundleSource, normalizeErrorReason, KNOWN_BUNDLE_SOURCES, KNOWN_ERROR_REASONS, } from './telemetry';
|
|
8
|
+
export type { TelemetryCounter, CounterSnapshotEntry, KnownBundleSource, KnownErrorReason } from './telemetry';
|
|
8
9
|
export { createTestTracer, getFinishedSpans, assertSpanExists, assertSpanAttribute, findSpan, findSpansByAttribute, } from './testing';
|
|
9
10
|
export type { TestTracer } from './testing';
|
|
10
11
|
export { RequestLogCollector, REQUEST_LOG_COLLECTOR } from './request-log';
|
package/index.js
CHANGED
|
@@ -322,6 +322,8 @@ __export(index_exports, {
|
|
|
322
322
|
FrontMcpAttributes: () => FrontMcpAttributes,
|
|
323
323
|
FrontMcpPropagator: () => FrontMcpPropagator,
|
|
324
324
|
HttpAttributes: () => HttpAttributes,
|
|
325
|
+
KNOWN_BUNDLE_SOURCES: () => KNOWN_BUNDLE_SOURCES,
|
|
326
|
+
KNOWN_ERROR_REASONS: () => KNOWN_ERROR_REASONS,
|
|
325
327
|
LOG_LEVEL_TO_OTEL_SEVERITY: () => LOG_LEVEL_TO_OTEL_SEVERITY,
|
|
326
328
|
McpAttributes: () => McpAttributes,
|
|
327
329
|
OTEL_CONFIG: () => OTEL_CONFIG,
|
|
@@ -336,11 +338,14 @@ __export(index_exports, {
|
|
|
336
338
|
StdoutSink: () => StdoutSink,
|
|
337
339
|
StructuredLogTransport: () => StructuredLogTransport,
|
|
338
340
|
TELEMETRY_ACCESSOR: () => TELEMETRY_ACCESSOR,
|
|
341
|
+
TELEMETRY_FACTORY: () => TELEMETRY_FACTORY,
|
|
339
342
|
TelemetryAccessor: () => TelemetryAccessor,
|
|
343
|
+
TelemetryFactory: () => TelemetryFactory,
|
|
340
344
|
TelemetrySpan: () => TelemetrySpan,
|
|
341
345
|
WinstonSink: () => WinstonSink,
|
|
342
346
|
assertSpanAttribute: () => assertSpanAttribute,
|
|
343
347
|
assertSpanExists: () => assertSpanExists,
|
|
348
|
+
createCounter: () => createCounter,
|
|
344
349
|
createOTelContextFromTrace: () => createOTelContextFromTrace,
|
|
345
350
|
createSink: () => createSink,
|
|
346
351
|
createSinks: () => createSinks,
|
|
@@ -351,11 +356,18 @@ __export(index_exports, {
|
|
|
351
356
|
findSpan: () => findSpan,
|
|
352
357
|
findSpansByAttribute: () => findSpansByAttribute,
|
|
353
358
|
frontmcpToOTelSpanContext: () => frontmcpToOTelSpanContext,
|
|
359
|
+
getCounterTotal: () => getCounterTotal,
|
|
354
360
|
getFinishedSpans: () => getFinishedSpans,
|
|
361
|
+
getMetricSnapshot: () => getMetricSnapshot,
|
|
362
|
+
normalizeBundleSource: () => normalizeBundleSource,
|
|
363
|
+
normalizeErrorReason: () => normalizeErrorReason,
|
|
355
364
|
otelToFrontmcpContext: () => otelToFrontmcpContext,
|
|
356
365
|
recordHookEvent: () => recordHookEvent,
|
|
357
366
|
redactFields: () => redactFields,
|
|
358
367
|
reportStartup: () => reportStartup,
|
|
368
|
+
resetCounterCacheForTesting: () => resetCounterCacheForTesting,
|
|
369
|
+
resetMetricSnapshot: () => resetMetricSnapshot,
|
|
370
|
+
resetTelemetrySnapshotForTesting: () => resetTelemetrySnapshotForTesting,
|
|
359
371
|
sessionTracingId: () => sessionTracingId,
|
|
360
372
|
setAuthMode: () => setAuthMode,
|
|
361
373
|
setAuthResult: () => setAuthResult,
|
|
@@ -378,7 +390,7 @@ __export(index_exports, {
|
|
|
378
390
|
module.exports = __toCommonJS(index_exports);
|
|
379
391
|
|
|
380
392
|
// libs/observability/src/plugin/observability.plugin.ts
|
|
381
|
-
var
|
|
393
|
+
var import_api16 = require("@opentelemetry/api");
|
|
382
394
|
var import_sdk2 = require("@frontmcp/sdk");
|
|
383
395
|
|
|
384
396
|
// libs/observability/src/logging/sinks/stdout.sink.ts
|
|
@@ -810,36 +822,7 @@ var RequestLogCollector = class {
|
|
|
810
822
|
var REQUEST_LOG_COLLECTOR = /* @__PURE__ */ Symbol.for("frontmcp:observability:request-log-collector");
|
|
811
823
|
|
|
812
824
|
// libs/observability/src/telemetry/telemetry.accessor.ts
|
|
813
|
-
var
|
|
814
|
-
|
|
815
|
-
// libs/observability/src/otel/trace-context-bridge.ts
|
|
816
|
-
var import_api = require("@opentelemetry/api");
|
|
817
|
-
function frontmcpToOTelSpanContext(tc) {
|
|
818
|
-
return {
|
|
819
|
-
traceId: tc.traceId,
|
|
820
|
-
spanId: tc.parentId,
|
|
821
|
-
traceFlags: tc.traceFlags & import_api.TraceFlags.SAMPLED ? import_api.TraceFlags.SAMPLED : import_api.TraceFlags.NONE,
|
|
822
|
-
isRemote: true
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
function otelToFrontmcpContext(sc) {
|
|
826
|
-
const flags = sc.traceFlags.toString(16).padStart(2, "0");
|
|
827
|
-
return {
|
|
828
|
-
traceId: sc.traceId,
|
|
829
|
-
parentId: sc.spanId,
|
|
830
|
-
traceFlags: sc.traceFlags,
|
|
831
|
-
raw: `00-${sc.traceId}-${sc.spanId}-${flags}`
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
function createOTelContextFromTrace(tc) {
|
|
835
|
-
const spanContext = frontmcpToOTelSpanContext(tc);
|
|
836
|
-
const span = import_api.trace.wrapSpanContext(spanContext);
|
|
837
|
-
return import_api.trace.setSpan(import_api.context.active(), span);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// libs/observability/src/plugin/observability.hooks.ts
|
|
841
|
-
var import_api12 = require("@opentelemetry/api");
|
|
842
|
-
var import_utils = require("@frontmcp/utils");
|
|
825
|
+
var import_api14 = require("@opentelemetry/api");
|
|
843
826
|
|
|
844
827
|
// libs/observability/src/otel/otel.types.ts
|
|
845
828
|
var McpAttributes = {
|
|
@@ -954,6 +937,35 @@ var EnduserAttributes = {
|
|
|
954
937
|
SCOPE: "enduser.scope"
|
|
955
938
|
};
|
|
956
939
|
|
|
940
|
+
// libs/observability/src/otel/trace-context-bridge.ts
|
|
941
|
+
var import_api = require("@opentelemetry/api");
|
|
942
|
+
function frontmcpToOTelSpanContext(tc) {
|
|
943
|
+
return {
|
|
944
|
+
traceId: tc.traceId,
|
|
945
|
+
spanId: tc.parentId,
|
|
946
|
+
traceFlags: tc.traceFlags & import_api.TraceFlags.SAMPLED ? import_api.TraceFlags.SAMPLED : import_api.TraceFlags.NONE,
|
|
947
|
+
isRemote: true
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function otelToFrontmcpContext(sc) {
|
|
951
|
+
const flags = sc.traceFlags.toString(16).padStart(2, "0");
|
|
952
|
+
return {
|
|
953
|
+
traceId: sc.traceId,
|
|
954
|
+
parentId: sc.spanId,
|
|
955
|
+
traceFlags: sc.traceFlags,
|
|
956
|
+
raw: `00-${sc.traceId}-${sc.spanId}-${flags}`
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
function createOTelContextFromTrace(tc) {
|
|
960
|
+
const spanContext = frontmcpToOTelSpanContext(tc);
|
|
961
|
+
const span = import_api.trace.wrapSpanContext(spanContext);
|
|
962
|
+
return import_api.trace.setSpan(import_api.context.active(), span);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// libs/observability/src/plugin/observability.hooks.ts
|
|
966
|
+
var import_api12 = require("@opentelemetry/api");
|
|
967
|
+
var import_utils = require("@frontmcp/utils");
|
|
968
|
+
|
|
957
969
|
// libs/observability/src/otel/spans/auth.span.ts
|
|
958
970
|
var import_api3 = require("@opentelemetry/api");
|
|
959
971
|
|
|
@@ -1727,6 +1739,97 @@ function reportStartup(data) {
|
|
|
1727
1739
|
emitStartupReport(tracer, data);
|
|
1728
1740
|
}
|
|
1729
1741
|
|
|
1742
|
+
// libs/observability/src/telemetry/telemetry.counters.ts
|
|
1743
|
+
var import_api13 = require("@opentelemetry/api");
|
|
1744
|
+
function snapshotKey(name, attributes) {
|
|
1745
|
+
const keys = Object.keys(attributes).sort();
|
|
1746
|
+
if (keys.length === 0) return name;
|
|
1747
|
+
const tail = keys.map((k) => `${k}=${attributes[k]}`).join(",");
|
|
1748
|
+
return `${name}{${tail}}`;
|
|
1749
|
+
}
|
|
1750
|
+
var snapshotStore = /* @__PURE__ */ new Map();
|
|
1751
|
+
var TelemetryCounterImpl = class {
|
|
1752
|
+
constructor(name, otelCounter) {
|
|
1753
|
+
this.name = name;
|
|
1754
|
+
this.otelCounter = otelCounter;
|
|
1755
|
+
}
|
|
1756
|
+
inc(by = 1, attributes = {}) {
|
|
1757
|
+
if (!Number.isFinite(by) || by < 0) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
try {
|
|
1761
|
+
this.otelCounter.add(by, attributes);
|
|
1762
|
+
} catch {
|
|
1763
|
+
}
|
|
1764
|
+
const key = snapshotKey(this.name, attributes);
|
|
1765
|
+
const existing = snapshotStore.get(key);
|
|
1766
|
+
if (existing) {
|
|
1767
|
+
existing.count += by;
|
|
1768
|
+
} else {
|
|
1769
|
+
snapshotStore.set(key, { name: this.name, count: by, attributes: { ...attributes } });
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
var METER_NAME = "@frontmcp/observability";
|
|
1774
|
+
var counterCache = /* @__PURE__ */ new Map();
|
|
1775
|
+
function createCounter(name, description) {
|
|
1776
|
+
const existing = counterCache.get(name);
|
|
1777
|
+
if (existing) return existing;
|
|
1778
|
+
const meter = import_api13.metrics.getMeter(METER_NAME);
|
|
1779
|
+
const otelCounter = meter.createCounter(name, description ? { description } : void 0);
|
|
1780
|
+
const counter = new TelemetryCounterImpl(name, otelCounter);
|
|
1781
|
+
counterCache.set(name, counter);
|
|
1782
|
+
return counter;
|
|
1783
|
+
}
|
|
1784
|
+
function getMetricSnapshot() {
|
|
1785
|
+
return Array.from(snapshotStore.values()).map((entry) => ({
|
|
1786
|
+
name: entry.name,
|
|
1787
|
+
count: entry.count,
|
|
1788
|
+
attributes: { ...entry.attributes }
|
|
1789
|
+
}));
|
|
1790
|
+
}
|
|
1791
|
+
function getCounterTotal(name) {
|
|
1792
|
+
let sum = 0;
|
|
1793
|
+
for (const entry of snapshotStore.values()) {
|
|
1794
|
+
if (entry.name === name) sum += entry.count;
|
|
1795
|
+
}
|
|
1796
|
+
return sum;
|
|
1797
|
+
}
|
|
1798
|
+
function resetMetricSnapshot() {
|
|
1799
|
+
snapshotStore.clear();
|
|
1800
|
+
}
|
|
1801
|
+
function resetTelemetrySnapshotForTesting() {
|
|
1802
|
+
snapshotStore.clear();
|
|
1803
|
+
}
|
|
1804
|
+
function resetCounterCacheForTesting() {
|
|
1805
|
+
counterCache.clear();
|
|
1806
|
+
}
|
|
1807
|
+
var KNOWN_BUNDLE_SOURCES = ["static", "npm", "saas-pull", "webhook", "filesystem", "unknown"];
|
|
1808
|
+
function normalizeBundleSource(value) {
|
|
1809
|
+
if (typeof value !== "string") return "unknown";
|
|
1810
|
+
return KNOWN_BUNDLE_SOURCES.includes(value) ? value : "unknown";
|
|
1811
|
+
}
|
|
1812
|
+
var KNOWN_ERROR_REASONS = ["network", "timeout", "invalid_response", "parse_error", "unknown"];
|
|
1813
|
+
function normalizeErrorReason(err) {
|
|
1814
|
+
if (!err) return "unknown";
|
|
1815
|
+
const name = err instanceof Error ? err.name : "";
|
|
1816
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
1817
|
+
const haystack = `${name} ${message}`.toLowerCase();
|
|
1818
|
+
if (name === "TimeoutError" || haystack.includes("timeout") || haystack.includes("timed out")) {
|
|
1819
|
+
return "timeout";
|
|
1820
|
+
}
|
|
1821
|
+
if (name === "NetworkError" || name === "FetchError" || haystack.includes("network") || haystack.includes("econnrefused") || haystack.includes("econnreset") || haystack.includes("enotfound") || haystack.includes("socket") || haystack.includes("dns")) {
|
|
1822
|
+
return "network";
|
|
1823
|
+
}
|
|
1824
|
+
if (name === "SyntaxError" || haystack.includes("parse") || haystack.includes("json")) {
|
|
1825
|
+
return "parse_error";
|
|
1826
|
+
}
|
|
1827
|
+
if (name === "TypeError" || name === "RangeError" || haystack.includes("invalid") || haystack.includes("malformed") || haystack.includes("schema")) {
|
|
1828
|
+
return "invalid_response";
|
|
1829
|
+
}
|
|
1830
|
+
return "unknown";
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1730
1833
|
// libs/observability/src/telemetry/telemetry.accessor.ts
|
|
1731
1834
|
var TelemetrySpan = class {
|
|
1732
1835
|
constructor(span) {
|
|
@@ -1751,14 +1854,14 @@ var TelemetrySpan = class {
|
|
|
1751
1854
|
/** Record an error on this span */
|
|
1752
1855
|
recordError(error) {
|
|
1753
1856
|
this.span.recordException(error);
|
|
1754
|
-
this.span.setStatus({ code:
|
|
1857
|
+
this.span.setStatus({ code: import_api14.SpanStatusCode.ERROR, message: error.message });
|
|
1755
1858
|
this.hasError = true;
|
|
1756
1859
|
return this;
|
|
1757
1860
|
}
|
|
1758
1861
|
/** End the span (preserves ERROR status if already set) */
|
|
1759
1862
|
end() {
|
|
1760
1863
|
if (!this.hasError) {
|
|
1761
|
-
this.span.setStatus({ code:
|
|
1864
|
+
this.span.setStatus({ code: import_api14.SpanStatusCode.OK });
|
|
1762
1865
|
}
|
|
1763
1866
|
this.span.end();
|
|
1764
1867
|
}
|
|
@@ -1766,7 +1869,7 @@ var TelemetrySpan = class {
|
|
|
1766
1869
|
endWithError(error) {
|
|
1767
1870
|
const msg = typeof error === "string" ? error : error.message;
|
|
1768
1871
|
if (error instanceof Error) this.span.recordException(error);
|
|
1769
|
-
this.span.setStatus({ code:
|
|
1872
|
+
this.span.setStatus({ code: import_api14.SpanStatusCode.ERROR, message: msg });
|
|
1770
1873
|
this.span.end();
|
|
1771
1874
|
}
|
|
1772
1875
|
/** Get the underlying OTel span (escape hatch) */
|
|
@@ -1777,7 +1880,7 @@ var TelemetrySpan = class {
|
|
|
1777
1880
|
var TelemetryAccessor = class {
|
|
1778
1881
|
constructor(ctx) {
|
|
1779
1882
|
this.ctx = ctx;
|
|
1780
|
-
this.tracer =
|
|
1883
|
+
this.tracer = import_api14.trace.getTracer("@frontmcp/observability");
|
|
1781
1884
|
this.fallbackContext = createOTelContextFromTrace(ctx.traceContext);
|
|
1782
1885
|
this.sessionHash = sessionTracingId(ctx.sessionId);
|
|
1783
1886
|
this.baseAttributes = {
|
|
@@ -1813,7 +1916,7 @@ var TelemetryAccessor = class {
|
|
|
1813
1916
|
const span = this.tracer.startSpan(
|
|
1814
1917
|
name,
|
|
1815
1918
|
{
|
|
1816
|
-
kind:
|
|
1919
|
+
kind: import_api14.SpanKind.INTERNAL,
|
|
1817
1920
|
attributes: { ...this.baseAttributes, ...attributes }
|
|
1818
1921
|
},
|
|
1819
1922
|
this.getActiveContext()
|
|
@@ -1862,11 +1965,11 @@ var TelemetryAccessor = class {
|
|
|
1862
1965
|
} else {
|
|
1863
1966
|
const span = this.tracer.startSpan(
|
|
1864
1967
|
name,
|
|
1865
|
-
{ kind:
|
|
1968
|
+
{ kind: import_api14.SpanKind.INTERNAL, attributes: this.baseAttributes },
|
|
1866
1969
|
this.fallbackContext
|
|
1867
1970
|
);
|
|
1868
1971
|
if (attributes) span.addEvent(name, attributes);
|
|
1869
|
-
span.setStatus({ code:
|
|
1972
|
+
span.setStatus({ code: import_api14.SpanStatusCode.OK });
|
|
1870
1973
|
span.end();
|
|
1871
1974
|
}
|
|
1872
1975
|
}
|
|
@@ -1884,12 +1987,26 @@ var TelemetryAccessor = class {
|
|
|
1884
1987
|
activeSpan.setAttributes(attrs);
|
|
1885
1988
|
}
|
|
1886
1989
|
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Create (or retrieve cached) a named counter.
|
|
1992
|
+
*
|
|
1993
|
+
* Counter increments are exported through the global OTel MeterProvider
|
|
1994
|
+
* when one is configured, and are always recorded in an in-memory snapshot
|
|
1995
|
+
* store (`getMetricSnapshot()` from `@frontmcp/observability`) so tests can
|
|
1996
|
+
* assert behavior without standing up a full OTel pipeline.
|
|
1997
|
+
*
|
|
1998
|
+
* @param name - OTel-style snake_case name, e.g. `frontmcp_skills_bundle_pulls_total`
|
|
1999
|
+
* @param description - human-readable description
|
|
2000
|
+
*/
|
|
2001
|
+
createCounter(name, description) {
|
|
2002
|
+
return createCounter(name, description);
|
|
2003
|
+
}
|
|
1887
2004
|
/**
|
|
1888
2005
|
* Get the trace ID of the current request.
|
|
1889
2006
|
* Useful for including in external API calls or logs.
|
|
1890
2007
|
*/
|
|
1891
2008
|
get traceId() {
|
|
1892
|
-
return
|
|
2009
|
+
return import_api14.trace.getSpan(this.fallbackContext)?.spanContext().traceId ?? "";
|
|
1893
2010
|
}
|
|
1894
2011
|
/**
|
|
1895
2012
|
* Get the session tracing ID (privacy-safe hash).
|
|
@@ -1899,8 +2016,55 @@ var TelemetryAccessor = class {
|
|
|
1899
2016
|
}
|
|
1900
2017
|
};
|
|
1901
2018
|
|
|
2019
|
+
// libs/observability/src/telemetry/telemetry.factory.ts
|
|
2020
|
+
var import_api15 = require("@opentelemetry/api");
|
|
2021
|
+
var TelemetryFactory = class {
|
|
2022
|
+
constructor(tracerName = "@frontmcp/observability") {
|
|
2023
|
+
this.tracer = import_api15.trace.getTracer(tracerName);
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* Create (or retrieve cached) a named counter. Increments propagate to
|
|
2027
|
+
* the OTel meter (when configured) and to the in-memory snapshot store.
|
|
2028
|
+
*
|
|
2029
|
+
* @param name - OTel-style snake_case name with `_total` suffix.
|
|
2030
|
+
* @param description - human-readable description (deduped: subsequent
|
|
2031
|
+
* calls with a different description return the
|
|
2032
|
+
* cached counter unchanged).
|
|
2033
|
+
*/
|
|
2034
|
+
createCounter(name, description) {
|
|
2035
|
+
return createCounter(name, description);
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Start a span. Caller is responsible for calling `end()` /
|
|
2039
|
+
* `endWithError()`. The span parents to the currently-active OTel
|
|
2040
|
+
* context; if none is active it becomes a root span.
|
|
2041
|
+
*/
|
|
2042
|
+
startSpan(name, attributes, parent) {
|
|
2043
|
+
const opts = attributes ? { attributes } : void 0;
|
|
2044
|
+
const span = parent ? this.tracer.startSpan(name, opts, parent) : this.tracer.startSpan(name, opts);
|
|
2045
|
+
return new TelemetrySpan(span);
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Convenience wrapper — start a span, run `fn` with it, end on success
|
|
2049
|
+
* or error. Same lifecycle as `TelemetryAccessor.withSpan` but without
|
|
2050
|
+
* the per-request base attributes.
|
|
2051
|
+
*/
|
|
2052
|
+
async withSpan(name, fn, attributes) {
|
|
2053
|
+
const span = this.startSpan(name, attributes);
|
|
2054
|
+
try {
|
|
2055
|
+
const result = await fn(span);
|
|
2056
|
+
span.end();
|
|
2057
|
+
return result;
|
|
2058
|
+
} catch (err) {
|
|
2059
|
+
span.endWithError(err instanceof Error ? err : String(err));
|
|
2060
|
+
throw err;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
|
|
1902
2065
|
// libs/observability/src/telemetry/telemetry.tokens.ts
|
|
1903
2066
|
var TELEMETRY_ACCESSOR = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-accessor");
|
|
2067
|
+
var TELEMETRY_FACTORY = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-factory");
|
|
1904
2068
|
|
|
1905
2069
|
// libs/observability/src/plugin/observability.plugin.ts
|
|
1906
2070
|
var PromptHook = (0, import_sdk2.FlowHooksOf)("prompts:get-prompt");
|
|
@@ -2334,7 +2498,7 @@ ObservabilityPlugin.dynamicProviders = (input) => {
|
|
|
2334
2498
|
providers.push({
|
|
2335
2499
|
name: "observability:tracer",
|
|
2336
2500
|
provide: OTEL_TRACER,
|
|
2337
|
-
useValue:
|
|
2501
|
+
useValue: import_api16.trace.getTracer("@frontmcp/observability")
|
|
2338
2502
|
});
|
|
2339
2503
|
providers.push({
|
|
2340
2504
|
name: "observability:config",
|
|
@@ -2348,6 +2512,11 @@ ObservabilityPlugin.dynamicProviders = (input) => {
|
|
|
2348
2512
|
inject: () => [import_sdk2.FRONTMCP_CONTEXT],
|
|
2349
2513
|
useFactory: (ctx) => new TelemetryAccessor(ctx)
|
|
2350
2514
|
});
|
|
2515
|
+
providers.push({
|
|
2516
|
+
name: "observability:telemetry-factory",
|
|
2517
|
+
provide: TELEMETRY_FACTORY,
|
|
2518
|
+
useValue: new TelemetryFactory()
|
|
2519
|
+
});
|
|
2351
2520
|
}
|
|
2352
2521
|
if (options.logging !== false) {
|
|
2353
2522
|
const loggingOpts = options.logging;
|
|
@@ -2784,11 +2953,11 @@ function resolveRequestLogOptions(input) {
|
|
|
2784
2953
|
}
|
|
2785
2954
|
|
|
2786
2955
|
// libs/observability/src/otel/context-propagator.ts
|
|
2787
|
-
var
|
|
2956
|
+
var import_api17 = require("@opentelemetry/api");
|
|
2788
2957
|
var TRACEPARENT_HEADER = "traceparent";
|
|
2789
2958
|
var FrontMcpPropagator = class {
|
|
2790
2959
|
inject(context, carrier, setter) {
|
|
2791
|
-
const span =
|
|
2960
|
+
const span = import_api17.trace.getSpan(context);
|
|
2792
2961
|
if (!span) return;
|
|
2793
2962
|
const sc = span.spanContext();
|
|
2794
2963
|
if (!sc || !isValidSpanContext(sc)) return;
|
|
@@ -2808,8 +2977,8 @@ var FrontMcpPropagator = class {
|
|
|
2808
2977
|
traceFlags: parsed.traceFlags,
|
|
2809
2978
|
isRemote: true
|
|
2810
2979
|
};
|
|
2811
|
-
const span =
|
|
2812
|
-
return
|
|
2980
|
+
const span = import_api17.trace.wrapSpanContext(spanContext);
|
|
2981
|
+
return import_api17.trace.setSpan(context, span);
|
|
2813
2982
|
}
|
|
2814
2983
|
fields() {
|
|
2815
2984
|
return [TRACEPARENT_HEADER];
|
|
@@ -2877,7 +3046,7 @@ function setupOTel(options) {
|
|
|
2877
3046
|
}
|
|
2878
3047
|
|
|
2879
3048
|
// libs/observability/src/otel/spans/hook.span.ts
|
|
2880
|
-
var
|
|
3049
|
+
var import_api18 = require("@opentelemetry/api");
|
|
2881
3050
|
function recordHookEvent(parentSpan, stage, owner) {
|
|
2882
3051
|
const attributes = {};
|
|
2883
3052
|
if (owner) {
|
|
@@ -2897,14 +3066,14 @@ function startHookSpan(tracer, options) {
|
|
|
2897
3066
|
}
|
|
2898
3067
|
return startSpan(tracer, {
|
|
2899
3068
|
name: `hook ${options.stage}`,
|
|
2900
|
-
kind:
|
|
3069
|
+
kind: import_api18.SpanKind.INTERNAL,
|
|
2901
3070
|
attributes,
|
|
2902
3071
|
parentContext: options.parentContext
|
|
2903
3072
|
});
|
|
2904
3073
|
}
|
|
2905
3074
|
|
|
2906
3075
|
// libs/observability/src/otel/spans/pretty-span-exporter.ts
|
|
2907
|
-
var
|
|
3076
|
+
var import_api19 = require("@opentelemetry/api");
|
|
2908
3077
|
var RESET = "\x1B[0m";
|
|
2909
3078
|
var DIM = "\x1B[2m";
|
|
2910
3079
|
var BOLD = "\x1B[1m";
|
|
@@ -2913,11 +3082,11 @@ var GREEN = "\x1B[32m";
|
|
|
2913
3082
|
var RED = "\x1B[31m";
|
|
2914
3083
|
var YELLOW = "\x1B[33m";
|
|
2915
3084
|
var KIND_LABELS = {
|
|
2916
|
-
[
|
|
2917
|
-
[
|
|
2918
|
-
[
|
|
2919
|
-
[
|
|
2920
|
-
[
|
|
3085
|
+
[import_api19.SpanKind.INTERNAL]: "INTERNAL",
|
|
3086
|
+
[import_api19.SpanKind.SERVER]: "SERVER",
|
|
3087
|
+
[import_api19.SpanKind.CLIENT]: "CLIENT",
|
|
3088
|
+
[import_api19.SpanKind.PRODUCER]: "PRODUCER",
|
|
3089
|
+
[import_api19.SpanKind.CONSUMER]: "CONSUMER"
|
|
2921
3090
|
};
|
|
2922
3091
|
var SHOW_ATTRS = /* @__PURE__ */ new Set([
|
|
2923
3092
|
"rpc.system",
|
|
@@ -2956,7 +3125,7 @@ var PrettySpanExporter = class {
|
|
|
2956
3125
|
const traceShort = span.spanContext().traceId.slice(0, 8);
|
|
2957
3126
|
const durationMs = (span.duration[0] * 1e3 + span.duration[1] / 1e6).toFixed(1);
|
|
2958
3127
|
const kind = KIND_LABELS[span.kind] ?? "UNKNOWN";
|
|
2959
|
-
const statusOk = span.status.code !==
|
|
3128
|
+
const statusOk = span.status.code !== import_api19.SpanStatusCode.ERROR;
|
|
2960
3129
|
const attrs = Object.entries(span.attributes).filter(([k]) => SHOW_ATTRS.has(k)).map(([k, v]) => {
|
|
2961
3130
|
const short = k.replace("frontmcp.", "").replace("mcp.", "");
|
|
2962
3131
|
return `${short}=${v}`;
|
|
@@ -2965,7 +3134,7 @@ var PrettySpanExporter = class {
|
|
|
2965
3134
|
const eventSummary = eventCount > 0 ? ` (${eventCount} events)` : "";
|
|
2966
3135
|
if (this.useAnsi) {
|
|
2967
3136
|
const statusColor = statusOk ? GREEN : RED;
|
|
2968
|
-
const kindColor = span.kind ===
|
|
3137
|
+
const kindColor = span.kind === import_api19.SpanKind.SERVER ? CYAN : DIM;
|
|
2969
3138
|
return [
|
|
2970
3139
|
`${DIM}\u21B3${RESET}`,
|
|
2971
3140
|
`${BOLD}${statusColor}SPAN${RESET}`,
|
|
@@ -3037,6 +3206,8 @@ function findSpansByAttribute(spans, key, value) {
|
|
|
3037
3206
|
FrontMcpAttributes,
|
|
3038
3207
|
FrontMcpPropagator,
|
|
3039
3208
|
HttpAttributes,
|
|
3209
|
+
KNOWN_BUNDLE_SOURCES,
|
|
3210
|
+
KNOWN_ERROR_REASONS,
|
|
3040
3211
|
LOG_LEVEL_TO_OTEL_SEVERITY,
|
|
3041
3212
|
McpAttributes,
|
|
3042
3213
|
OTEL_CONFIG,
|
|
@@ -3051,11 +3222,14 @@ function findSpansByAttribute(spans, key, value) {
|
|
|
3051
3222
|
StdoutSink,
|
|
3052
3223
|
StructuredLogTransport,
|
|
3053
3224
|
TELEMETRY_ACCESSOR,
|
|
3225
|
+
TELEMETRY_FACTORY,
|
|
3054
3226
|
TelemetryAccessor,
|
|
3227
|
+
TelemetryFactory,
|
|
3055
3228
|
TelemetrySpan,
|
|
3056
3229
|
WinstonSink,
|
|
3057
3230
|
assertSpanAttribute,
|
|
3058
3231
|
assertSpanExists,
|
|
3232
|
+
createCounter,
|
|
3059
3233
|
createOTelContextFromTrace,
|
|
3060
3234
|
createSink,
|
|
3061
3235
|
createSinks,
|
|
@@ -3066,11 +3240,18 @@ function findSpansByAttribute(spans, key, value) {
|
|
|
3066
3240
|
findSpan,
|
|
3067
3241
|
findSpansByAttribute,
|
|
3068
3242
|
frontmcpToOTelSpanContext,
|
|
3243
|
+
getCounterTotal,
|
|
3069
3244
|
getFinishedSpans,
|
|
3245
|
+
getMetricSnapshot,
|
|
3246
|
+
normalizeBundleSource,
|
|
3247
|
+
normalizeErrorReason,
|
|
3070
3248
|
otelToFrontmcpContext,
|
|
3071
3249
|
recordHookEvent,
|
|
3072
3250
|
redactFields,
|
|
3073
3251
|
reportStartup,
|
|
3252
|
+
resetCounterCacheForTesting,
|
|
3253
|
+
resetMetricSnapshot,
|
|
3254
|
+
resetTelemetrySnapshotForTesting,
|
|
3074
3255
|
sessionTracingId,
|
|
3075
3256
|
setAuthMode,
|
|
3076
3257
|
setAuthResult,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/observability",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "OpenTelemetry instrumentation, structured JSON logging, and request log objects for FrontMCP",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@opentelemetry/api": "^1.9.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@frontmcp/sdk": "1.1
|
|
55
|
-
"@frontmcp/utils": "1.1
|
|
54
|
+
"@frontmcp/sdk": "1.2.1",
|
|
55
|
+
"@frontmcp/utils": "1.2.1",
|
|
56
56
|
"@opentelemetry/sdk-trace-base": "^1.25.0",
|
|
57
57
|
"@opentelemetry/sdk-node": "^0.52.0",
|
|
58
58
|
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
package/telemetry/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
export { TelemetryAccessor, TelemetrySpan } from './telemetry.accessor';
|
|
2
|
-
export { TELEMETRY_ACCESSOR } from './telemetry.tokens';
|
|
3
1
|
import './telemetry.context-extension';
|
|
2
|
+
export { TelemetryAccessor, TelemetrySpan } from './telemetry.accessor';
|
|
3
|
+
export { TELEMETRY_ACCESSOR, TELEMETRY_FACTORY } from './telemetry.tokens';
|
|
4
|
+
export { TelemetryFactory } from './telemetry.factory';
|
|
5
|
+
export { createCounter, getMetricSnapshot, getCounterTotal, resetMetricSnapshot, resetTelemetrySnapshotForTesting, resetCounterCacheForTesting, normalizeBundleSource, normalizeErrorReason, KNOWN_BUNDLE_SOURCES, KNOWN_ERROR_REASONS, } from './telemetry.counters';
|
|
6
|
+
export type { TelemetryCounter, CounterSnapshotEntry, KnownBundleSource, KnownErrorReason } from './telemetry.counters';
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { type Span } from '@opentelemetry/api';
|
|
35
35
|
import { type TraceContextLike } from '../otel/trace-context-bridge';
|
|
36
|
+
import { type TelemetryCounter } from './telemetry.counters';
|
|
36
37
|
/**
|
|
37
38
|
* Minimal context shape — avoids tight coupling to FrontMcpContext.
|
|
38
39
|
*/
|
|
@@ -138,6 +139,18 @@ export declare class TelemetryAccessor {
|
|
|
138
139
|
* @param attrs — key-value attributes
|
|
139
140
|
*/
|
|
140
141
|
setAttributes(attrs: Record<string, string | number | boolean>): void;
|
|
142
|
+
/**
|
|
143
|
+
* Create (or retrieve cached) a named counter.
|
|
144
|
+
*
|
|
145
|
+
* Counter increments are exported through the global OTel MeterProvider
|
|
146
|
+
* when one is configured, and are always recorded in an in-memory snapshot
|
|
147
|
+
* store (`getMetricSnapshot()` from `@frontmcp/observability`) so tests can
|
|
148
|
+
* assert behavior without standing up a full OTel pipeline.
|
|
149
|
+
*
|
|
150
|
+
* @param name - OTel-style snake_case name, e.g. `frontmcp_skills_bundle_pulls_total`
|
|
151
|
+
* @param description - human-readable description
|
|
152
|
+
*/
|
|
153
|
+
createCounter(name: string, description?: string): TelemetryCounter;
|
|
141
154
|
/**
|
|
142
155
|
* Get the trace ID of the current request.
|
|
143
156
|
* Useful for including in external API calls or logs.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelemetryCounter — minimal counter abstraction for FrontMCP.
|
|
3
|
+
*
|
|
4
|
+
* Backed by `@opentelemetry/api` `metrics.getMeter(...)` so installations
|
|
5
|
+
* that wire up an OTel MeterProvider get real counter exports. Independently,
|
|
6
|
+
* every increment is also recorded in an in-memory snapshot store so tests
|
|
7
|
+
* (and operators without a configured meter provider) can introspect counter
|
|
8
|
+
* activity without standing up a full OTel pipeline.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: ObservabilityPlugin does NOT register a `MeterProvider` — it only
|
|
11
|
+
* wires up tracing. To export counters to a real metrics backend (Prometheus,
|
|
12
|
+
* OTLP, etc.) the host application must call `metrics.setGlobalMeterProvider()`
|
|
13
|
+
* with its own configured `MeterProvider`. Without that step, counters are
|
|
14
|
+
* observable only via `getMetricSnapshot()` / `getCounterTotal()`.
|
|
15
|
+
*
|
|
16
|
+
* Naming convention follows OTel-style snake_case with a `_total` suffix:
|
|
17
|
+
* - `frontmcp_skills_bundle_pulls_total`
|
|
18
|
+
* - `frontmcp_skills_signature_failures_total`
|
|
19
|
+
* - `frontmcp_skills_signature_verifications_total`
|
|
20
|
+
* - `frontmcp_skills_replay_rejects_total`
|
|
21
|
+
* - `frontmcp_skills_replay_checks_total`
|
|
22
|
+
*
|
|
23
|
+
* Attributes should be lowercase snake_case and bounded cardinality
|
|
24
|
+
* (e.g. `status`, `source`, `reason`).
|
|
25
|
+
*
|
|
26
|
+
* Bounded vocabularies (enforced by helpers below):
|
|
27
|
+
* - `source`: 'static' | 'npm' | 'saas-pull' | 'filesystem' | 'unknown'
|
|
28
|
+
* - `reason` (errors): 'network' | 'timeout' | 'invalid_response' |
|
|
29
|
+
* 'parse_error' | 'unknown'
|
|
30
|
+
* - `status`: 'ok' | 'error'
|
|
31
|
+
*
|
|
32
|
+
* Anything outside the whitelist coerces to `'unknown'` (or `'other'` for
|
|
33
|
+
* subsystem-specific reason classifiers like `classifyReason()` in
|
|
34
|
+
* bundle-signature.ts) so untrusted input cannot create unbounded label
|
|
35
|
+
* cardinality.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Public counter handle — increments propagate to both the OTel meter
|
|
39
|
+
* (when configured) and the in-memory snapshot store.
|
|
40
|
+
*/
|
|
41
|
+
export interface TelemetryCounter {
|
|
42
|
+
/**
|
|
43
|
+
* Increment the counter.
|
|
44
|
+
*
|
|
45
|
+
* @param by - amount to add (default 1, must be >= 0)
|
|
46
|
+
* @param attributes - bounded-cardinality labels
|
|
47
|
+
*/
|
|
48
|
+
inc(by?: number, attributes?: Record<string, string>): void;
|
|
49
|
+
/** The metric name, useful for diagnostics/snapshot keys. */
|
|
50
|
+
readonly name: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create (or retrieve cached) named counter.
|
|
54
|
+
*
|
|
55
|
+
* Counters with the same name are de-duped — subsequent calls return the
|
|
56
|
+
* same handle. This prevents accidentally exporting two counters with the
|
|
57
|
+
* same name but slightly different descriptions.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createCounter(name: string, description?: string): TelemetryCounter;
|
|
60
|
+
/**
|
|
61
|
+
* Snapshot of all counters recorded since process start (or last reset).
|
|
62
|
+
* Useful for assertions in tests.
|
|
63
|
+
*
|
|
64
|
+
* Each entry is a (name, attributes, count) tuple. Counters with attributes
|
|
65
|
+
* are reported per unique attribute combination.
|
|
66
|
+
*/
|
|
67
|
+
export interface CounterSnapshotEntry {
|
|
68
|
+
name: string;
|
|
69
|
+
count: number;
|
|
70
|
+
attributes: Record<string, string>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read a snapshot of every counter increment recorded so far. Returned
|
|
74
|
+
* array is a fresh copy — mutating it does not affect the store.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getMetricSnapshot(): CounterSnapshotEntry[];
|
|
77
|
+
/**
|
|
78
|
+
* Lookup helper — sum of all increments for a given counter name (across
|
|
79
|
+
* every attribute combination).
|
|
80
|
+
*/
|
|
81
|
+
export declare function getCounterTotal(name: string): number;
|
|
82
|
+
/**
|
|
83
|
+
* Test helper — clear the in-memory snapshot store. Does not reset the
|
|
84
|
+
* underlying OTel meter (which has its own lifecycle).
|
|
85
|
+
*
|
|
86
|
+
* @deprecated Prefer {@link resetTelemetrySnapshotForTesting}, which is
|
|
87
|
+
* spelled to make its test-only intent explicit. Kept for backwards
|
|
88
|
+
* compatibility.
|
|
89
|
+
*/
|
|
90
|
+
export declare function resetMetricSnapshot(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Test helper — clear the in-memory snapshot store between specs to prevent
|
|
93
|
+
* cross-test leakage. The snapshot store is process-global; without an
|
|
94
|
+
* explicit reset between tests, counter increments from one spec can show
|
|
95
|
+
* up in another's assertions.
|
|
96
|
+
*
|
|
97
|
+
* @internal Test-only API. Production code MUST NOT call this.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resetTelemetrySnapshotForTesting(): void;
|
|
100
|
+
/**
|
|
101
|
+
* Test helper — clear the counter cache. Used only by tests that need to
|
|
102
|
+
* exercise the create-path repeatedly.
|
|
103
|
+
*
|
|
104
|
+
* @internal Test-only API. Production code MUST NOT call this.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resetCounterCacheForTesting(): void;
|
|
107
|
+
/**
|
|
108
|
+
* @deprecated Use {@link resetCounterCacheForTesting} instead. The leading
|
|
109
|
+
* underscore violates project naming conventions (CLAUDE.md). Kept temporarily
|
|
110
|
+
* for backwards compatibility with existing specs.
|
|
111
|
+
*/
|
|
112
|
+
export declare const _resetCounterCacheForTesting: typeof resetCounterCacheForTesting;
|
|
113
|
+
/**
|
|
114
|
+
* Bounded vocabulary for the `source` counter attribute. Anything outside
|
|
115
|
+
* this set is coerced to `'unknown'` to prevent cardinality explosions when
|
|
116
|
+
* upstream callers pass URLs, tenant IDs, or other unbounded strings.
|
|
117
|
+
*/
|
|
118
|
+
export declare const KNOWN_BUNDLE_SOURCES: readonly ["static", "npm", "saas-pull", "webhook", "filesystem", "unknown"];
|
|
119
|
+
export type KnownBundleSource = (typeof KNOWN_BUNDLE_SOURCES)[number];
|
|
120
|
+
/**
|
|
121
|
+
* Coerce a free-text source label to the bounded `KnownBundleSource` set.
|
|
122
|
+
* Unknown / undefined / non-string inputs map to `'unknown'`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function normalizeBundleSource(value: unknown): KnownBundleSource;
|
|
125
|
+
/**
|
|
126
|
+
* Bounded vocabulary for `reason` counter attributes derived from generic
|
|
127
|
+
* errors. Subsystem-specific classifiers (e.g. signature/replay) MAY define
|
|
128
|
+
* their own narrower vocabularies, but should use this one for catch-all
|
|
129
|
+
* "an Error was thrown" paths.
|
|
130
|
+
*/
|
|
131
|
+
export declare const KNOWN_ERROR_REASONS: readonly ["network", "timeout", "invalid_response", "parse_error", "unknown"];
|
|
132
|
+
export type KnownErrorReason = (typeof KNOWN_ERROR_REASONS)[number];
|
|
133
|
+
/**
|
|
134
|
+
* Map an arbitrary error to a low-cardinality `reason` label. Inspects the
|
|
135
|
+
* error name and a small substring of the message — never echoes the raw
|
|
136
|
+
* message back as a label, which would let untrusted error sources spawn
|
|
137
|
+
* unbounded timeseries.
|
|
138
|
+
*/
|
|
139
|
+
export declare function normalizeErrorReason(err: unknown): KnownErrorReason;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TelemetryFactory — SCOPE-scoped helper for creating counters and spans
|
|
3
|
+
* without requiring an active request context.
|
|
4
|
+
*
|
|
5
|
+
* `TelemetryAccessor` is CONTEXT-scoped (one per request) so it can read
|
|
6
|
+
* the active flow span and attach base attributes (request ID, session ID,
|
|
7
|
+
* etc). That's correct for tool/resource/prompt code paths but the wrong
|
|
8
|
+
* lifetime for scope-lifetime singletons:
|
|
9
|
+
*
|
|
10
|
+
* - `BundleStore` is constructed at scope-init and lives for the life
|
|
11
|
+
* of the scope. Its `swap()` may be invoked from a source's onChange
|
|
12
|
+
* callback (no request context) or from a meta-tool call (request
|
|
13
|
+
* context). Either way it just needs counters that work, and spans
|
|
14
|
+
* parented to whatever active OTel context is on the stack.
|
|
15
|
+
*
|
|
16
|
+
* The factory wraps the same process-global `createCounter` exported from
|
|
17
|
+
* this package, plus a tracer so callers can start spans. `startSpan`
|
|
18
|
+
* uses `trace.setSpan(context.active(), ...)` semantics implicitly via
|
|
19
|
+
* `tracer.startSpan` so callers nest correctly when a parent span exists.
|
|
20
|
+
*
|
|
21
|
+
* @see TELEMETRY_FACTORY
|
|
22
|
+
*/
|
|
23
|
+
import { type Context as OTelContext } from '@opentelemetry/api';
|
|
24
|
+
import { TelemetrySpan } from './telemetry.accessor';
|
|
25
|
+
import { type TelemetryCounter } from './telemetry.counters';
|
|
26
|
+
export declare class TelemetryFactory {
|
|
27
|
+
private readonly tracer;
|
|
28
|
+
constructor(tracerName?: string);
|
|
29
|
+
/**
|
|
30
|
+
* Create (or retrieve cached) a named counter. Increments propagate to
|
|
31
|
+
* the OTel meter (when configured) and to the in-memory snapshot store.
|
|
32
|
+
*
|
|
33
|
+
* @param name - OTel-style snake_case name with `_total` suffix.
|
|
34
|
+
* @param description - human-readable description (deduped: subsequent
|
|
35
|
+
* calls with a different description return the
|
|
36
|
+
* cached counter unchanged).
|
|
37
|
+
*/
|
|
38
|
+
createCounter(name: string, description?: string): TelemetryCounter;
|
|
39
|
+
/**
|
|
40
|
+
* Start a span. Caller is responsible for calling `end()` /
|
|
41
|
+
* `endWithError()`. The span parents to the currently-active OTel
|
|
42
|
+
* context; if none is active it becomes a root span.
|
|
43
|
+
*/
|
|
44
|
+
startSpan(name: string, attributes?: Record<string, string | number | boolean>, parent?: OTelContext): TelemetrySpan;
|
|
45
|
+
/**
|
|
46
|
+
* Convenience wrapper — start a span, run `fn` with it, end on success
|
|
47
|
+
* or error. Same lifecycle as `TelemetryAccessor.withSpan` but without
|
|
48
|
+
* the per-request base attributes.
|
|
49
|
+
*/
|
|
50
|
+
withSpan<T>(name: string, fn: (span: TelemetrySpan) => Promise<T>, attributes?: Record<string, string | number | boolean>): Promise<T>;
|
|
51
|
+
}
|
|
@@ -5,3 +5,14 @@
|
|
|
5
5
|
* Registered by ObservabilityPlugin when tracing is enabled.
|
|
6
6
|
*/
|
|
7
7
|
export declare const TELEMETRY_ACCESSOR: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* DI token for the SCOPE-level TelemetryFactory.
|
|
10
|
+
*
|
|
11
|
+
* Unlike `TELEMETRY_ACCESSOR` (CONTEXT-scoped), this factory is resolvable
|
|
12
|
+
* at scope-init time without an active request context. It's intended for
|
|
13
|
+
* scope-lifetime singletons (e.g. `BundleStore`, security guards) that need
|
|
14
|
+
* to create process-global counters / spans before any request arrives.
|
|
15
|
+
*
|
|
16
|
+
* Registered by ObservabilityPlugin when tracing is enabled.
|
|
17
|
+
*/
|
|
18
|
+
export declare const TELEMETRY_FACTORY: unique symbol;
|