@llmops/app 0.6.3-beta.1 → 0.6.5
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/dist/index.cjs +410 -77
- package/dist/index.mjs +409 -77
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -59,6 +59,8 @@ let hono_cors = require("hono/cors");
|
|
|
59
59
|
let node_crypto = require("node:crypto");
|
|
60
60
|
let __llmops_gateway = require("@llmops/gateway");
|
|
61
61
|
__llmops_gateway = __toESM(__llmops_gateway);
|
|
62
|
+
let protobufjs = require("protobufjs");
|
|
63
|
+
protobufjs = __toESM(protobufjs);
|
|
62
64
|
let node_process = require("node:process");
|
|
63
65
|
let __llmops_core_db = require("@llmops/core/db");
|
|
64
66
|
let better_auth = require("better-auth");
|
|
@@ -18287,6 +18289,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
|
|
|
18287
18289
|
const batch = queue;
|
|
18288
18290
|
queue = [];
|
|
18289
18291
|
try {
|
|
18292
|
+
__llmops_core.logger.info(`[TraceBatchWriter] Flushing ${batch.length} items`);
|
|
18290
18293
|
log(`[TraceBatchWriter] Flushing ${batch.length} items`);
|
|
18291
18294
|
const traceMap = /* @__PURE__ */ new Map();
|
|
18292
18295
|
for (const item of batch) {
|
|
@@ -18320,6 +18323,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
|
|
|
18320
18323
|
if (allSpans.length > 0) await deps.batchInsertSpans(allSpans);
|
|
18321
18324
|
const allEvents = batch.flatMap((item) => item.events ?? []);
|
|
18322
18325
|
if (allEvents.length > 0) await deps.batchInsertSpanEvents(allEvents);
|
|
18326
|
+
__llmops_core.logger.info(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
|
|
18323
18327
|
log(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
|
|
18324
18328
|
} catch (error$47) {
|
|
18325
18329
|
const errorMsg = error$47 instanceof Error ? error$47.message : String(error$47);
|
|
@@ -18808,6 +18812,317 @@ app$4.use("*", (0, hono_pretty_json.prettyJSON)()).get("/health", async (c) => {
|
|
|
18808
18812
|
});
|
|
18809
18813
|
var genai_default = app$4;
|
|
18810
18814
|
|
|
18815
|
+
//#endregion
|
|
18816
|
+
//#region src/server/handlers/otlp/decode.ts
|
|
18817
|
+
/**
|
|
18818
|
+
* OTLP Protobuf Decoder
|
|
18819
|
+
*
|
|
18820
|
+
* Decodes binary protobuf ExportTraceServiceRequest into the same JSON
|
|
18821
|
+
* structure the OTLP handler already expects. Uses an embedded protobufjs
|
|
18822
|
+
* JSON descriptor — no .proto files needed at runtime.
|
|
18823
|
+
*/
|
|
18824
|
+
const protoDescriptor = { nested: {
|
|
18825
|
+
ExportTraceServiceRequest: { fields: { resourceSpans: {
|
|
18826
|
+
rule: "repeated",
|
|
18827
|
+
type: "ResourceSpans",
|
|
18828
|
+
id: 1
|
|
18829
|
+
} } },
|
|
18830
|
+
ResourceSpans: { fields: {
|
|
18831
|
+
resource: {
|
|
18832
|
+
type: "Resource",
|
|
18833
|
+
id: 1
|
|
18834
|
+
},
|
|
18835
|
+
scopeSpans: {
|
|
18836
|
+
rule: "repeated",
|
|
18837
|
+
type: "ScopeSpans",
|
|
18838
|
+
id: 2
|
|
18839
|
+
},
|
|
18840
|
+
instrumentationLibrarySpans: {
|
|
18841
|
+
rule: "repeated",
|
|
18842
|
+
type: "ScopeSpans",
|
|
18843
|
+
id: 1e3
|
|
18844
|
+
}
|
|
18845
|
+
} },
|
|
18846
|
+
Resource: { fields: { attributes: {
|
|
18847
|
+
rule: "repeated",
|
|
18848
|
+
type: "KeyValue",
|
|
18849
|
+
id: 1
|
|
18850
|
+
} } },
|
|
18851
|
+
ScopeSpans: { fields: {
|
|
18852
|
+
scope: {
|
|
18853
|
+
type: "InstrumentationScope",
|
|
18854
|
+
id: 1
|
|
18855
|
+
},
|
|
18856
|
+
spans: {
|
|
18857
|
+
rule: "repeated",
|
|
18858
|
+
type: "Span",
|
|
18859
|
+
id: 2
|
|
18860
|
+
}
|
|
18861
|
+
} },
|
|
18862
|
+
InstrumentationScope: { fields: {
|
|
18863
|
+
name: {
|
|
18864
|
+
type: "string",
|
|
18865
|
+
id: 1
|
|
18866
|
+
},
|
|
18867
|
+
version: {
|
|
18868
|
+
type: "string",
|
|
18869
|
+
id: 2
|
|
18870
|
+
}
|
|
18871
|
+
} },
|
|
18872
|
+
Span: { fields: {
|
|
18873
|
+
traceId: {
|
|
18874
|
+
type: "bytes",
|
|
18875
|
+
id: 1
|
|
18876
|
+
},
|
|
18877
|
+
spanId: {
|
|
18878
|
+
type: "bytes",
|
|
18879
|
+
id: 2
|
|
18880
|
+
},
|
|
18881
|
+
traceState: {
|
|
18882
|
+
type: "string",
|
|
18883
|
+
id: 3
|
|
18884
|
+
},
|
|
18885
|
+
parentSpanId: {
|
|
18886
|
+
type: "bytes",
|
|
18887
|
+
id: 4
|
|
18888
|
+
},
|
|
18889
|
+
name: {
|
|
18890
|
+
type: "string",
|
|
18891
|
+
id: 5
|
|
18892
|
+
},
|
|
18893
|
+
kind: {
|
|
18894
|
+
type: "SpanKind",
|
|
18895
|
+
id: 6
|
|
18896
|
+
},
|
|
18897
|
+
startTimeUnixNano: {
|
|
18898
|
+
type: "fixed64",
|
|
18899
|
+
id: 7
|
|
18900
|
+
},
|
|
18901
|
+
endTimeUnixNano: {
|
|
18902
|
+
type: "fixed64",
|
|
18903
|
+
id: 8
|
|
18904
|
+
},
|
|
18905
|
+
attributes: {
|
|
18906
|
+
rule: "repeated",
|
|
18907
|
+
type: "KeyValue",
|
|
18908
|
+
id: 9
|
|
18909
|
+
},
|
|
18910
|
+
events: {
|
|
18911
|
+
rule: "repeated",
|
|
18912
|
+
type: "SpanEvent",
|
|
18913
|
+
id: 10
|
|
18914
|
+
},
|
|
18915
|
+
status: {
|
|
18916
|
+
type: "Status",
|
|
18917
|
+
id: 15
|
|
18918
|
+
}
|
|
18919
|
+
} },
|
|
18920
|
+
SpanKind: { values: {
|
|
18921
|
+
SPAN_KIND_UNSPECIFIED: 0,
|
|
18922
|
+
SPAN_KIND_INTERNAL: 1,
|
|
18923
|
+
SPAN_KIND_SERVER: 2,
|
|
18924
|
+
SPAN_KIND_CLIENT: 3,
|
|
18925
|
+
SPAN_KIND_PRODUCER: 4,
|
|
18926
|
+
SPAN_KIND_CONSUMER: 5
|
|
18927
|
+
} },
|
|
18928
|
+
SpanEvent: { fields: {
|
|
18929
|
+
timeUnixNano: {
|
|
18930
|
+
type: "fixed64",
|
|
18931
|
+
id: 1
|
|
18932
|
+
},
|
|
18933
|
+
name: {
|
|
18934
|
+
type: "string",
|
|
18935
|
+
id: 2
|
|
18936
|
+
},
|
|
18937
|
+
attributes: {
|
|
18938
|
+
rule: "repeated",
|
|
18939
|
+
type: "KeyValue",
|
|
18940
|
+
id: 3
|
|
18941
|
+
}
|
|
18942
|
+
} },
|
|
18943
|
+
Status: { fields: {
|
|
18944
|
+
message: {
|
|
18945
|
+
type: "string",
|
|
18946
|
+
id: 2
|
|
18947
|
+
},
|
|
18948
|
+
code: {
|
|
18949
|
+
type: "StatusCode",
|
|
18950
|
+
id: 3
|
|
18951
|
+
}
|
|
18952
|
+
} },
|
|
18953
|
+
StatusCode: { values: {
|
|
18954
|
+
STATUS_CODE_UNSET: 0,
|
|
18955
|
+
STATUS_CODE_OK: 1,
|
|
18956
|
+
STATUS_CODE_ERROR: 2
|
|
18957
|
+
} },
|
|
18958
|
+
KeyValue: { fields: {
|
|
18959
|
+
key: {
|
|
18960
|
+
type: "string",
|
|
18961
|
+
id: 1
|
|
18962
|
+
},
|
|
18963
|
+
value: {
|
|
18964
|
+
type: "AnyValue",
|
|
18965
|
+
id: 2
|
|
18966
|
+
}
|
|
18967
|
+
} },
|
|
18968
|
+
AnyValue: {
|
|
18969
|
+
fields: {
|
|
18970
|
+
stringValue: {
|
|
18971
|
+
type: "string",
|
|
18972
|
+
id: 1
|
|
18973
|
+
},
|
|
18974
|
+
boolValue: {
|
|
18975
|
+
type: "bool",
|
|
18976
|
+
id: 2
|
|
18977
|
+
},
|
|
18978
|
+
intValue: {
|
|
18979
|
+
type: "int64",
|
|
18980
|
+
id: 3
|
|
18981
|
+
},
|
|
18982
|
+
doubleValue: {
|
|
18983
|
+
type: "double",
|
|
18984
|
+
id: 4
|
|
18985
|
+
},
|
|
18986
|
+
arrayValue: {
|
|
18987
|
+
type: "ArrayValue",
|
|
18988
|
+
id: 5
|
|
18989
|
+
},
|
|
18990
|
+
kvlistValue: {
|
|
18991
|
+
type: "KeyValueList",
|
|
18992
|
+
id: 6
|
|
18993
|
+
},
|
|
18994
|
+
bytesValue: {
|
|
18995
|
+
type: "bytes",
|
|
18996
|
+
id: 7
|
|
18997
|
+
}
|
|
18998
|
+
},
|
|
18999
|
+
oneofs: { value: { oneof: [
|
|
19000
|
+
"stringValue",
|
|
19001
|
+
"boolValue",
|
|
19002
|
+
"intValue",
|
|
19003
|
+
"doubleValue",
|
|
19004
|
+
"arrayValue",
|
|
19005
|
+
"kvlistValue",
|
|
19006
|
+
"bytesValue"
|
|
19007
|
+
] } }
|
|
19008
|
+
},
|
|
19009
|
+
ArrayValue: { fields: { values: {
|
|
19010
|
+
rule: "repeated",
|
|
19011
|
+
type: "AnyValue",
|
|
19012
|
+
id: 1
|
|
19013
|
+
} } },
|
|
19014
|
+
KeyValueList: { fields: { values: {
|
|
19015
|
+
rule: "repeated",
|
|
19016
|
+
type: "KeyValue",
|
|
19017
|
+
id: 1
|
|
19018
|
+
} } }
|
|
19019
|
+
} };
|
|
19020
|
+
let _root = null;
|
|
19021
|
+
let _RequestType = null;
|
|
19022
|
+
function getRequestType() {
|
|
19023
|
+
if (!_RequestType) {
|
|
19024
|
+
_root = protobufjs.default.Root.fromJSON(protoDescriptor);
|
|
19025
|
+
_RequestType = _root.lookupType("ExportTraceServiceRequest");
|
|
19026
|
+
}
|
|
19027
|
+
return _RequestType;
|
|
19028
|
+
}
|
|
19029
|
+
function bytesToHex(bytes) {
|
|
19030
|
+
return Buffer.from(bytes).toString("hex");
|
|
19031
|
+
}
|
|
19032
|
+
function isZeroBytes(bytes) {
|
|
19033
|
+
for (let i = 0; i < bytes.length; i++) if (bytes[i] !== 0) return false;
|
|
19034
|
+
return true;
|
|
19035
|
+
}
|
|
19036
|
+
/**
|
|
19037
|
+
* Convert a protobufjs Long / number / string to a nanosecond string.
|
|
19038
|
+
* protobufjs decodes fixed64 as Long when longs are not forced to number.
|
|
19039
|
+
*/
|
|
19040
|
+
function toNanoString(val) {
|
|
19041
|
+
if (val == null) return "0";
|
|
19042
|
+
if (typeof val === "object" && val !== null && "toString" in val) return val.toString();
|
|
19043
|
+
return String(val);
|
|
19044
|
+
}
|
|
19045
|
+
/**
|
|
19046
|
+
* Normalise an AnyValue from decoded protobuf into the OTLP JSON shape.
|
|
19047
|
+
*/
|
|
19048
|
+
function normaliseAnyValue(av) {
|
|
19049
|
+
const out = {};
|
|
19050
|
+
if (av.stringValue !== void 0 && av.stringValue !== "") out.stringValue = av.stringValue;
|
|
19051
|
+
if (av.boolValue !== void 0 && av.boolValue !== false) out.boolValue = av.boolValue;
|
|
19052
|
+
if (av.intValue !== void 0) {
|
|
19053
|
+
const v = av.intValue;
|
|
19054
|
+
out.intValue = typeof v === "object" && v !== null && "toNumber" in v ? v.toNumber() : Number(v);
|
|
19055
|
+
}
|
|
19056
|
+
if (av.doubleValue !== void 0 && av.doubleValue !== 0) out.doubleValue = av.doubleValue;
|
|
19057
|
+
if (av.arrayValue !== void 0 && av.arrayValue !== null) out.arrayValue = { values: (av.arrayValue.values ?? []).map((v) => normaliseAnyValue(v)) };
|
|
19058
|
+
if (av.kvlistValue !== void 0 && av.kvlistValue !== null) out.kvlistValue = { values: (av.kvlistValue.values ?? []).map((kv) => normaliseKeyValue(kv)) };
|
|
19059
|
+
if (av.bytesValue !== void 0) out.bytesValue = bytesToHex(av.bytesValue);
|
|
19060
|
+
return out;
|
|
19061
|
+
}
|
|
19062
|
+
function normaliseKeyValue(kv) {
|
|
19063
|
+
return {
|
|
19064
|
+
key: kv.key,
|
|
19065
|
+
value: normaliseAnyValue(kv.value ?? {})
|
|
19066
|
+
};
|
|
19067
|
+
}
|
|
19068
|
+
/**
|
|
19069
|
+
* Decode an OTLP protobuf-encoded ExportTraceServiceRequest binary into
|
|
19070
|
+
* the same JSON structure the handler expects.
|
|
19071
|
+
*
|
|
19072
|
+
* - bytes fields (trace_id, span_id, parent_span_id) → hex strings
|
|
19073
|
+
* - fixed64 nano timestamps → string (for nanoToDate())
|
|
19074
|
+
* - Long intValue → number
|
|
19075
|
+
* - deprecated instrumentationLibrarySpans merged into scopeSpans
|
|
19076
|
+
*/
|
|
19077
|
+
function decodeOtlpProtobuf(buffer) {
|
|
19078
|
+
return { resourceSpans: (getRequestType().decode(buffer).resourceSpans ?? []).map((rs) => {
|
|
19079
|
+
const rawResource = rs.resource;
|
|
19080
|
+
return {
|
|
19081
|
+
resource: rawResource ? { attributes: (rawResource.attributes ?? []).map(normaliseKeyValue) } : void 0,
|
|
19082
|
+
scopeSpans: [...rs.scopeSpans ?? [], ...rs.instrumentationLibrarySpans ?? []].map((ss) => {
|
|
19083
|
+
const rawScope = ss.scope;
|
|
19084
|
+
const spans = (ss.spans ?? []).map((s) => {
|
|
19085
|
+
const traceIdBytes = s.traceId;
|
|
19086
|
+
const spanIdBytes = s.spanId;
|
|
19087
|
+
const parentSpanIdBytes = s.parentSpanId;
|
|
19088
|
+
const traceId = traceIdBytes ? bytesToHex(traceIdBytes) : "";
|
|
19089
|
+
const spanId = spanIdBytes ? bytesToHex(spanIdBytes) : "";
|
|
19090
|
+
const parentSpanId = parentSpanIdBytes && parentSpanIdBytes.length > 0 && !isZeroBytes(parentSpanIdBytes) ? bytesToHex(parentSpanIdBytes) : void 0;
|
|
19091
|
+
const attributes = (s.attributes ?? []).map(normaliseKeyValue);
|
|
19092
|
+
const events = (s.events ?? []).map((e) => ({
|
|
19093
|
+
name: e.name ?? "",
|
|
19094
|
+
timeUnixNano: toNanoString(e.timeUnixNano),
|
|
19095
|
+
attributes: (e.attributes ?? []).map(normaliseKeyValue)
|
|
19096
|
+
}));
|
|
19097
|
+
const rawStatus = s.status;
|
|
19098
|
+
return {
|
|
19099
|
+
traceId,
|
|
19100
|
+
spanId,
|
|
19101
|
+
parentSpanId,
|
|
19102
|
+
name: s.name ?? "",
|
|
19103
|
+
kind: s.kind,
|
|
19104
|
+
startTimeUnixNano: toNanoString(s.startTimeUnixNano),
|
|
19105
|
+
endTimeUnixNano: s.endTimeUnixNano ? toNanoString(s.endTimeUnixNano) : void 0,
|
|
19106
|
+
attributes,
|
|
19107
|
+
events: events.length > 0 ? events : void 0,
|
|
19108
|
+
status: rawStatus ? {
|
|
19109
|
+
code: rawStatus.code,
|
|
19110
|
+
message: rawStatus.message
|
|
19111
|
+
} : void 0
|
|
19112
|
+
};
|
|
19113
|
+
});
|
|
19114
|
+
return {
|
|
19115
|
+
scope: rawScope ? {
|
|
19116
|
+
name: rawScope.name,
|
|
19117
|
+
version: rawScope.version
|
|
19118
|
+
} : void 0,
|
|
19119
|
+
spans
|
|
19120
|
+
};
|
|
19121
|
+
})
|
|
19122
|
+
};
|
|
19123
|
+
}) };
|
|
19124
|
+
}
|
|
19125
|
+
|
|
18811
19126
|
//#endregion
|
|
18812
19127
|
//#region src/server/handlers/otlp/index.ts
|
|
18813
19128
|
/**
|
|
@@ -18876,9 +19191,10 @@ function extractTypedFields(attrs) {
|
|
|
18876
19191
|
const pricingProvider = (0, __llmops_core.getDefaultPricingProvider)();
|
|
18877
19192
|
/**
|
|
18878
19193
|
* OTLP ingestion endpoint
|
|
18879
|
-
* Accepts OTLP JSON (ExportTraceServiceRequest)
|
|
19194
|
+
* Accepts OTLP JSON and Protobuf (ExportTraceServiceRequest) formats
|
|
18880
19195
|
*/
|
|
18881
19196
|
const app$3 = new hono.Hono().post("/v1/traces", async (c) => {
|
|
19197
|
+
__llmops_core.logger.info(`[OTLP] POST /v1/traces hit — url: ${c.req.url}`);
|
|
18882
19198
|
const authHeader = c.req.header("authorization");
|
|
18883
19199
|
if (!authHeader) return c.json({ error: "Authorization header required" }, 401);
|
|
18884
19200
|
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
@@ -18886,11 +19202,20 @@ const app$3 = new hono.Hono().post("/v1/traces", async (c) => {
|
|
|
18886
19202
|
const envSec = match[1].trim();
|
|
18887
19203
|
c.set("envSec", envSec);
|
|
18888
19204
|
let body;
|
|
19205
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
19206
|
+
__llmops_core.logger.info(`[OTLP] Received request — Content-Type: ${contentType}`);
|
|
18889
19207
|
try {
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
19208
|
+
if (contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf")) {
|
|
19209
|
+
const buffer = await c.req.arrayBuffer();
|
|
19210
|
+
__llmops_core.logger.info(`[OTLP] Protobuf body size: ${buffer.byteLength} bytes`);
|
|
19211
|
+
body = decodeOtlpProtobuf(new Uint8Array(buffer));
|
|
19212
|
+
} else body = await c.req.json();
|
|
19213
|
+
} catch (e) {
|
|
19214
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19215
|
+
__llmops_core.logger.error(`[OTLP] Failed to parse request body: ${msg}`);
|
|
19216
|
+
return c.json({ error: "Invalid request body" }, 400);
|
|
19217
|
+
}
|
|
19218
|
+
__llmops_core.logger.info(`[OTLP] Parsed body — resourceSpans count: ${body.resourceSpans?.length ?? 0}`);
|
|
18894
19219
|
if (!body.resourceSpans || !Array.isArray(body.resourceSpans)) return c.json({ error: "Missing resourceSpans" }, 400);
|
|
18895
19220
|
const db = c.get("db");
|
|
18896
19221
|
if (!db) return c.json({ error: "Database not configured" }, 503);
|
|
@@ -18903,82 +19228,90 @@ const app$3 = new hono.Hono().post("/v1/traces", async (c) => {
|
|
|
18903
19228
|
try {
|
|
18904
19229
|
for (const resourceSpan of body.resourceSpans) {
|
|
18905
19230
|
const resourceAttrs = attributesToRecord(resourceSpan.resource?.attributes);
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
const
|
|
18920
|
-
|
|
19231
|
+
__llmops_core.logger.info(`[OTLP] resourceSpan — scopeSpans count: ${resourceSpan.scopeSpans?.length ?? 0}, resource attrs: ${JSON.stringify(resourceAttrs)}`);
|
|
19232
|
+
for (const scopeSpan of resourceSpan.scopeSpans) {
|
|
19233
|
+
__llmops_core.logger.info(`[OTLP] scopeSpan — scope: ${scopeSpan.scope?.name ?? "<none>"}, spans count: ${scopeSpan.spans?.length ?? 0}`);
|
|
19234
|
+
for (const otlpSpan of scopeSpan.spans) {
|
|
19235
|
+
__llmops_core.logger.info(`[OTLP] raw span — traceId: ${otlpSpan.traceId}, spanId: ${otlpSpan.spanId}, parentSpanId: ${otlpSpan.parentSpanId ?? "null"}, name: ${otlpSpan.name}, startTimeUnixNano: ${otlpSpan.startTimeUnixNano}, endTimeUnixNano: ${otlpSpan.endTimeUnixNano ?? "null"}`);
|
|
19236
|
+
const allAttrs = {
|
|
19237
|
+
...resourceAttrs,
|
|
19238
|
+
...attributesToRecord(otlpSpan.attributes)
|
|
19239
|
+
};
|
|
19240
|
+
const typed = extractTypedFields(allAttrs);
|
|
19241
|
+
const startTime = nanoToDate(otlpSpan.startTimeUnixNano);
|
|
19242
|
+
const endTime = otlpSpan.endTimeUnixNano ? nanoToDate(otlpSpan.endTimeUnixNano) : null;
|
|
19243
|
+
const durationMs = endTime ? endTime.getTime() - startTime.getTime() : null;
|
|
19244
|
+
const spanStatus = otlpSpan.status?.code ?? 0;
|
|
19245
|
+
const traceStatus = spanStatus === 2 ? "error" : spanStatus === 1 ? "ok" : "unset";
|
|
19246
|
+
let cost = 0;
|
|
19247
|
+
if (typed.provider && typed.model && (typed.promptTokens > 0 || typed.completionTokens > 0)) try {
|
|
19248
|
+
const pricing = await pricingProvider.getModelPricing(typed.provider, typed.model);
|
|
19249
|
+
if (pricing) cost = (0, __llmops_core.calculateCacheAwareCost)({
|
|
19250
|
+
promptTokens: typed.promptTokens,
|
|
19251
|
+
completionTokens: typed.completionTokens
|
|
19252
|
+
}, pricing, typed.provider).totalCost;
|
|
19253
|
+
} catch (e) {
|
|
19254
|
+
__llmops_core.logger.info(`[OTLP] Failed to calculate cost for ${typed.provider}/${typed.model}: ${e instanceof Error ? e.message : String(e)}`);
|
|
19255
|
+
}
|
|
19256
|
+
const spanData = {
|
|
19257
|
+
traceId: otlpSpan.traceId,
|
|
19258
|
+
spanId: otlpSpan.spanId,
|
|
19259
|
+
parentSpanId: otlpSpan.parentSpanId || null,
|
|
19260
|
+
name: otlpSpan.name,
|
|
19261
|
+
kind: otlpSpan.kind ?? 1,
|
|
19262
|
+
status: spanStatus,
|
|
19263
|
+
statusMessage: otlpSpan.status?.message ?? null,
|
|
19264
|
+
startTime,
|
|
19265
|
+
endTime,
|
|
19266
|
+
durationMs,
|
|
19267
|
+
provider: typed.provider,
|
|
19268
|
+
model: typed.model,
|
|
18921
19269
|
promptTokens: typed.promptTokens,
|
|
18922
|
-
completionTokens: typed.completionTokens
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
|
|
19270
|
+
completionTokens: typed.completionTokens,
|
|
19271
|
+
totalTokens: typed.totalTokens,
|
|
19272
|
+
cost,
|
|
19273
|
+
source: "otlp",
|
|
19274
|
+
input: typed.input,
|
|
19275
|
+
output: typed.output,
|
|
19276
|
+
attributes: allAttrs
|
|
19277
|
+
};
|
|
19278
|
+
const spanEvents = (otlpSpan.events ?? []).map((event) => ({
|
|
19279
|
+
traceId: otlpSpan.traceId,
|
|
19280
|
+
spanId: otlpSpan.spanId,
|
|
19281
|
+
name: event.name,
|
|
19282
|
+
timestamp: nanoToDate(event.timeUnixNano),
|
|
19283
|
+
attributes: attributesToRecord(event.attributes)
|
|
19284
|
+
}));
|
|
19285
|
+
const traceData = {
|
|
19286
|
+
traceId: otlpSpan.traceId,
|
|
19287
|
+
name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
|
|
19288
|
+
sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
|
|
19289
|
+
userId: null,
|
|
19290
|
+
status: traceStatus,
|
|
19291
|
+
startTime,
|
|
19292
|
+
endTime,
|
|
19293
|
+
durationMs,
|
|
19294
|
+
spanCount: 1,
|
|
19295
|
+
totalInputTokens: typed.promptTokens,
|
|
19296
|
+
totalOutputTokens: typed.completionTokens,
|
|
19297
|
+
totalTokens: typed.totalTokens,
|
|
19298
|
+
totalCost: cost,
|
|
19299
|
+
tags: {},
|
|
19300
|
+
metadata: {}
|
|
19301
|
+
};
|
|
19302
|
+
__llmops_core.logger.info(`[OTLP] spanData — traceId: ${spanData.traceId}, spanId: ${spanData.spanId}, parentSpanId: ${spanData.parentSpanId ?? "null"}, name: ${spanData.name}, source: ${spanData.source}, startTime: ${spanData.startTime.toISOString()}, endTime: ${spanData.endTime?.toISOString() ?? "null"}, durationMs: ${spanData.durationMs}`);
|
|
19303
|
+
__llmops_core.logger.info(`[OTLP] traceData — traceId: ${traceData.traceId}, name: ${traceData.name ?? "null"}, status: ${traceData.status}, spanCount: ${traceData.spanCount}`);
|
|
19304
|
+
const item = {
|
|
19305
|
+
span: spanData,
|
|
19306
|
+
events: spanEvents.length > 0 ? spanEvents : void 0,
|
|
19307
|
+
trace: traceData
|
|
19308
|
+
};
|
|
19309
|
+
traceBatchWriter.enqueue(item);
|
|
19310
|
+
spanCount++;
|
|
18926
19311
|
}
|
|
18927
|
-
const spanData = {
|
|
18928
|
-
traceId: otlpSpan.traceId,
|
|
18929
|
-
spanId: otlpSpan.spanId,
|
|
18930
|
-
parentSpanId: otlpSpan.parentSpanId || null,
|
|
18931
|
-
name: otlpSpan.name,
|
|
18932
|
-
kind: otlpSpan.kind ?? 1,
|
|
18933
|
-
status: spanStatus,
|
|
18934
|
-
statusMessage: otlpSpan.status?.message ?? null,
|
|
18935
|
-
startTime,
|
|
18936
|
-
endTime,
|
|
18937
|
-
durationMs,
|
|
18938
|
-
provider: typed.provider,
|
|
18939
|
-
model: typed.model,
|
|
18940
|
-
promptTokens: typed.promptTokens,
|
|
18941
|
-
completionTokens: typed.completionTokens,
|
|
18942
|
-
totalTokens: typed.totalTokens,
|
|
18943
|
-
cost,
|
|
18944
|
-
source: "otlp",
|
|
18945
|
-
input: typed.input,
|
|
18946
|
-
output: typed.output,
|
|
18947
|
-
attributes: allAttrs
|
|
18948
|
-
};
|
|
18949
|
-
const spanEvents = (otlpSpan.events ?? []).map((event) => ({
|
|
18950
|
-
traceId: otlpSpan.traceId,
|
|
18951
|
-
spanId: otlpSpan.spanId,
|
|
18952
|
-
name: event.name,
|
|
18953
|
-
timestamp: nanoToDate(event.timeUnixNano),
|
|
18954
|
-
attributes: attributesToRecord(event.attributes)
|
|
18955
|
-
}));
|
|
18956
|
-
const traceData = {
|
|
18957
|
-
traceId: otlpSpan.traceId,
|
|
18958
|
-
name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
|
|
18959
|
-
sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
|
|
18960
|
-
userId: null,
|
|
18961
|
-
status: traceStatus,
|
|
18962
|
-
startTime,
|
|
18963
|
-
endTime,
|
|
18964
|
-
durationMs,
|
|
18965
|
-
spanCount: 1,
|
|
18966
|
-
totalInputTokens: typed.promptTokens,
|
|
18967
|
-
totalOutputTokens: typed.completionTokens,
|
|
18968
|
-
totalTokens: typed.totalTokens,
|
|
18969
|
-
totalCost: cost,
|
|
18970
|
-
tags: {},
|
|
18971
|
-
metadata: {}
|
|
18972
|
-
};
|
|
18973
|
-
const item = {
|
|
18974
|
-
span: spanData,
|
|
18975
|
-
events: spanEvents.length > 0 ? spanEvents : void 0,
|
|
18976
|
-
trace: traceData
|
|
18977
|
-
};
|
|
18978
|
-
traceBatchWriter.enqueue(item);
|
|
18979
|
-
spanCount++;
|
|
18980
19312
|
}
|
|
18981
19313
|
}
|
|
19314
|
+
__llmops_core.logger.info(`[OTLP] Enqueued ${spanCount} spans total`);
|
|
18982
19315
|
return c.json({ partialSuccess: {} });
|
|
18983
19316
|
} catch (error$47) {
|
|
18984
19317
|
const msg = error$47 instanceof Error ? error$47.message : String(error$47);
|
package/dist/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { prettyJSON } from "hono/pretty-json";
|
|
|
14
14
|
import { cors } from "hono/cors";
|
|
15
15
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
16
16
|
import gateway from "@llmops/gateway";
|
|
17
|
+
import protobuf from "protobufjs";
|
|
17
18
|
import { env } from "node:process";
|
|
18
19
|
import { createDatabaseFromConnection, detectDatabaseType } from "@llmops/core/db";
|
|
19
20
|
import { betterAuth } from "better-auth";
|
|
@@ -18259,6 +18260,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
|
|
|
18259
18260
|
const batch = queue;
|
|
18260
18261
|
queue = [];
|
|
18261
18262
|
try {
|
|
18263
|
+
logger.info(`[TraceBatchWriter] Flushing ${batch.length} items`);
|
|
18262
18264
|
log(`[TraceBatchWriter] Flushing ${batch.length} items`);
|
|
18263
18265
|
const traceMap = /* @__PURE__ */ new Map();
|
|
18264
18266
|
for (const item of batch) {
|
|
@@ -18292,6 +18294,7 @@ function createTraceBatchWriter(deps, config$1 = {}) {
|
|
|
18292
18294
|
if (allSpans.length > 0) await deps.batchInsertSpans(allSpans);
|
|
18293
18295
|
const allEvents = batch.flatMap((item) => item.events ?? []);
|
|
18294
18296
|
if (allEvents.length > 0) await deps.batchInsertSpanEvents(allEvents);
|
|
18297
|
+
logger.info(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
|
|
18295
18298
|
log(`[TraceBatchWriter] Flushed ${traceMap.size} traces, ${allSpans.length} spans, ${allEvents.length} events`);
|
|
18296
18299
|
} catch (error$47) {
|
|
18297
18300
|
const errorMsg = error$47 instanceof Error ? error$47.message : String(error$47);
|
|
@@ -18780,6 +18783,317 @@ app$4.use("*", prettyJSON()).get("/health", async (c) => {
|
|
|
18780
18783
|
});
|
|
18781
18784
|
var genai_default = app$4;
|
|
18782
18785
|
|
|
18786
|
+
//#endregion
|
|
18787
|
+
//#region src/server/handlers/otlp/decode.ts
|
|
18788
|
+
/**
|
|
18789
|
+
* OTLP Protobuf Decoder
|
|
18790
|
+
*
|
|
18791
|
+
* Decodes binary protobuf ExportTraceServiceRequest into the same JSON
|
|
18792
|
+
* structure the OTLP handler already expects. Uses an embedded protobufjs
|
|
18793
|
+
* JSON descriptor — no .proto files needed at runtime.
|
|
18794
|
+
*/
|
|
18795
|
+
const protoDescriptor = { nested: {
|
|
18796
|
+
ExportTraceServiceRequest: { fields: { resourceSpans: {
|
|
18797
|
+
rule: "repeated",
|
|
18798
|
+
type: "ResourceSpans",
|
|
18799
|
+
id: 1
|
|
18800
|
+
} } },
|
|
18801
|
+
ResourceSpans: { fields: {
|
|
18802
|
+
resource: {
|
|
18803
|
+
type: "Resource",
|
|
18804
|
+
id: 1
|
|
18805
|
+
},
|
|
18806
|
+
scopeSpans: {
|
|
18807
|
+
rule: "repeated",
|
|
18808
|
+
type: "ScopeSpans",
|
|
18809
|
+
id: 2
|
|
18810
|
+
},
|
|
18811
|
+
instrumentationLibrarySpans: {
|
|
18812
|
+
rule: "repeated",
|
|
18813
|
+
type: "ScopeSpans",
|
|
18814
|
+
id: 1e3
|
|
18815
|
+
}
|
|
18816
|
+
} },
|
|
18817
|
+
Resource: { fields: { attributes: {
|
|
18818
|
+
rule: "repeated",
|
|
18819
|
+
type: "KeyValue",
|
|
18820
|
+
id: 1
|
|
18821
|
+
} } },
|
|
18822
|
+
ScopeSpans: { fields: {
|
|
18823
|
+
scope: {
|
|
18824
|
+
type: "InstrumentationScope",
|
|
18825
|
+
id: 1
|
|
18826
|
+
},
|
|
18827
|
+
spans: {
|
|
18828
|
+
rule: "repeated",
|
|
18829
|
+
type: "Span",
|
|
18830
|
+
id: 2
|
|
18831
|
+
}
|
|
18832
|
+
} },
|
|
18833
|
+
InstrumentationScope: { fields: {
|
|
18834
|
+
name: {
|
|
18835
|
+
type: "string",
|
|
18836
|
+
id: 1
|
|
18837
|
+
},
|
|
18838
|
+
version: {
|
|
18839
|
+
type: "string",
|
|
18840
|
+
id: 2
|
|
18841
|
+
}
|
|
18842
|
+
} },
|
|
18843
|
+
Span: { fields: {
|
|
18844
|
+
traceId: {
|
|
18845
|
+
type: "bytes",
|
|
18846
|
+
id: 1
|
|
18847
|
+
},
|
|
18848
|
+
spanId: {
|
|
18849
|
+
type: "bytes",
|
|
18850
|
+
id: 2
|
|
18851
|
+
},
|
|
18852
|
+
traceState: {
|
|
18853
|
+
type: "string",
|
|
18854
|
+
id: 3
|
|
18855
|
+
},
|
|
18856
|
+
parentSpanId: {
|
|
18857
|
+
type: "bytes",
|
|
18858
|
+
id: 4
|
|
18859
|
+
},
|
|
18860
|
+
name: {
|
|
18861
|
+
type: "string",
|
|
18862
|
+
id: 5
|
|
18863
|
+
},
|
|
18864
|
+
kind: {
|
|
18865
|
+
type: "SpanKind",
|
|
18866
|
+
id: 6
|
|
18867
|
+
},
|
|
18868
|
+
startTimeUnixNano: {
|
|
18869
|
+
type: "fixed64",
|
|
18870
|
+
id: 7
|
|
18871
|
+
},
|
|
18872
|
+
endTimeUnixNano: {
|
|
18873
|
+
type: "fixed64",
|
|
18874
|
+
id: 8
|
|
18875
|
+
},
|
|
18876
|
+
attributes: {
|
|
18877
|
+
rule: "repeated",
|
|
18878
|
+
type: "KeyValue",
|
|
18879
|
+
id: 9
|
|
18880
|
+
},
|
|
18881
|
+
events: {
|
|
18882
|
+
rule: "repeated",
|
|
18883
|
+
type: "SpanEvent",
|
|
18884
|
+
id: 10
|
|
18885
|
+
},
|
|
18886
|
+
status: {
|
|
18887
|
+
type: "Status",
|
|
18888
|
+
id: 15
|
|
18889
|
+
}
|
|
18890
|
+
} },
|
|
18891
|
+
SpanKind: { values: {
|
|
18892
|
+
SPAN_KIND_UNSPECIFIED: 0,
|
|
18893
|
+
SPAN_KIND_INTERNAL: 1,
|
|
18894
|
+
SPAN_KIND_SERVER: 2,
|
|
18895
|
+
SPAN_KIND_CLIENT: 3,
|
|
18896
|
+
SPAN_KIND_PRODUCER: 4,
|
|
18897
|
+
SPAN_KIND_CONSUMER: 5
|
|
18898
|
+
} },
|
|
18899
|
+
SpanEvent: { fields: {
|
|
18900
|
+
timeUnixNano: {
|
|
18901
|
+
type: "fixed64",
|
|
18902
|
+
id: 1
|
|
18903
|
+
},
|
|
18904
|
+
name: {
|
|
18905
|
+
type: "string",
|
|
18906
|
+
id: 2
|
|
18907
|
+
},
|
|
18908
|
+
attributes: {
|
|
18909
|
+
rule: "repeated",
|
|
18910
|
+
type: "KeyValue",
|
|
18911
|
+
id: 3
|
|
18912
|
+
}
|
|
18913
|
+
} },
|
|
18914
|
+
Status: { fields: {
|
|
18915
|
+
message: {
|
|
18916
|
+
type: "string",
|
|
18917
|
+
id: 2
|
|
18918
|
+
},
|
|
18919
|
+
code: {
|
|
18920
|
+
type: "StatusCode",
|
|
18921
|
+
id: 3
|
|
18922
|
+
}
|
|
18923
|
+
} },
|
|
18924
|
+
StatusCode: { values: {
|
|
18925
|
+
STATUS_CODE_UNSET: 0,
|
|
18926
|
+
STATUS_CODE_OK: 1,
|
|
18927
|
+
STATUS_CODE_ERROR: 2
|
|
18928
|
+
} },
|
|
18929
|
+
KeyValue: { fields: {
|
|
18930
|
+
key: {
|
|
18931
|
+
type: "string",
|
|
18932
|
+
id: 1
|
|
18933
|
+
},
|
|
18934
|
+
value: {
|
|
18935
|
+
type: "AnyValue",
|
|
18936
|
+
id: 2
|
|
18937
|
+
}
|
|
18938
|
+
} },
|
|
18939
|
+
AnyValue: {
|
|
18940
|
+
fields: {
|
|
18941
|
+
stringValue: {
|
|
18942
|
+
type: "string",
|
|
18943
|
+
id: 1
|
|
18944
|
+
},
|
|
18945
|
+
boolValue: {
|
|
18946
|
+
type: "bool",
|
|
18947
|
+
id: 2
|
|
18948
|
+
},
|
|
18949
|
+
intValue: {
|
|
18950
|
+
type: "int64",
|
|
18951
|
+
id: 3
|
|
18952
|
+
},
|
|
18953
|
+
doubleValue: {
|
|
18954
|
+
type: "double",
|
|
18955
|
+
id: 4
|
|
18956
|
+
},
|
|
18957
|
+
arrayValue: {
|
|
18958
|
+
type: "ArrayValue",
|
|
18959
|
+
id: 5
|
|
18960
|
+
},
|
|
18961
|
+
kvlistValue: {
|
|
18962
|
+
type: "KeyValueList",
|
|
18963
|
+
id: 6
|
|
18964
|
+
},
|
|
18965
|
+
bytesValue: {
|
|
18966
|
+
type: "bytes",
|
|
18967
|
+
id: 7
|
|
18968
|
+
}
|
|
18969
|
+
},
|
|
18970
|
+
oneofs: { value: { oneof: [
|
|
18971
|
+
"stringValue",
|
|
18972
|
+
"boolValue",
|
|
18973
|
+
"intValue",
|
|
18974
|
+
"doubleValue",
|
|
18975
|
+
"arrayValue",
|
|
18976
|
+
"kvlistValue",
|
|
18977
|
+
"bytesValue"
|
|
18978
|
+
] } }
|
|
18979
|
+
},
|
|
18980
|
+
ArrayValue: { fields: { values: {
|
|
18981
|
+
rule: "repeated",
|
|
18982
|
+
type: "AnyValue",
|
|
18983
|
+
id: 1
|
|
18984
|
+
} } },
|
|
18985
|
+
KeyValueList: { fields: { values: {
|
|
18986
|
+
rule: "repeated",
|
|
18987
|
+
type: "KeyValue",
|
|
18988
|
+
id: 1
|
|
18989
|
+
} } }
|
|
18990
|
+
} };
|
|
18991
|
+
let _root = null;
|
|
18992
|
+
let _RequestType = null;
|
|
18993
|
+
function getRequestType() {
|
|
18994
|
+
if (!_RequestType) {
|
|
18995
|
+
_root = protobuf.Root.fromJSON(protoDescriptor);
|
|
18996
|
+
_RequestType = _root.lookupType("ExportTraceServiceRequest");
|
|
18997
|
+
}
|
|
18998
|
+
return _RequestType;
|
|
18999
|
+
}
|
|
19000
|
+
function bytesToHex(bytes) {
|
|
19001
|
+
return Buffer.from(bytes).toString("hex");
|
|
19002
|
+
}
|
|
19003
|
+
function isZeroBytes(bytes) {
|
|
19004
|
+
for (let i = 0; i < bytes.length; i++) if (bytes[i] !== 0) return false;
|
|
19005
|
+
return true;
|
|
19006
|
+
}
|
|
19007
|
+
/**
|
|
19008
|
+
* Convert a protobufjs Long / number / string to a nanosecond string.
|
|
19009
|
+
* protobufjs decodes fixed64 as Long when longs are not forced to number.
|
|
19010
|
+
*/
|
|
19011
|
+
function toNanoString(val) {
|
|
19012
|
+
if (val == null) return "0";
|
|
19013
|
+
if (typeof val === "object" && val !== null && "toString" in val) return val.toString();
|
|
19014
|
+
return String(val);
|
|
19015
|
+
}
|
|
19016
|
+
/**
|
|
19017
|
+
* Normalise an AnyValue from decoded protobuf into the OTLP JSON shape.
|
|
19018
|
+
*/
|
|
19019
|
+
function normaliseAnyValue(av) {
|
|
19020
|
+
const out = {};
|
|
19021
|
+
if (av.stringValue !== void 0 && av.stringValue !== "") out.stringValue = av.stringValue;
|
|
19022
|
+
if (av.boolValue !== void 0 && av.boolValue !== false) out.boolValue = av.boolValue;
|
|
19023
|
+
if (av.intValue !== void 0) {
|
|
19024
|
+
const v = av.intValue;
|
|
19025
|
+
out.intValue = typeof v === "object" && v !== null && "toNumber" in v ? v.toNumber() : Number(v);
|
|
19026
|
+
}
|
|
19027
|
+
if (av.doubleValue !== void 0 && av.doubleValue !== 0) out.doubleValue = av.doubleValue;
|
|
19028
|
+
if (av.arrayValue !== void 0 && av.arrayValue !== null) out.arrayValue = { values: (av.arrayValue.values ?? []).map((v) => normaliseAnyValue(v)) };
|
|
19029
|
+
if (av.kvlistValue !== void 0 && av.kvlistValue !== null) out.kvlistValue = { values: (av.kvlistValue.values ?? []).map((kv) => normaliseKeyValue(kv)) };
|
|
19030
|
+
if (av.bytesValue !== void 0) out.bytesValue = bytesToHex(av.bytesValue);
|
|
19031
|
+
return out;
|
|
19032
|
+
}
|
|
19033
|
+
function normaliseKeyValue(kv) {
|
|
19034
|
+
return {
|
|
19035
|
+
key: kv.key,
|
|
19036
|
+
value: normaliseAnyValue(kv.value ?? {})
|
|
19037
|
+
};
|
|
19038
|
+
}
|
|
19039
|
+
/**
|
|
19040
|
+
* Decode an OTLP protobuf-encoded ExportTraceServiceRequest binary into
|
|
19041
|
+
* the same JSON structure the handler expects.
|
|
19042
|
+
*
|
|
19043
|
+
* - bytes fields (trace_id, span_id, parent_span_id) → hex strings
|
|
19044
|
+
* - fixed64 nano timestamps → string (for nanoToDate())
|
|
19045
|
+
* - Long intValue → number
|
|
19046
|
+
* - deprecated instrumentationLibrarySpans merged into scopeSpans
|
|
19047
|
+
*/
|
|
19048
|
+
function decodeOtlpProtobuf(buffer) {
|
|
19049
|
+
return { resourceSpans: (getRequestType().decode(buffer).resourceSpans ?? []).map((rs) => {
|
|
19050
|
+
const rawResource = rs.resource;
|
|
19051
|
+
return {
|
|
19052
|
+
resource: rawResource ? { attributes: (rawResource.attributes ?? []).map(normaliseKeyValue) } : void 0,
|
|
19053
|
+
scopeSpans: [...rs.scopeSpans ?? [], ...rs.instrumentationLibrarySpans ?? []].map((ss) => {
|
|
19054
|
+
const rawScope = ss.scope;
|
|
19055
|
+
const spans = (ss.spans ?? []).map((s) => {
|
|
19056
|
+
const traceIdBytes = s.traceId;
|
|
19057
|
+
const spanIdBytes = s.spanId;
|
|
19058
|
+
const parentSpanIdBytes = s.parentSpanId;
|
|
19059
|
+
const traceId = traceIdBytes ? bytesToHex(traceIdBytes) : "";
|
|
19060
|
+
const spanId = spanIdBytes ? bytesToHex(spanIdBytes) : "";
|
|
19061
|
+
const parentSpanId = parentSpanIdBytes && parentSpanIdBytes.length > 0 && !isZeroBytes(parentSpanIdBytes) ? bytesToHex(parentSpanIdBytes) : void 0;
|
|
19062
|
+
const attributes = (s.attributes ?? []).map(normaliseKeyValue);
|
|
19063
|
+
const events = (s.events ?? []).map((e) => ({
|
|
19064
|
+
name: e.name ?? "",
|
|
19065
|
+
timeUnixNano: toNanoString(e.timeUnixNano),
|
|
19066
|
+
attributes: (e.attributes ?? []).map(normaliseKeyValue)
|
|
19067
|
+
}));
|
|
19068
|
+
const rawStatus = s.status;
|
|
19069
|
+
return {
|
|
19070
|
+
traceId,
|
|
19071
|
+
spanId,
|
|
19072
|
+
parentSpanId,
|
|
19073
|
+
name: s.name ?? "",
|
|
19074
|
+
kind: s.kind,
|
|
19075
|
+
startTimeUnixNano: toNanoString(s.startTimeUnixNano),
|
|
19076
|
+
endTimeUnixNano: s.endTimeUnixNano ? toNanoString(s.endTimeUnixNano) : void 0,
|
|
19077
|
+
attributes,
|
|
19078
|
+
events: events.length > 0 ? events : void 0,
|
|
19079
|
+
status: rawStatus ? {
|
|
19080
|
+
code: rawStatus.code,
|
|
19081
|
+
message: rawStatus.message
|
|
19082
|
+
} : void 0
|
|
19083
|
+
};
|
|
19084
|
+
});
|
|
19085
|
+
return {
|
|
19086
|
+
scope: rawScope ? {
|
|
19087
|
+
name: rawScope.name,
|
|
19088
|
+
version: rawScope.version
|
|
19089
|
+
} : void 0,
|
|
19090
|
+
spans
|
|
19091
|
+
};
|
|
19092
|
+
})
|
|
19093
|
+
};
|
|
19094
|
+
}) };
|
|
19095
|
+
}
|
|
19096
|
+
|
|
18783
19097
|
//#endregion
|
|
18784
19098
|
//#region src/server/handlers/otlp/index.ts
|
|
18785
19099
|
/**
|
|
@@ -18848,9 +19162,10 @@ function extractTypedFields(attrs) {
|
|
|
18848
19162
|
const pricingProvider = getDefaultPricingProvider();
|
|
18849
19163
|
/**
|
|
18850
19164
|
* OTLP ingestion endpoint
|
|
18851
|
-
* Accepts OTLP JSON (ExportTraceServiceRequest)
|
|
19165
|
+
* Accepts OTLP JSON and Protobuf (ExportTraceServiceRequest) formats
|
|
18852
19166
|
*/
|
|
18853
19167
|
const app$3 = new Hono().post("/v1/traces", async (c) => {
|
|
19168
|
+
logger.info(`[OTLP] POST /v1/traces hit — url: ${c.req.url}`);
|
|
18854
19169
|
const authHeader = c.req.header("authorization");
|
|
18855
19170
|
if (!authHeader) return c.json({ error: "Authorization header required" }, 401);
|
|
18856
19171
|
const match = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
@@ -18858,11 +19173,20 @@ const app$3 = new Hono().post("/v1/traces", async (c) => {
|
|
|
18858
19173
|
const envSec = match[1].trim();
|
|
18859
19174
|
c.set("envSec", envSec);
|
|
18860
19175
|
let body;
|
|
19176
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
19177
|
+
logger.info(`[OTLP] Received request — Content-Type: ${contentType}`);
|
|
18861
19178
|
try {
|
|
18862
|
-
|
|
18863
|
-
|
|
18864
|
-
|
|
18865
|
-
|
|
19179
|
+
if (contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf")) {
|
|
19180
|
+
const buffer = await c.req.arrayBuffer();
|
|
19181
|
+
logger.info(`[OTLP] Protobuf body size: ${buffer.byteLength} bytes`);
|
|
19182
|
+
body = decodeOtlpProtobuf(new Uint8Array(buffer));
|
|
19183
|
+
} else body = await c.req.json();
|
|
19184
|
+
} catch (e) {
|
|
19185
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19186
|
+
logger.error(`[OTLP] Failed to parse request body: ${msg}`);
|
|
19187
|
+
return c.json({ error: "Invalid request body" }, 400);
|
|
19188
|
+
}
|
|
19189
|
+
logger.info(`[OTLP] Parsed body — resourceSpans count: ${body.resourceSpans?.length ?? 0}`);
|
|
18866
19190
|
if (!body.resourceSpans || !Array.isArray(body.resourceSpans)) return c.json({ error: "Missing resourceSpans" }, 400);
|
|
18867
19191
|
const db = c.get("db");
|
|
18868
19192
|
if (!db) return c.json({ error: "Database not configured" }, 503);
|
|
@@ -18875,82 +19199,90 @@ const app$3 = new Hono().post("/v1/traces", async (c) => {
|
|
|
18875
19199
|
try {
|
|
18876
19200
|
for (const resourceSpan of body.resourceSpans) {
|
|
18877
19201
|
const resourceAttrs = attributesToRecord(resourceSpan.resource?.attributes);
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
|
|
18885
|
-
|
|
18886
|
-
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
const
|
|
18892
|
-
|
|
19202
|
+
logger.info(`[OTLP] resourceSpan — scopeSpans count: ${resourceSpan.scopeSpans?.length ?? 0}, resource attrs: ${JSON.stringify(resourceAttrs)}`);
|
|
19203
|
+
for (const scopeSpan of resourceSpan.scopeSpans) {
|
|
19204
|
+
logger.info(`[OTLP] scopeSpan — scope: ${scopeSpan.scope?.name ?? "<none>"}, spans count: ${scopeSpan.spans?.length ?? 0}`);
|
|
19205
|
+
for (const otlpSpan of scopeSpan.spans) {
|
|
19206
|
+
logger.info(`[OTLP] raw span — traceId: ${otlpSpan.traceId}, spanId: ${otlpSpan.spanId}, parentSpanId: ${otlpSpan.parentSpanId ?? "null"}, name: ${otlpSpan.name}, startTimeUnixNano: ${otlpSpan.startTimeUnixNano}, endTimeUnixNano: ${otlpSpan.endTimeUnixNano ?? "null"}`);
|
|
19207
|
+
const allAttrs = {
|
|
19208
|
+
...resourceAttrs,
|
|
19209
|
+
...attributesToRecord(otlpSpan.attributes)
|
|
19210
|
+
};
|
|
19211
|
+
const typed = extractTypedFields(allAttrs);
|
|
19212
|
+
const startTime = nanoToDate(otlpSpan.startTimeUnixNano);
|
|
19213
|
+
const endTime = otlpSpan.endTimeUnixNano ? nanoToDate(otlpSpan.endTimeUnixNano) : null;
|
|
19214
|
+
const durationMs = endTime ? endTime.getTime() - startTime.getTime() : null;
|
|
19215
|
+
const spanStatus = otlpSpan.status?.code ?? 0;
|
|
19216
|
+
const traceStatus = spanStatus === 2 ? "error" : spanStatus === 1 ? "ok" : "unset";
|
|
19217
|
+
let cost = 0;
|
|
19218
|
+
if (typed.provider && typed.model && (typed.promptTokens > 0 || typed.completionTokens > 0)) try {
|
|
19219
|
+
const pricing = await pricingProvider.getModelPricing(typed.provider, typed.model);
|
|
19220
|
+
if (pricing) cost = calculateCacheAwareCost({
|
|
19221
|
+
promptTokens: typed.promptTokens,
|
|
19222
|
+
completionTokens: typed.completionTokens
|
|
19223
|
+
}, pricing, typed.provider).totalCost;
|
|
19224
|
+
} catch (e) {
|
|
19225
|
+
logger.info(`[OTLP] Failed to calculate cost for ${typed.provider}/${typed.model}: ${e instanceof Error ? e.message : String(e)}`);
|
|
19226
|
+
}
|
|
19227
|
+
const spanData = {
|
|
19228
|
+
traceId: otlpSpan.traceId,
|
|
19229
|
+
spanId: otlpSpan.spanId,
|
|
19230
|
+
parentSpanId: otlpSpan.parentSpanId || null,
|
|
19231
|
+
name: otlpSpan.name,
|
|
19232
|
+
kind: otlpSpan.kind ?? 1,
|
|
19233
|
+
status: spanStatus,
|
|
19234
|
+
statusMessage: otlpSpan.status?.message ?? null,
|
|
19235
|
+
startTime,
|
|
19236
|
+
endTime,
|
|
19237
|
+
durationMs,
|
|
19238
|
+
provider: typed.provider,
|
|
19239
|
+
model: typed.model,
|
|
18893
19240
|
promptTokens: typed.promptTokens,
|
|
18894
|
-
completionTokens: typed.completionTokens
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
19241
|
+
completionTokens: typed.completionTokens,
|
|
19242
|
+
totalTokens: typed.totalTokens,
|
|
19243
|
+
cost,
|
|
19244
|
+
source: "otlp",
|
|
19245
|
+
input: typed.input,
|
|
19246
|
+
output: typed.output,
|
|
19247
|
+
attributes: allAttrs
|
|
19248
|
+
};
|
|
19249
|
+
const spanEvents = (otlpSpan.events ?? []).map((event) => ({
|
|
19250
|
+
traceId: otlpSpan.traceId,
|
|
19251
|
+
spanId: otlpSpan.spanId,
|
|
19252
|
+
name: event.name,
|
|
19253
|
+
timestamp: nanoToDate(event.timeUnixNano),
|
|
19254
|
+
attributes: attributesToRecord(event.attributes)
|
|
19255
|
+
}));
|
|
19256
|
+
const traceData = {
|
|
19257
|
+
traceId: otlpSpan.traceId,
|
|
19258
|
+
name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
|
|
19259
|
+
sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
|
|
19260
|
+
userId: null,
|
|
19261
|
+
status: traceStatus,
|
|
19262
|
+
startTime,
|
|
19263
|
+
endTime,
|
|
19264
|
+
durationMs,
|
|
19265
|
+
spanCount: 1,
|
|
19266
|
+
totalInputTokens: typed.promptTokens,
|
|
19267
|
+
totalOutputTokens: typed.completionTokens,
|
|
19268
|
+
totalTokens: typed.totalTokens,
|
|
19269
|
+
totalCost: cost,
|
|
19270
|
+
tags: {},
|
|
19271
|
+
metadata: {}
|
|
19272
|
+
};
|
|
19273
|
+
logger.info(`[OTLP] spanData — traceId: ${spanData.traceId}, spanId: ${spanData.spanId}, parentSpanId: ${spanData.parentSpanId ?? "null"}, name: ${spanData.name}, source: ${spanData.source}, startTime: ${spanData.startTime.toISOString()}, endTime: ${spanData.endTime?.toISOString() ?? "null"}, durationMs: ${spanData.durationMs}`);
|
|
19274
|
+
logger.info(`[OTLP] traceData — traceId: ${traceData.traceId}, name: ${traceData.name ?? "null"}, status: ${traceData.status}, spanCount: ${traceData.spanCount}`);
|
|
19275
|
+
const item = {
|
|
19276
|
+
span: spanData,
|
|
19277
|
+
events: spanEvents.length > 0 ? spanEvents : void 0,
|
|
19278
|
+
trace: traceData
|
|
19279
|
+
};
|
|
19280
|
+
traceBatchWriter.enqueue(item);
|
|
19281
|
+
spanCount++;
|
|
18898
19282
|
}
|
|
18899
|
-
const spanData = {
|
|
18900
|
-
traceId: otlpSpan.traceId,
|
|
18901
|
-
spanId: otlpSpan.spanId,
|
|
18902
|
-
parentSpanId: otlpSpan.parentSpanId || null,
|
|
18903
|
-
name: otlpSpan.name,
|
|
18904
|
-
kind: otlpSpan.kind ?? 1,
|
|
18905
|
-
status: spanStatus,
|
|
18906
|
-
statusMessage: otlpSpan.status?.message ?? null,
|
|
18907
|
-
startTime,
|
|
18908
|
-
endTime,
|
|
18909
|
-
durationMs,
|
|
18910
|
-
provider: typed.provider,
|
|
18911
|
-
model: typed.model,
|
|
18912
|
-
promptTokens: typed.promptTokens,
|
|
18913
|
-
completionTokens: typed.completionTokens,
|
|
18914
|
-
totalTokens: typed.totalTokens,
|
|
18915
|
-
cost,
|
|
18916
|
-
source: "otlp",
|
|
18917
|
-
input: typed.input,
|
|
18918
|
-
output: typed.output,
|
|
18919
|
-
attributes: allAttrs
|
|
18920
|
-
};
|
|
18921
|
-
const spanEvents = (otlpSpan.events ?? []).map((event) => ({
|
|
18922
|
-
traceId: otlpSpan.traceId,
|
|
18923
|
-
spanId: otlpSpan.spanId,
|
|
18924
|
-
name: event.name,
|
|
18925
|
-
timestamp: nanoToDate(event.timeUnixNano),
|
|
18926
|
-
attributes: attributesToRecord(event.attributes)
|
|
18927
|
-
}));
|
|
18928
|
-
const traceData = {
|
|
18929
|
-
traceId: otlpSpan.traceId,
|
|
18930
|
-
name: !otlpSpan.parentSpanId ? otlpSpan.name : null,
|
|
18931
|
-
sessionId: allAttrs["gen_ai.conversation.id"] ?? null,
|
|
18932
|
-
userId: null,
|
|
18933
|
-
status: traceStatus,
|
|
18934
|
-
startTime,
|
|
18935
|
-
endTime,
|
|
18936
|
-
durationMs,
|
|
18937
|
-
spanCount: 1,
|
|
18938
|
-
totalInputTokens: typed.promptTokens,
|
|
18939
|
-
totalOutputTokens: typed.completionTokens,
|
|
18940
|
-
totalTokens: typed.totalTokens,
|
|
18941
|
-
totalCost: cost,
|
|
18942
|
-
tags: {},
|
|
18943
|
-
metadata: {}
|
|
18944
|
-
};
|
|
18945
|
-
const item = {
|
|
18946
|
-
span: spanData,
|
|
18947
|
-
events: spanEvents.length > 0 ? spanEvents : void 0,
|
|
18948
|
-
trace: traceData
|
|
18949
|
-
};
|
|
18950
|
-
traceBatchWriter.enqueue(item);
|
|
18951
|
-
spanCount++;
|
|
18952
19283
|
}
|
|
18953
19284
|
}
|
|
19285
|
+
logger.info(`[OTLP] Enqueued ${spanCount} spans total`);
|
|
18954
19286
|
return c.json({ partialSuccess: {} });
|
|
18955
19287
|
} catch (error$47) {
|
|
18956
19288
|
const msg = error$47 instanceof Error ? error$47.message : String(error$47);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llmops/app",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "LLMOps application with server and client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -72,12 +72,13 @@
|
|
|
72
72
|
"motion": "^12.23.25",
|
|
73
73
|
"nunjucks": "^3.2.4",
|
|
74
74
|
"openai": "^6.10.0",
|
|
75
|
+
"protobufjs": "^8.0.0",
|
|
75
76
|
"react-aria-components": "^1.13.0",
|
|
76
77
|
"react-hook-form": "^7.68.0",
|
|
77
78
|
"recharts": "^3.6.0",
|
|
78
79
|
"uuid": "^13.0.0",
|
|
79
|
-
"@llmops/core": "^0.6.
|
|
80
|
-
"@llmops/gateway": "^0.6.
|
|
80
|
+
"@llmops/core": "^0.6.5",
|
|
81
|
+
"@llmops/gateway": "^0.6.5"
|
|
81
82
|
},
|
|
82
83
|
"peerDependencies": {
|
|
83
84
|
"react": "^19.2.1",
|