observability-toolkit 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backends/index.d.ts +93 -1
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/local-jsonl-boolean-search.test.d.ts +2 -0
- package/dist/backends/local-jsonl-boolean-search.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl-boolean-search.test.js +154 -0
- package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -0
- package/dist/backends/local-jsonl.d.ts +47 -3
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +635 -142
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/local-jsonl.test.js +2108 -133
- package/dist/backends/local-jsonl.test.js.map +1 -1
- package/dist/backends/signoz-api.d.ts +13 -1
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.js +326 -0
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.js +320 -0
- package/dist/backends/signoz-api.test.js.map +1 -1
- package/dist/lib/cache.d.ts +20 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/cache.js +63 -0
- package/dist/lib/cache.js.map +1 -0
- package/dist/lib/indexer.d.ts +78 -0
- package/dist/lib/indexer.d.ts.map +1 -0
- package/dist/lib/indexer.js +277 -0
- package/dist/lib/indexer.js.map +1 -0
- package/dist/lib/indexer.test.d.ts +2 -0
- package/dist/lib/indexer.test.d.ts.map +1 -0
- package/dist/lib/indexer.test.js +392 -0
- package/dist/lib/indexer.test.js.map +1 -0
- package/dist/lib/otlp-export.d.ts +178 -0
- package/dist/lib/otlp-export.d.ts.map +1 -0
- package/dist/lib/otlp-export.js +382 -0
- package/dist/lib/otlp-export.js.map +1 -0
- package/dist/tools/query-logs.d.ts +4 -4
- package/package.json +1 -1
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* span or log record, not the batched OpenTelemetry export format.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'path';
|
|
8
|
+
import { convertToOTLPTraces, convertToOTLPLogs, convertToOTLPMetrics, } from '../lib/otlp-export.js';
|
|
8
9
|
import { TELEMETRY_DIR, getTelemetryDirectories, getSpanKind, getStatusCodeName } from '../lib/constants.js';
|
|
9
10
|
import { listFiles, streamJsonl, parseDateFromFilename, getDateString, paginateResults, hasReachedLimit, } from '../lib/file-utils.js';
|
|
11
|
+
import { QueryCache, makeCacheKey } from '../lib/cache.js';
|
|
12
|
+
import { getIndexPath, readIndex, isIndexStale, queryIndex, readLinesByNumber, } from '../lib/indexer.js';
|
|
10
13
|
import { existsSync } from 'fs';
|
|
11
14
|
/**
|
|
12
15
|
* Insert item into a sorted array, maintaining sort order and max size.
|
|
@@ -36,6 +39,107 @@ function insertSortedBounded(arr, item, maxSize, compareFn) {
|
|
|
36
39
|
arr.pop();
|
|
37
40
|
}
|
|
38
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Apply numeric filter conditions to an attributes object.
|
|
44
|
+
* Returns true if all conditions pass, false otherwise.
|
|
45
|
+
*/
|
|
46
|
+
function applyNumericFilters(attributes, filters) {
|
|
47
|
+
for (const filter of filters) {
|
|
48
|
+
const attrValue = attributes?.[filter.attribute];
|
|
49
|
+
// Skip if attribute doesn't exist or isn't a number
|
|
50
|
+
if (typeof attrValue !== 'number') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
// Apply the comparison
|
|
54
|
+
switch (filter.operator) {
|
|
55
|
+
case 'gt':
|
|
56
|
+
if (!(attrValue > filter.value))
|
|
57
|
+
return false;
|
|
58
|
+
break;
|
|
59
|
+
case 'gte':
|
|
60
|
+
if (!(attrValue >= filter.value))
|
|
61
|
+
return false;
|
|
62
|
+
break;
|
|
63
|
+
case 'lt':
|
|
64
|
+
if (!(attrValue < filter.value))
|
|
65
|
+
return false;
|
|
66
|
+
break;
|
|
67
|
+
case 'lte':
|
|
68
|
+
if (!(attrValue <= filter.value))
|
|
69
|
+
return false;
|
|
70
|
+
break;
|
|
71
|
+
case 'eq':
|
|
72
|
+
if (!(attrValue === filter.value))
|
|
73
|
+
return false;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse time bucket string to milliseconds
|
|
81
|
+
* Supports: '1m', '5m', '1h', '1d', etc.
|
|
82
|
+
*/
|
|
83
|
+
function parseTimeBucket(bucket) {
|
|
84
|
+
const match = bucket.match(/^(\d+)(m|h|d)$/);
|
|
85
|
+
if (!match)
|
|
86
|
+
return null;
|
|
87
|
+
const value = parseInt(match[1], 10);
|
|
88
|
+
const unit = match[2];
|
|
89
|
+
switch (unit) {
|
|
90
|
+
case 'm':
|
|
91
|
+
return value * 60 * 1000; // minutes to ms
|
|
92
|
+
case 'h':
|
|
93
|
+
return value * 60 * 60 * 1000; // hours to ms
|
|
94
|
+
case 'd':
|
|
95
|
+
return value * 24 * 60 * 60 * 1000; // days to ms
|
|
96
|
+
default:
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Floor timestamp to bucket boundary
|
|
102
|
+
*/
|
|
103
|
+
function floorToBucket(timestamp, bucketMs) {
|
|
104
|
+
const ts = new Date(timestamp).getTime();
|
|
105
|
+
return Math.floor(ts / bucketMs) * bucketMs;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extract a field from an object using dot notation path.
|
|
109
|
+
* Returns undefined if the path doesn't exist or the object is not traversable.
|
|
110
|
+
*/
|
|
111
|
+
function extractField(obj, path) {
|
|
112
|
+
const parts = path.split('.');
|
|
113
|
+
let current = obj;
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
if (current == null || typeof current !== 'object')
|
|
116
|
+
return undefined;
|
|
117
|
+
current = current[part];
|
|
118
|
+
}
|
|
119
|
+
return current;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Extract multiple fields from a JSON string body.
|
|
123
|
+
* Returns undefined if body is not valid JSON or doesn't start with '{'.
|
|
124
|
+
*/
|
|
125
|
+
function extractFieldsFromBody(body, fields) {
|
|
126
|
+
if (!body.startsWith('{'))
|
|
127
|
+
return undefined;
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(body);
|
|
130
|
+
const extracted = {};
|
|
131
|
+
for (const field of fields) {
|
|
132
|
+
const value = extractField(parsed, field);
|
|
133
|
+
if (value !== undefined) {
|
|
134
|
+
extracted[field] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return Object.keys(extracted).length > 0 ? extracted : undefined;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
39
143
|
/**
|
|
40
144
|
* OTel-compliant severity number mapping
|
|
41
145
|
* https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
@@ -77,6 +181,21 @@ function normalizeSpan(raw) {
|
|
|
77
181
|
if (raw.resource?.serviceVersion) {
|
|
78
182
|
attributes['service.version'] = raw.resource.serviceVersion;
|
|
79
183
|
}
|
|
184
|
+
// Normalize span links
|
|
185
|
+
let links;
|
|
186
|
+
if (raw.links && raw.links.length > 0) {
|
|
187
|
+
links = raw.links
|
|
188
|
+
.filter(link => link.context?.traceId && link.context?.spanId)
|
|
189
|
+
.map(link => ({
|
|
190
|
+
traceId: link.context.traceId,
|
|
191
|
+
spanId: link.context.spanId,
|
|
192
|
+
attributes: link.attributes,
|
|
193
|
+
}));
|
|
194
|
+
// Only include if we have valid links
|
|
195
|
+
if (links.length === 0) {
|
|
196
|
+
links = undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
80
199
|
return {
|
|
81
200
|
traceId: raw.traceId,
|
|
82
201
|
spanId: raw.spanId,
|
|
@@ -89,12 +208,14 @@ function normalizeSpan(raw) {
|
|
|
89
208
|
status: raw.status?.code !== undefined ? { code: raw.status.code, message: raw.status.message } : undefined,
|
|
90
209
|
statusCode: getStatusCodeName(raw.status?.code),
|
|
91
210
|
attributes,
|
|
211
|
+
links,
|
|
212
|
+
instrumentationScope: raw.instrumentationScope,
|
|
92
213
|
};
|
|
93
214
|
}
|
|
94
215
|
/**
|
|
95
216
|
* Convert flat log to normalized LogRecord
|
|
96
217
|
*/
|
|
97
|
-
function normalizeLog(raw) {
|
|
218
|
+
function normalizeLog(raw, extractFields) {
|
|
98
219
|
const attributes = { ...raw.attributes };
|
|
99
220
|
if (raw.resource?.serviceName) {
|
|
100
221
|
attributes['service.name'] = raw.resource.serviceName;
|
|
@@ -110,16 +231,63 @@ function normalizeLog(raw) {
|
|
|
110
231
|
}
|
|
111
232
|
const severity = raw.severityText || raw.severity || 'INFO';
|
|
112
233
|
const severityNumber = SEVERITY_MAP[severity.toUpperCase()];
|
|
234
|
+
const body = raw.body || '';
|
|
235
|
+
// Extract fields from JSON body if requested
|
|
236
|
+
let extractedFields;
|
|
237
|
+
if (extractFields && extractFields.length > 0) {
|
|
238
|
+
extractedFields = extractFieldsFromBody(body, extractFields);
|
|
239
|
+
}
|
|
113
240
|
return {
|
|
114
241
|
timestamp,
|
|
115
242
|
severity,
|
|
116
243
|
severityNumber,
|
|
117
|
-
body
|
|
244
|
+
body,
|
|
118
245
|
traceId: raw.traceId,
|
|
119
246
|
spanId: raw.spanId,
|
|
120
247
|
attributes,
|
|
248
|
+
instrumentationScope: raw.instrumentationScope,
|
|
249
|
+
extractedFields,
|
|
121
250
|
};
|
|
122
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Convert OTel aggregation temporality to string representation
|
|
254
|
+
* OTel spec: 0=UNSPECIFIED, 1=DELTA, 2=CUMULATIVE
|
|
255
|
+
* https://opentelemetry.io/docs/specs/otel/metrics/data-model/#temporality
|
|
256
|
+
*/
|
|
257
|
+
function normalizeAggregationTemporality(value) {
|
|
258
|
+
if (value === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
// Handle string values
|
|
261
|
+
if (typeof value === 'string') {
|
|
262
|
+
const upper = value.toUpperCase();
|
|
263
|
+
if (upper === 'DELTA' || upper === 'CUMULATIVE' || upper === 'UNSPECIFIED') {
|
|
264
|
+
return upper;
|
|
265
|
+
}
|
|
266
|
+
return 'UNSPECIFIED';
|
|
267
|
+
}
|
|
268
|
+
// Handle numeric values (OTel spec)
|
|
269
|
+
switch (value) {
|
|
270
|
+
case 1:
|
|
271
|
+
return 'DELTA';
|
|
272
|
+
case 2:
|
|
273
|
+
return 'CUMULATIVE';
|
|
274
|
+
case 0:
|
|
275
|
+
default:
|
|
276
|
+
return 'UNSPECIFIED';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Normalize a flat exemplar timestamp to ISO string
|
|
281
|
+
*/
|
|
282
|
+
function normalizeExemplarTimestamp(ts, metricTimestamp) {
|
|
283
|
+
if (!ts)
|
|
284
|
+
return metricTimestamp;
|
|
285
|
+
if (typeof ts === 'string')
|
|
286
|
+
return ts;
|
|
287
|
+
// Convert [seconds, nanoseconds] to ISO string
|
|
288
|
+
const ms = ts[0] * 1000 + Math.floor(ts[1] / 1_000_000);
|
|
289
|
+
return new Date(ms).toISOString();
|
|
290
|
+
}
|
|
123
291
|
/**
|
|
124
292
|
* Convert flat metric to normalized MetricDataPoint
|
|
125
293
|
*/
|
|
@@ -128,12 +296,40 @@ function normalizeMetric(raw) {
|
|
|
128
296
|
if (raw.resource?.serviceName) {
|
|
129
297
|
attributes['service.name'] = raw.resource.serviceName;
|
|
130
298
|
}
|
|
299
|
+
// Build histogram data if present
|
|
300
|
+
let histogram;
|
|
301
|
+
if (raw.histogram && raw.type === 'histogram') {
|
|
302
|
+
histogram = {
|
|
303
|
+
buckets: raw.histogram.buckets.map(b => ({
|
|
304
|
+
le: b.le,
|
|
305
|
+
count: b.count,
|
|
306
|
+
})),
|
|
307
|
+
sum: raw.histogram.sum,
|
|
308
|
+
count: raw.histogram.count,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// Normalize exemplars if present
|
|
312
|
+
let exemplars;
|
|
313
|
+
if (raw.exemplars && raw.exemplars.length > 0) {
|
|
314
|
+
exemplars = raw.exemplars.map(e => ({
|
|
315
|
+
timestamp: normalizeExemplarTimestamp(e.timestamp, raw.timestamp),
|
|
316
|
+
value: e.value,
|
|
317
|
+
traceId: e.traceId,
|
|
318
|
+
spanId: e.spanId,
|
|
319
|
+
attributes: e.attributes,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
// Normalize aggregation temporality
|
|
323
|
+
const aggregationTemporality = normalizeAggregationTemporality(raw.aggregationTemporality);
|
|
131
324
|
return {
|
|
132
325
|
timestamp: raw.timestamp,
|
|
133
326
|
name: raw.name,
|
|
134
327
|
value: raw.value,
|
|
135
328
|
unit: raw.unit,
|
|
136
329
|
attributes,
|
|
330
|
+
histogram,
|
|
331
|
+
exemplars,
|
|
332
|
+
aggregationTemporality,
|
|
137
333
|
};
|
|
138
334
|
}
|
|
139
335
|
/**
|
|
@@ -163,163 +359,325 @@ function getFilesInRange(dir, pattern, startDate, endDate) {
|
|
|
163
359
|
export class LocalJsonlBackend {
|
|
164
360
|
name = 'local-jsonl';
|
|
165
361
|
telemetryDir;
|
|
166
|
-
|
|
362
|
+
traceCache = new QueryCache();
|
|
363
|
+
logCache = new QueryCache();
|
|
364
|
+
metricCache = new QueryCache();
|
|
365
|
+
llmEventCache = new QueryCache();
|
|
366
|
+
useIndexes;
|
|
367
|
+
constructor(telemetryDir, useIndexes = true) {
|
|
167
368
|
this.telemetryDir = telemetryDir || TELEMETRY_DIR;
|
|
369
|
+
this.useIndexes = useIndexes;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Clear all query caches
|
|
373
|
+
*/
|
|
374
|
+
clearCache() {
|
|
375
|
+
this.traceCache.clear();
|
|
376
|
+
this.logCache.clear();
|
|
377
|
+
this.metricCache.clear();
|
|
378
|
+
this.llmEventCache.clear();
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Try to use an index for a file, returning matching line numbers or null if full scan needed
|
|
382
|
+
*/
|
|
383
|
+
tryUseIndex(file, _type, indexOptions) {
|
|
384
|
+
if (!this.useIndexes)
|
|
385
|
+
return null;
|
|
386
|
+
const idxPath = getIndexPath(file);
|
|
387
|
+
const index = readIndex(idxPath);
|
|
388
|
+
if (!index)
|
|
389
|
+
return null;
|
|
390
|
+
if (isIndexStale(index, file))
|
|
391
|
+
return null;
|
|
392
|
+
return queryIndex(index, indexOptions);
|
|
168
393
|
}
|
|
169
394
|
async queryTraces(options) {
|
|
395
|
+
// Check cache first
|
|
396
|
+
const cacheKey = makeCacheKey('traces', options);
|
|
397
|
+
const cached = this.traceCache.get(cacheKey);
|
|
398
|
+
if (cached)
|
|
399
|
+
return cached;
|
|
170
400
|
const files = getFilesInRange(this.telemetryDir, /traces-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
|
|
171
401
|
const results = [];
|
|
172
402
|
const limit = options.limit || 100;
|
|
173
403
|
const offset = options.offset || 0;
|
|
404
|
+
// Compile regex once outside the loop, handling invalid patterns gracefully
|
|
405
|
+
let spanNameRegex = null;
|
|
406
|
+
if (options.spanNameRegex) {
|
|
407
|
+
try {
|
|
408
|
+
spanNameRegex = new RegExp(options.spanNameRegex);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
console.warn(`Invalid spanNameRegex pattern: ${options.spanNameRegex}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Build index query options for indexable filters
|
|
415
|
+
const indexOptions = {
|
|
416
|
+
traceId: options.traceId,
|
|
417
|
+
spanName: options.spanName,
|
|
418
|
+
serviceName: options.serviceName,
|
|
419
|
+
};
|
|
420
|
+
// Helper to apply non-indexable filters to a span
|
|
421
|
+
const applyFilters = (span) => {
|
|
422
|
+
// Regex filter (not indexable)
|
|
423
|
+
if (spanNameRegex && !spanNameRegex.test(span.name))
|
|
424
|
+
return false;
|
|
425
|
+
if (options.excludeSpanName && span.name.includes(options.excludeSpanName))
|
|
426
|
+
return false;
|
|
427
|
+
if (options.minDurationMs && (span.durationMs || 0) < options.minDurationMs)
|
|
428
|
+
return false;
|
|
429
|
+
if (options.maxDurationMs && (span.durationMs || Infinity) > options.maxDurationMs)
|
|
430
|
+
return false;
|
|
431
|
+
// Apply attribute filter
|
|
432
|
+
if (options.attributeFilter) {
|
|
433
|
+
for (const [key, value] of Object.entries(options.attributeFilter)) {
|
|
434
|
+
if (span.attributes?.[key] !== value)
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Apply attributeExists filter
|
|
439
|
+
if (options.attributeExists) {
|
|
440
|
+
for (const key of options.attributeExists) {
|
|
441
|
+
if (span.attributes?.[key] === undefined)
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Apply attributeNotExists filter
|
|
446
|
+
if (options.attributeNotExists) {
|
|
447
|
+
for (const key of options.attributeNotExists) {
|
|
448
|
+
if (span.attributes?.[key] !== undefined)
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Apply numeric filter conditions
|
|
453
|
+
if (options.numericFilter && options.numericFilter.length > 0) {
|
|
454
|
+
if (!applyNumericFilters(span.attributes, options.numericFilter))
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
return true;
|
|
458
|
+
};
|
|
174
459
|
for (const file of files) {
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (options.spanName && !span.name.includes(options.spanName))
|
|
184
|
-
continue;
|
|
185
|
-
if (options.excludeSpanName && span.name.includes(options.excludeSpanName))
|
|
186
|
-
continue;
|
|
187
|
-
if (options.minDurationMs && (span.durationMs || 0) < options.minDurationMs)
|
|
188
|
-
continue;
|
|
189
|
-
if (options.maxDurationMs && (span.durationMs || Infinity) > options.maxDurationMs)
|
|
190
|
-
continue;
|
|
191
|
-
if (options.serviceName) {
|
|
192
|
-
const svc = span.attributes?.['service.name'];
|
|
193
|
-
if (svc !== options.serviceName)
|
|
460
|
+
// Try to use index for pre-filtering
|
|
461
|
+
const matchingLines = this.tryUseIndex(file, 'traces', indexOptions);
|
|
462
|
+
if (matchingLines !== null) {
|
|
463
|
+
// Use indexed query - read only matching lines
|
|
464
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
465
|
+
for (const raw of rawRecords) {
|
|
466
|
+
const span = normalizeSpan(raw);
|
|
467
|
+
if (!span)
|
|
194
468
|
continue;
|
|
195
|
-
|
|
196
|
-
// Apply attribute filter
|
|
197
|
-
if (options.attributeFilter) {
|
|
198
|
-
let matches = true;
|
|
199
|
-
for (const [key, value] of Object.entries(options.attributeFilter)) {
|
|
200
|
-
if (span.attributes?.[key] !== value) {
|
|
201
|
-
matches = false;
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
if (!matches)
|
|
469
|
+
if (!applyFilters(span))
|
|
206
470
|
continue;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (span.attributes?.[key] === undefined) {
|
|
213
|
-
allExist = false;
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
471
|
+
results.push(span);
|
|
472
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
473
|
+
const paginated = paginateResults(results, offset, limit);
|
|
474
|
+
this.traceCache.set(cacheKey, paginated);
|
|
475
|
+
return paginated;
|
|
216
476
|
}
|
|
217
|
-
if (!allExist)
|
|
218
|
-
continue;
|
|
219
477
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
// Fall back to full file scan
|
|
481
|
+
for await (const raw of streamJsonl(file)) {
|
|
482
|
+
const span = normalizeSpan(raw);
|
|
483
|
+
if (!span)
|
|
484
|
+
continue;
|
|
485
|
+
// Apply indexable filters (since no index was used)
|
|
486
|
+
if (options.traceId && span.traceId !== options.traceId)
|
|
487
|
+
continue;
|
|
488
|
+
if (options.spanName && !span.name.includes(options.spanName))
|
|
489
|
+
continue;
|
|
490
|
+
if (options.serviceName) {
|
|
491
|
+
const svc = span.attributes?.['service.name'];
|
|
492
|
+
if (svc !== options.serviceName)
|
|
493
|
+
continue;
|
|
228
494
|
}
|
|
229
|
-
if (
|
|
495
|
+
if (!applyFilters(span))
|
|
230
496
|
continue;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
497
|
+
results.push(span);
|
|
498
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
499
|
+
const paginated = paginateResults(results, offset, limit);
|
|
500
|
+
this.traceCache.set(cacheKey, paginated);
|
|
501
|
+
return paginated;
|
|
502
|
+
}
|
|
235
503
|
}
|
|
236
504
|
}
|
|
237
505
|
}
|
|
238
|
-
|
|
506
|
+
const paginated = paginateResults(results, offset, limit);
|
|
507
|
+
this.traceCache.set(cacheKey, paginated);
|
|
508
|
+
return paginated;
|
|
239
509
|
}
|
|
240
510
|
async queryLogs(options) {
|
|
511
|
+
// Check cache first
|
|
512
|
+
const cacheKey = makeCacheKey('logs', options);
|
|
513
|
+
const cached = this.logCache.get(cacheKey);
|
|
514
|
+
if (cached)
|
|
515
|
+
return cached;
|
|
241
516
|
const files = getFilesInRange(this.telemetryDir, /logs-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
|
|
242
517
|
const results = [];
|
|
243
518
|
const limit = options.limit || 100;
|
|
244
519
|
const offset = options.offset || 0;
|
|
520
|
+
// Build index query options for indexable filters
|
|
521
|
+
const indexOptions = {
|
|
522
|
+
traceId: options.traceId,
|
|
523
|
+
severity: options.severity,
|
|
524
|
+
};
|
|
525
|
+
// Helper to apply non-indexable filters to a log
|
|
526
|
+
const applyFilters = (log) => {
|
|
527
|
+
if (options.search && !log.body.toLowerCase().includes(options.search.toLowerCase()))
|
|
528
|
+
return false;
|
|
529
|
+
// Apply boolean search with multiple terms
|
|
530
|
+
if (options.searchTerms && options.searchTerms.length > 0) {
|
|
531
|
+
const bodyLower = log.body.toLowerCase();
|
|
532
|
+
const operator = options.searchOperator || 'AND';
|
|
533
|
+
if (operator === 'AND') {
|
|
534
|
+
const allMatch = options.searchTerms.every(term => bodyLower.includes(term.toLowerCase()));
|
|
535
|
+
if (!allMatch)
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
const anyMatch = options.searchTerms.some(term => bodyLower.includes(term.toLowerCase()));
|
|
540
|
+
if (!anyMatch)
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (options.excludeSearch && log.body.toLowerCase().includes(options.excludeSearch.toLowerCase()))
|
|
545
|
+
return false;
|
|
546
|
+
// Apply attributeExists filter
|
|
547
|
+
if (options.attributeExists) {
|
|
548
|
+
for (const key of options.attributeExists) {
|
|
549
|
+
if (log.attributes?.[key] === undefined)
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Apply attributeNotExists filter
|
|
554
|
+
if (options.attributeNotExists) {
|
|
555
|
+
for (const key of options.attributeNotExists) {
|
|
556
|
+
if (log.attributes?.[key] !== undefined)
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Apply numeric filter conditions
|
|
561
|
+
if (options.numericFilter && options.numericFilter.length > 0) {
|
|
562
|
+
if (!applyNumericFilters(log.attributes, options.numericFilter))
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
return true;
|
|
566
|
+
};
|
|
245
567
|
for (const file of files) {
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (options.traceId && log.traceId !== options.traceId)
|
|
255
|
-
continue;
|
|
256
|
-
if (options.search && !log.body.toLowerCase().includes(options.search.toLowerCase()))
|
|
257
|
-
continue;
|
|
258
|
-
if (options.excludeSearch && log.body.toLowerCase().includes(options.excludeSearch.toLowerCase()))
|
|
259
|
-
continue;
|
|
260
|
-
// Apply attributeExists filter - all specified attributes must exist
|
|
261
|
-
if (options.attributeExists) {
|
|
262
|
-
let allExist = true;
|
|
263
|
-
for (const key of options.attributeExists) {
|
|
264
|
-
if (log.attributes?.[key] === undefined) {
|
|
265
|
-
allExist = false;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (!allExist)
|
|
568
|
+
// Try to use index for pre-filtering
|
|
569
|
+
const matchingLines = this.tryUseIndex(file, 'logs', indexOptions);
|
|
570
|
+
if (matchingLines !== null) {
|
|
571
|
+
// Use indexed query - read only matching lines
|
|
572
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
573
|
+
for (const raw of rawRecords) {
|
|
574
|
+
const log = normalizeLog(raw, options.extractFields);
|
|
575
|
+
if (!log)
|
|
270
576
|
continue;
|
|
271
|
-
|
|
272
|
-
// Apply attributeNotExists filter - exclude if any specified attribute exists
|
|
273
|
-
if (options.attributeNotExists) {
|
|
274
|
-
let anyExist = false;
|
|
275
|
-
for (const key of options.attributeNotExists) {
|
|
276
|
-
if (log.attributes?.[key] !== undefined) {
|
|
277
|
-
anyExist = true;
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (anyExist)
|
|
577
|
+
if (!applyFilters(log))
|
|
282
578
|
continue;
|
|
579
|
+
results.push(log);
|
|
580
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
581
|
+
const paginated = paginateResults(results, offset, limit);
|
|
582
|
+
this.logCache.set(cacheKey, paginated);
|
|
583
|
+
return paginated;
|
|
584
|
+
}
|
|
283
585
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
// Fall back to full file scan
|
|
589
|
+
for await (const raw of streamJsonl(file)) {
|
|
590
|
+
const log = normalizeLog(raw, options.extractFields);
|
|
591
|
+
if (!log)
|
|
592
|
+
continue;
|
|
593
|
+
// Apply indexable filters (since no index was used)
|
|
594
|
+
if (options.severity && log.severity.toUpperCase() !== options.severity.toUpperCase())
|
|
595
|
+
continue;
|
|
596
|
+
if (options.traceId && log.traceId !== options.traceId)
|
|
597
|
+
continue;
|
|
598
|
+
if (!applyFilters(log))
|
|
599
|
+
continue;
|
|
600
|
+
results.push(log);
|
|
601
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
602
|
+
const paginated = paginateResults(results, offset, limit);
|
|
603
|
+
this.logCache.set(cacheKey, paginated);
|
|
604
|
+
return paginated;
|
|
605
|
+
}
|
|
287
606
|
}
|
|
288
607
|
}
|
|
289
608
|
}
|
|
290
|
-
|
|
609
|
+
const paginated = paginateResults(results, offset, limit);
|
|
610
|
+
this.logCache.set(cacheKey, paginated);
|
|
611
|
+
return paginated;
|
|
291
612
|
}
|
|
292
613
|
async queryMetrics(options) {
|
|
614
|
+
// Check cache first
|
|
615
|
+
const cacheKey = makeCacheKey('metrics', options);
|
|
616
|
+
const cached = this.metricCache.get(cacheKey);
|
|
617
|
+
if (cached)
|
|
618
|
+
return cached;
|
|
293
619
|
const files = getFilesInRange(this.telemetryDir, /metrics-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
|
|
294
620
|
const results = [];
|
|
295
621
|
const limit = options.limit || 100;
|
|
296
622
|
const offset = options.offset || 0;
|
|
623
|
+
// Build index query options for indexable filters
|
|
624
|
+
const indexOptions = {
|
|
625
|
+
metricName: options.metricName,
|
|
626
|
+
};
|
|
297
627
|
outer: for (const file of files) {
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
628
|
+
// Try to use index for pre-filtering
|
|
629
|
+
const matchingLines = this.tryUseIndex(file, 'metrics', indexOptions);
|
|
630
|
+
if (matchingLines !== null) {
|
|
631
|
+
// Use indexed query - read only matching lines
|
|
632
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
633
|
+
for (const raw of rawRecords) {
|
|
634
|
+
const point = normalizeMetric(raw);
|
|
635
|
+
if (!point)
|
|
636
|
+
continue;
|
|
637
|
+
results.push(point);
|
|
638
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
639
|
+
break outer;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// Fall back to full file scan
|
|
645
|
+
for await (const raw of streamJsonl(file)) {
|
|
646
|
+
const point = normalizeMetric(raw);
|
|
647
|
+
if (!point)
|
|
648
|
+
continue;
|
|
649
|
+
// Apply filters (since no index was used)
|
|
650
|
+
if (options.metricName && !point.name.includes(options.metricName))
|
|
651
|
+
continue;
|
|
652
|
+
results.push(point);
|
|
653
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
654
|
+
break outer;
|
|
655
|
+
}
|
|
309
656
|
}
|
|
310
657
|
}
|
|
311
658
|
}
|
|
312
659
|
// Apply aggregation if requested
|
|
313
660
|
if (options.aggregation && results.length > 0) {
|
|
314
|
-
|
|
661
|
+
const aggregated = this.aggregate(results, options.aggregation, options.groupBy, options.timeBucket);
|
|
662
|
+
this.metricCache.set(cacheKey, aggregated);
|
|
663
|
+
return aggregated;
|
|
315
664
|
}
|
|
316
|
-
|
|
665
|
+
const paginated = paginateResults(results, offset, limit);
|
|
666
|
+
this.metricCache.set(cacheKey, paginated);
|
|
667
|
+
return paginated;
|
|
317
668
|
}
|
|
318
|
-
aggregate(points, aggregation, groupBy) {
|
|
319
|
-
//
|
|
669
|
+
aggregate(points, aggregation, groupBy, timeBucket) {
|
|
670
|
+
// Parse time bucket if provided
|
|
671
|
+
const bucketMs = timeBucket ? parseTimeBucket(timeBucket) : null;
|
|
672
|
+
// Group by metric name, optional attributes, and time bucket
|
|
320
673
|
const groups = new Map();
|
|
321
674
|
for (const point of points) {
|
|
322
675
|
let key = point.name;
|
|
676
|
+
// Add time bucket to key if specified
|
|
677
|
+
if (bucketMs) {
|
|
678
|
+
const bucketTimestamp = floorToBucket(point.timestamp, bucketMs);
|
|
679
|
+
key += `|_bucket=${bucketTimestamp}`;
|
|
680
|
+
}
|
|
323
681
|
if (groupBy) {
|
|
324
682
|
for (const attr of groupBy) {
|
|
325
683
|
const val = point.attributes?.[attr];
|
|
@@ -352,57 +710,150 @@ export class LocalJsonlBackend {
|
|
|
352
710
|
case 'count':
|
|
353
711
|
value = values.length;
|
|
354
712
|
break;
|
|
713
|
+
case 'p50':
|
|
714
|
+
value = this.calculatePercentile(values, 50);
|
|
715
|
+
break;
|
|
716
|
+
case 'p95':
|
|
717
|
+
value = this.calculatePercentile(values, 95);
|
|
718
|
+
break;
|
|
719
|
+
case 'p99':
|
|
720
|
+
value = this.calculatePercentile(values, 99);
|
|
721
|
+
break;
|
|
722
|
+
case 'rate':
|
|
723
|
+
value = this.calculateRate(group);
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
// Use bucket timestamp if time bucketing is enabled
|
|
727
|
+
let timestamp = group[group.length - 1].timestamp;
|
|
728
|
+
if (bucketMs) {
|
|
729
|
+
const bucketTimestamp = floorToBucket(group[0].timestamp, bucketMs);
|
|
730
|
+
timestamp = new Date(bucketTimestamp).toISOString();
|
|
355
731
|
}
|
|
356
732
|
results.push({
|
|
357
|
-
timestamp
|
|
733
|
+
timestamp,
|
|
358
734
|
name: group[0].name,
|
|
359
735
|
value,
|
|
360
736
|
unit: group[0].unit,
|
|
361
737
|
attributes: group[0].attributes,
|
|
362
738
|
});
|
|
363
739
|
}
|
|
740
|
+
// Sort results by timestamp when using time buckets
|
|
741
|
+
if (bucketMs) {
|
|
742
|
+
results.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
743
|
+
}
|
|
364
744
|
return results;
|
|
365
745
|
}
|
|
746
|
+
calculatePercentile(values, percentile) {
|
|
747
|
+
if (values.length === 0)
|
|
748
|
+
return 0;
|
|
749
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
750
|
+
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
|
|
751
|
+
return sorted[Math.max(0, index)];
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Calculate rate of change per second.
|
|
755
|
+
* Rate = (last_value - first_value) / duration_in_seconds
|
|
756
|
+
* Edge cases: single value returns 0, same timestamp returns 0
|
|
757
|
+
*/
|
|
758
|
+
calculateRate(group) {
|
|
759
|
+
if (group.length < 2)
|
|
760
|
+
return 0;
|
|
761
|
+
// Sort by timestamp
|
|
762
|
+
const sorted = [...group].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
763
|
+
const firstPoint = sorted[0];
|
|
764
|
+
const lastPoint = sorted[sorted.length - 1];
|
|
765
|
+
const firstTime = new Date(firstPoint.timestamp).getTime();
|
|
766
|
+
const lastTime = new Date(lastPoint.timestamp).getTime();
|
|
767
|
+
const durationMs = lastTime - firstTime;
|
|
768
|
+
// Avoid division by zero when timestamps are the same
|
|
769
|
+
if (durationMs === 0)
|
|
770
|
+
return 0;
|
|
771
|
+
const durationSeconds = durationMs / 1000;
|
|
772
|
+
return (lastPoint.value - firstPoint.value) / durationSeconds;
|
|
773
|
+
}
|
|
366
774
|
async queryLLMEvents(options) {
|
|
775
|
+
// Check cache first
|
|
776
|
+
const cacheKey = makeCacheKey('llm-events', options);
|
|
777
|
+
const cached = this.llmEventCache.get(cacheKey);
|
|
778
|
+
if (cached)
|
|
779
|
+
return cached;
|
|
367
780
|
const files = getFilesInRange(this.telemetryDir, /llm-events-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
|
|
368
781
|
const results = [];
|
|
369
782
|
const limit = options.limit || 100;
|
|
370
783
|
const offset = options.offset || 0;
|
|
784
|
+
// Build index query options - eventName maps to spanName in index
|
|
785
|
+
const indexOptions = {
|
|
786
|
+
spanName: options.eventName,
|
|
787
|
+
};
|
|
788
|
+
// Helper to apply non-indexable filters to an event
|
|
789
|
+
const applyFilters = (event) => {
|
|
790
|
+
if (options.model) {
|
|
791
|
+
const model = event.attributes?.['gen_ai.request.model'] || event.attributes?.['model'];
|
|
792
|
+
if (model !== options.model)
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
if (options.provider) {
|
|
796
|
+
const provider = event.attributes?.['gen_ai.system'] || event.attributes?.['provider'];
|
|
797
|
+
if (provider !== options.provider)
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
if (options.search) {
|
|
801
|
+
const searchLower = options.search.toLowerCase();
|
|
802
|
+
const attrStr = JSON.stringify(event.attributes).toLowerCase();
|
|
803
|
+
if (!attrStr.includes(searchLower) && !event.name.toLowerCase().includes(searchLower))
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
return true;
|
|
807
|
+
};
|
|
371
808
|
for (const file of files) {
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (options.model) {
|
|
380
|
-
const model = event.attributes?.['gen_ai.request.model'] || event.attributes?.['model'];
|
|
381
|
-
if (model !== options.model)
|
|
809
|
+
// Try to use index for pre-filtering
|
|
810
|
+
const matchingLines = this.tryUseIndex(file, 'llm-events', indexOptions);
|
|
811
|
+
if (matchingLines !== null) {
|
|
812
|
+
// Use indexed query - read only matching lines
|
|
813
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
814
|
+
for (const event of rawRecords) {
|
|
815
|
+
if (!event.timestamp || !event.name)
|
|
382
816
|
continue;
|
|
383
|
-
|
|
384
|
-
if (options.provider) {
|
|
385
|
-
const provider = event.attributes?.['gen_ai.system'] || event.attributes?.['provider'];
|
|
386
|
-
if (provider !== options.provider)
|
|
817
|
+
if (!applyFilters(event))
|
|
387
818
|
continue;
|
|
819
|
+
results.push({
|
|
820
|
+
timestamp: event.timestamp,
|
|
821
|
+
name: event.name,
|
|
822
|
+
attributes: event.attributes,
|
|
823
|
+
});
|
|
824
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
825
|
+
const paginated = paginateResults(results, offset, limit);
|
|
826
|
+
this.llmEventCache.set(cacheKey, paginated);
|
|
827
|
+
return paginated;
|
|
828
|
+
}
|
|
388
829
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
// Fall back to full file scan
|
|
833
|
+
for await (const event of streamJsonl(file)) {
|
|
834
|
+
if (!event.timestamp || !event.name)
|
|
393
835
|
continue;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
836
|
+
// Apply indexable filters (since no index was used)
|
|
837
|
+
if (options.eventName && !event.name.includes(options.eventName))
|
|
838
|
+
continue;
|
|
839
|
+
if (!applyFilters(event))
|
|
840
|
+
continue;
|
|
841
|
+
results.push({
|
|
842
|
+
timestamp: event.timestamp,
|
|
843
|
+
name: event.name,
|
|
844
|
+
attributes: event.attributes,
|
|
845
|
+
});
|
|
846
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
847
|
+
const paginated = paginateResults(results, offset, limit);
|
|
848
|
+
this.llmEventCache.set(cacheKey, paginated);
|
|
849
|
+
return paginated;
|
|
850
|
+
}
|
|
402
851
|
}
|
|
403
852
|
}
|
|
404
853
|
}
|
|
405
|
-
|
|
854
|
+
const paginated = paginateResults(results, offset, limit);
|
|
855
|
+
this.llmEventCache.set(cacheKey, paginated);
|
|
856
|
+
return paginated;
|
|
406
857
|
}
|
|
407
858
|
async healthCheck() {
|
|
408
859
|
if (!existsSync(this.telemetryDir)) {
|
|
@@ -428,6 +879,27 @@ export class LocalJsonlBackend {
|
|
|
428
879
|
message: `Found: ${found} for ${today}`,
|
|
429
880
|
};
|
|
430
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Export traces in OTLP JSON format
|
|
884
|
+
*/
|
|
885
|
+
async exportTracesOTLP(options) {
|
|
886
|
+
const traces = await this.queryTraces(options);
|
|
887
|
+
return convertToOTLPTraces(traces);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Export logs in OTLP JSON format
|
|
891
|
+
*/
|
|
892
|
+
async exportLogsOTLP(options) {
|
|
893
|
+
const logs = await this.queryLogs(options);
|
|
894
|
+
return convertToOTLPLogs(logs);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Export metrics in OTLP JSON format
|
|
898
|
+
*/
|
|
899
|
+
async exportMetricsOTLP(options) {
|
|
900
|
+
const metrics = await this.queryMetrics(options);
|
|
901
|
+
return convertToOTLPMetrics(metrics);
|
|
902
|
+
}
|
|
431
903
|
}
|
|
432
904
|
/**
|
|
433
905
|
* Multi-directory backend that queries all telemetry directories
|
|
@@ -437,9 +909,9 @@ export class MultiDirectoryBackend {
|
|
|
437
909
|
name = 'multi-directory';
|
|
438
910
|
backends;
|
|
439
911
|
directories;
|
|
440
|
-
constructor(cwd) {
|
|
912
|
+
constructor(cwd, useIndexes = true) {
|
|
441
913
|
this.directories = getTelemetryDirectories(cwd);
|
|
442
|
-
this.backends = this.directories.map(d => new LocalJsonlBackend(d.path));
|
|
914
|
+
this.backends = this.directories.map(d => new LocalJsonlBackend(d.path, useIndexes));
|
|
443
915
|
}
|
|
444
916
|
getDirectories() {
|
|
445
917
|
return this.directories;
|
|
@@ -509,5 +981,26 @@ export class MultiDirectoryBackend {
|
|
|
509
981
|
directories: dirStatuses,
|
|
510
982
|
};
|
|
511
983
|
}
|
|
984
|
+
/**
|
|
985
|
+
* Export traces in OTLP JSON format
|
|
986
|
+
*/
|
|
987
|
+
async exportTracesOTLP(options) {
|
|
988
|
+
const traces = await this.queryTraces(options);
|
|
989
|
+
return convertToOTLPTraces(traces);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Export logs in OTLP JSON format
|
|
993
|
+
*/
|
|
994
|
+
async exportLogsOTLP(options) {
|
|
995
|
+
const logs = await this.queryLogs(options);
|
|
996
|
+
return convertToOTLPLogs(logs);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Export metrics in OTLP JSON format
|
|
1000
|
+
*/
|
|
1001
|
+
async exportMetricsOTLP(options) {
|
|
1002
|
+
const metrics = await this.queryMetrics(options);
|
|
1003
|
+
return convertToOTLPMetrics(metrics);
|
|
1004
|
+
}
|
|
512
1005
|
}
|
|
513
1006
|
//# sourceMappingURL=local-jsonl.js.map
|