@blogic-cz/agent-tools 0.11.0 → 0.12.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.
@@ -2,17 +2,24 @@ import { Effect, Option } from "effect";
2
2
  import { Flag } from "effect/unstable/cli";
3
3
 
4
4
  import { ConfigService, getToolConfig } from "#config";
5
- import type { GrafanaConfig } from "#config";
5
+ import type { ObservabilityConfig } from "#config";
6
6
 
7
- import { GrafanaToolError } from "./errors";
8
- import type { DsQueryOpts, DsQueryResponse, GrafanaEnvConfig } from "./types";
7
+ import { ObservabilityToolError } from "./errors";
8
+ import type {
9
+ DsQueryOpts,
10
+ DsQueryResponse,
11
+ GrafanaDatasource,
12
+ ObservabilityEnvConfig,
13
+ } from "./types";
9
14
 
10
15
  const DEFAULT_LOCAL_URL = "http://localhost:40300";
11
16
  const DEFAULT_PROMETHEUS_UID = "prometheus";
12
17
  const DEFAULT_LOKI_UID = "loki";
13
- export function formatGrafanaError(error: unknown): string {
14
- if (error instanceof GrafanaToolError) {
15
- return formatGrafanaError(error.cause);
18
+ const DEFAULT_TEMPO_UID = "tempo";
19
+
20
+ export function formatObservabilityError(error: unknown): string {
21
+ if (error instanceof ObservabilityToolError) {
22
+ return formatObservabilityError(error.cause);
16
23
  }
17
24
 
18
25
  if (error instanceof Error) {
@@ -30,7 +37,7 @@ export const envOption = Flag.string("env").pipe(
30
37
  export const profileOption = Flag.optional(
31
38
  Flag.string("profile").pipe(
32
39
  Flag.withDescription(
33
- "Grafana profile name from agent-tools config (default: 'default' key or single entry)",
40
+ "Observability profile name from agent-tools config (default: 'default' key or single entry)",
34
41
  ),
35
42
  ),
36
43
  );
@@ -48,43 +55,67 @@ function resolveToken(tokenEnvVar?: string): string | undefined {
48
55
  return token;
49
56
  }
50
57
 
51
- function resolveFromProfile(
52
- profile: GrafanaConfig | undefined,
58
+ async function discoverDatasources(url: string, token?: string): Promise<GrafanaDatasource[]> {
59
+ const headers = buildHeaders(token);
60
+ const response = await fetch(`${url}/api/datasources`, { headers });
61
+
62
+ if (!response.ok) {
63
+ const body = await response.text().catch(() => "");
64
+ throw new Error(`Grafana API ${response.status}: /api/datasources — ${body}`);
65
+ }
66
+
67
+ return (await response.json()) as GrafanaDatasource[];
68
+ }
69
+
70
+ async function resolveFromProfile(
71
+ profile: ObservabilityConfig | undefined,
53
72
  env: string,
54
- ): GrafanaEnvConfig | undefined {
73
+ ): Promise<ObservabilityEnvConfig | undefined> {
55
74
  const environment = profile?.environments[env];
56
75
  if (!environment) {
57
76
  return undefined;
58
77
  }
59
78
 
79
+ const token = resolveToken(environment.tokenEnvVar);
80
+ const datasources = await discoverDatasources(environment.url, token);
81
+
82
+ const tempoUid =
83
+ datasources.find((datasource) => datasource.uid === DEFAULT_TEMPO_UID)?.uid ??
84
+ datasources.find((datasource) => datasource.type === "tempo")?.uid;
85
+
86
+ if (!tempoUid) {
87
+ throw new Error(`No Tempo datasource found in observability.${env} config`);
88
+ }
89
+
60
90
  return {
61
91
  url: environment.url,
62
- token: resolveToken(environment.tokenEnvVar),
92
+ token,
63
93
  prometheusUid: environment.prometheusUid ?? DEFAULT_PROMETHEUS_UID,
64
94
  lokiUid: environment.lokiUid ?? DEFAULT_LOKI_UID,
95
+ tempoUid,
65
96
  };
66
97
  }
67
98
 
68
- function resolveFromEnv(env: string): GrafanaEnvConfig {
99
+ function resolveFromEnv(
100
+ env: string,
101
+ ): Pick<ObservabilityEnvConfig, "url" | "token" | "prometheusUid" | "lokiUid"> {
69
102
  if (env === "local") {
70
103
  return {
71
- url: process.env.GRAFANA_URL_LOCAL ?? DEFAULT_LOCAL_URL,
72
- token: process.env.GRAFANA_TOKEN_LOCAL,
104
+ url: process.env.OBSERVABILITY_URL_LOCAL ?? DEFAULT_LOCAL_URL,
105
+ token: process.env.OBSERVABILITY_TOKEN_LOCAL,
73
106
  prometheusUid: DEFAULT_PROMETHEUS_UID,
74
107
  lokiUid: DEFAULT_LOKI_UID,
75
108
  };
76
109
  }
77
110
 
78
111
  const upper = env.toUpperCase();
79
- const url = process.env[`GRAFANA_URL_${upper}`];
80
- const token = process.env[`GRAFANA_TOKEN_${upper}`];
112
+ const url = process.env[`OBSERVABILITY_URL_${upper}`];
113
+ const token = process.env[`OBSERVABILITY_TOKEN_${upper}`];
81
114
 
82
115
  if (!url) {
83
- throw new Error(`No grafana.${env} config found and GRAFANA_URL_${upper} is not set`);
84
- }
85
-
86
- if (!token) {
87
- throw new Error(`No grafana.${env} config found and GRAFANA_TOKEN_${upper} is not set`);
116
+ throw new Error(
117
+ `No observability.${env} config found and OBSERVABILITY_URL_${upper} is not set`,
118
+ );
88
119
  }
89
120
 
90
121
  return {
@@ -99,11 +130,36 @@ export const resolveConfig = (env: string, profile: Option.Option<string>) =>
99
130
  Effect.gen(function* () {
100
131
  const config = yield* ConfigService;
101
132
  const profileName = Option.getOrUndefined(profile);
102
- const grafanaConfig = getToolConfig<GrafanaConfig>(config, "grafana", profileName);
103
-
104
- return yield* Effect.try({
105
- try: () => resolveFromProfile(grafanaConfig, env) ?? resolveFromEnv(env),
106
- catch: (cause) => new GrafanaToolError({ cause }),
133
+ const observabilityConfig = getToolConfig<ObservabilityConfig>(
134
+ config,
135
+ "observability",
136
+ profileName,
137
+ );
138
+
139
+ return yield* Effect.tryPromise({
140
+ try: async () => {
141
+ const profileResolved = await resolveFromProfile(observabilityConfig, env);
142
+ if (profileResolved) {
143
+ return profileResolved;
144
+ }
145
+
146
+ const resolved = resolveFromEnv(env);
147
+
148
+ const datasources = await discoverDatasources(resolved.url, resolved.token);
149
+ const tempoUid =
150
+ datasources.find((datasource) => datasource.uid === DEFAULT_TEMPO_UID)?.uid ??
151
+ datasources.find((datasource) => datasource.type === "tempo")?.uid;
152
+
153
+ if (!tempoUid) {
154
+ throw new Error(`No Tempo datasource found for environment '${env}'`);
155
+ }
156
+
157
+ return {
158
+ ...resolved,
159
+ tempoUid,
160
+ } satisfies ObservabilityEnvConfig;
161
+ },
162
+ catch: (cause) => new ObservabilityToolError({ cause }),
107
163
  });
108
164
  });
109
165
 
@@ -120,11 +176,11 @@ export function buildHeaders(token?: string): Headers {
120
176
  return headers;
121
177
  }
122
178
 
123
- export function grafanaFetch<T>(
124
- config: GrafanaEnvConfig,
179
+ export function observabilityFetch<T>(
180
+ config: ObservabilityEnvConfig,
125
181
  path: string,
126
182
  init?: RequestInit,
127
- ): Effect.Effect<T, GrafanaToolError> {
183
+ ): Effect.Effect<T, ObservabilityToolError> {
128
184
  return Effect.tryPromise({
129
185
  try: async () => {
130
186
  const headers = buildHeaders(config.token);
@@ -147,17 +203,17 @@ export function grafanaFetch<T>(
147
203
 
148
204
  return (await response.json()) as T;
149
205
  },
150
- catch: (cause) => new GrafanaToolError({ cause }),
206
+ catch: (cause) => new ObservabilityToolError({ cause }),
151
207
  });
152
208
  }
153
209
 
154
- export function grafanaDsQuery(
155
- config: GrafanaEnvConfig,
210
+ export function observabilityDsQuery(
211
+ config: ObservabilityEnvConfig,
156
212
  datasourceUid: string,
157
213
  datasourceType: "prometheus" | "loki",
158
214
  expr: string,
159
215
  options?: DsQueryOpts,
160
- ): Effect.Effect<DsQueryResponse, GrafanaToolError> {
216
+ ): Effect.Effect<DsQueryResponse, ObservabilityToolError> {
161
217
  const opts = options ?? {};
162
218
  const query: Record<string, unknown> = {
163
219
  refId: "A",
@@ -183,7 +239,7 @@ export function grafanaDsQuery(
183
239
  }
184
240
  }
185
241
 
186
- return grafanaFetch<DsQueryResponse>(config, "/api/ds/query", {
242
+ return observabilityFetch<DsQueryResponse>(config, "/api/ds/query", {
187
243
  method: "POST",
188
244
  body: JSON.stringify({
189
245
  queries: [query],
@@ -0,0 +1,379 @@
1
+ import { Console, Effect } from "effect";
2
+ import { Argument, Command, Flag } from "effect/unstable/cli";
3
+
4
+ import { formatOption, formatOutput } from "#shared";
5
+
6
+ import { ObservabilityToolError } from "./errors";
7
+ import {
8
+ envOption,
9
+ formatObservabilityError,
10
+ observabilityDsQuery,
11
+ observabilityFetch,
12
+ profileOption,
13
+ resolveConfig,
14
+ } from "./shared";
15
+ import type {
16
+ FlattenedSpan,
17
+ OtlpAnyValue,
18
+ OtlpAttribute,
19
+ TempoTraceResponse,
20
+ TraceSummary,
21
+ } from "./types";
22
+
23
+ function isHexTraceId(value: string): boolean {
24
+ return /^[\da-f]{32}$/i.test(value);
25
+ }
26
+
27
+ function getStringAttribute(
28
+ attributes: ReadonlyArray<OtlpAttribute> | null,
29
+ key: string,
30
+ ): string | undefined {
31
+ const attribute = attributes?.find((item) => item.key === key);
32
+ const value = attribute?.value;
33
+
34
+ if (value?.stringValue !== undefined) return value.stringValue;
35
+ if (value?.intValue !== undefined) return String(value.intValue);
36
+ if (value?.doubleValue !== undefined) return String(value.doubleValue);
37
+ if (value?.boolValue !== undefined) return String(value.boolValue);
38
+ return undefined;
39
+ }
40
+
41
+ function anyValueToUnknown(value?: OtlpAnyValue): unknown {
42
+ if (value === undefined) return undefined;
43
+ if (value.stringValue !== undefined) return value.stringValue;
44
+ if (value.boolValue !== undefined) return value.boolValue;
45
+ if (value.intValue !== undefined) return value.intValue;
46
+ if (value.doubleValue !== undefined) return value.doubleValue;
47
+ if (value.arrayValue?.values !== undefined) {
48
+ return value.arrayValue.values.map((item) => anyValueToUnknown(item));
49
+ }
50
+ if (value.kvlistValue?.values !== undefined) {
51
+ return Object.fromEntries(
52
+ value.kvlistValue.values.map((entry) => [entry.key, anyValueToUnknown(entry.value)]),
53
+ );
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function attributesToRecord(
59
+ attributes: ReadonlyArray<OtlpAttribute> | null,
60
+ ): Record<string, unknown> {
61
+ return Object.fromEntries(
62
+ (attributes ?? [])
63
+ .map((attribute) => [attribute.key, anyValueToUnknown(attribute.value)] as const)
64
+ .filter((entry) => entry[1] !== undefined),
65
+ );
66
+ }
67
+
68
+ function decodeIdToHex(value?: string, expectedBytes?: number): string | undefined {
69
+ if (value === undefined) return undefined;
70
+ const trimmed = value.trim();
71
+ if (trimmed.length === 0) return undefined;
72
+
73
+ const expectedHexLength = expectedBytes === undefined ? undefined : expectedBytes * 2;
74
+ if (
75
+ expectedHexLength !== undefined &&
76
+ new RegExp(`^[\\da-f]{${expectedHexLength}}$`, "i").test(trimmed)
77
+ ) {
78
+ return trimmed.toLowerCase();
79
+ }
80
+
81
+ try {
82
+ const bytes = Buffer.from(trimmed, "base64");
83
+ if (expectedBytes !== undefined && bytes.length !== expectedBytes) {
84
+ return undefined;
85
+ }
86
+ return bytes.toString("hex");
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ function nanoToBigInt(value?: string): bigint | undefined {
93
+ if (value === undefined || value.trim().length === 0) {
94
+ return undefined;
95
+ }
96
+ try {
97
+ return BigInt(value);
98
+ } catch {
99
+ return undefined;
100
+ }
101
+ }
102
+
103
+ function computeDurationMs(start?: string, end?: string): number | undefined {
104
+ const startNano = nanoToBigInt(start);
105
+ const endNano = nanoToBigInt(end);
106
+
107
+ if (startNano === undefined || endNano === undefined || endNano < startNano) {
108
+ return undefined;
109
+ }
110
+
111
+ return Number(endNano - startNano) / 1_000_000;
112
+ }
113
+
114
+ function isErrorStatus(code?: string | number): boolean {
115
+ if (code === undefined) return false;
116
+ if (typeof code === "number") return code === 2;
117
+ return code.toUpperCase().includes("ERROR") || code === "2";
118
+ }
119
+
120
+ function flattenTrace(trace: TempoTraceResponse): FlattenedSpan[] {
121
+ const spans: FlattenedSpan[] = [];
122
+
123
+ for (const batch of trace.batches ?? []) {
124
+ const resourceAttributes = attributesToRecord(batch.resource?.attributes ?? null);
125
+ const serviceName =
126
+ getStringAttribute(batch.resource?.attributes ?? null, "service.name") ?? "unknown-service";
127
+
128
+ for (const scopeSpan of batch.scopeSpans ?? []) {
129
+ for (const span of scopeSpan.spans ?? []) {
130
+ spans.push({
131
+ serviceName,
132
+ scopeName: scopeSpan.scope?.name,
133
+ name: span.name ?? "unknown-span",
134
+ kind: span.kind,
135
+ traceId: decodeIdToHex(span.traceId, 16),
136
+ spanId: decodeIdToHex(span.spanId, 8),
137
+ parentSpanId: decodeIdToHex(span.parentSpanId, 8),
138
+ startTimeUnixNano: span.startTimeUnixNano,
139
+ endTimeUnixNano: span.endTimeUnixNano,
140
+ durationMs: computeDurationMs(span.startTimeUnixNano, span.endTimeUnixNano),
141
+ statusCode: span.status?.code,
142
+ statusMessage: span.status?.message,
143
+ isError: isErrorStatus(span.status?.code),
144
+ attributes: attributesToRecord(span.attributes ?? null),
145
+ resourceAttributes,
146
+ });
147
+ }
148
+ }
149
+ }
150
+
151
+ return spans.toSorted((left, right) => {
152
+ const leftStart = nanoToBigInt(left.startTimeUnixNano) ?? 0n;
153
+ const rightStart = nanoToBigInt(right.startTimeUnixNano) ?? 0n;
154
+ return leftStart < rightStart ? -1 : leftStart > rightStart ? 1 : 0;
155
+ });
156
+ }
157
+
158
+ function summarizeTrace(traceId: string, spans: readonly FlattenedSpan[]): TraceSummary {
159
+ const services = [...new Set(spans.map((span) => span.serviceName))].toSorted();
160
+ const spanIds = new Set(
161
+ spans.map((span) => span.spanId).filter((value): value is string => value !== undefined),
162
+ );
163
+ const rootSpans = spans.filter(
164
+ (span) => span.parentSpanId === undefined || !spanIds.has(span.parentSpanId),
165
+ );
166
+
167
+ const startedAt = spans.reduce<bigint | undefined>((current, span) => {
168
+ const value = nanoToBigInt(span.startTimeUnixNano);
169
+ if (value === undefined) return current;
170
+ if (current === undefined || value < current) return value;
171
+ return current;
172
+ }, undefined);
173
+ const endedAt = spans.reduce<bigint | undefined>((current, span) => {
174
+ const value = nanoToBigInt(span.endTimeUnixNano);
175
+ if (value === undefined) return current;
176
+ if (current === undefined || value > current) return value;
177
+ return current;
178
+ }, undefined);
179
+
180
+ return {
181
+ traceId,
182
+ spanCount: spans.length,
183
+ serviceCount: services.length,
184
+ services,
185
+ errorSpanCount: spans.filter((span) => span.isError).length,
186
+ rootSpans,
187
+ totalDurationMs:
188
+ startedAt !== undefined && endedAt !== undefined && endedAt >= startedAt
189
+ ? Number(endedAt - startedAt) / 1_000_000
190
+ : undefined,
191
+ startedAtUnixNano: startedAt?.toString(),
192
+ endedAtUnixNano: endedAt?.toString(),
193
+ };
194
+ }
195
+
196
+ function parseLabel(value: string | Record<string, string>): Record<string, string> {
197
+ if (typeof value === "object" && value !== null) {
198
+ return value;
199
+ }
200
+
201
+ try {
202
+ return JSON.parse(value) as Record<string, string>;
203
+ } catch {
204
+ return {};
205
+ }
206
+ }
207
+
208
+ const getCommand = Command.make(
209
+ "get",
210
+ {
211
+ traceId: Argument.string("traceId"),
212
+ format: formatOption,
213
+ env: envOption,
214
+ profile: profileOption,
215
+ },
216
+ ({ traceId, format, env, profile }) => {
217
+ const startedAt = Date.now();
218
+
219
+ return Effect.gen(function* () {
220
+ if (!isHexTraceId(traceId)) {
221
+ return yield* new ObservabilityToolError({
222
+ cause: new Error("trace get requires a 32-character hex trace ID"),
223
+ });
224
+ }
225
+
226
+ const config = yield* resolveConfig(env, profile);
227
+ const raw = yield* observabilityFetch<TempoTraceResponse>(
228
+ config,
229
+ `/api/datasources/proxy/uid/${config.tempoUid}/api/traces/${traceId.toLowerCase()}`,
230
+ );
231
+ const spans = flattenTrace(raw);
232
+
233
+ if (spans.length === 0) {
234
+ return yield* new ObservabilityToolError({
235
+ cause: new Error(`Trace ${traceId} returned zero spans`),
236
+ });
237
+ }
238
+
239
+ const result = {
240
+ success: true,
241
+ message: `Resolved trace ${traceId.toLowerCase()} with ${spans.length} span(s)`,
242
+ data: {
243
+ environment: env,
244
+ grafanaUrl: config.url,
245
+ tempoDatasourceUid: config.tempoUid,
246
+ summary: summarizeTrace(traceId.toLowerCase(), spans),
247
+ spans,
248
+ },
249
+ executionTimeMs: Date.now() - startedAt,
250
+ };
251
+
252
+ yield* Console.log(formatOutput(result, format));
253
+ }).pipe(
254
+ Effect.catch((error) =>
255
+ Effect.gen(function* () {
256
+ const result = {
257
+ success: false,
258
+ message: "Failed to resolve trace from Tempo",
259
+ error: formatObservabilityError(error),
260
+ hint: "Check trace ID format and Grafana/Tempo connectivity",
261
+ executionTimeMs: Date.now() - startedAt,
262
+ };
263
+ yield* Console.log(formatOutput(result, format));
264
+ }),
265
+ ),
266
+ );
267
+ },
268
+ ).pipe(Command.withDescription("Resolve a trace by ID via Grafana/Tempo"));
269
+
270
+ const logsCommand = Command.make(
271
+ "logs",
272
+ {
273
+ traceId: Argument.string("traceId"),
274
+ format: formatOption,
275
+ env: envOption,
276
+ profile: profileOption,
277
+ limit: Flag.integer("limit").pipe(
278
+ Flag.withDescription("Max log lines (default: 100)"),
279
+ Flag.withDefault(100),
280
+ ),
281
+ start: Flag.string("start").pipe(
282
+ Flag.withDescription("Start time (default: now-1h)"),
283
+ Flag.withDefault("now-1h"),
284
+ ),
285
+ end: Flag.string("end").pipe(
286
+ Flag.withDescription("End time (default: now)"),
287
+ Flag.withDefault("now"),
288
+ ),
289
+ },
290
+ ({ traceId, format, env, profile, limit, start, end }) => {
291
+ const startedAt = Date.now();
292
+
293
+ return Effect.gen(function* () {
294
+ if (!isHexTraceId(traceId)) {
295
+ return yield* new ObservabilityToolError({
296
+ cause: new Error("trace logs requires a 32-character hex trace ID"),
297
+ });
298
+ }
299
+
300
+ const config = yield* resolveConfig(env, profile);
301
+ const normalizedTraceId = traceId.toLowerCase();
302
+ const logql = `{job=~".+"} |= "${normalizedTraceId}"`;
303
+ const response = yield* observabilityDsQuery(config, config.lokiUid, "loki", logql, {
304
+ from: start,
305
+ to: end,
306
+ maxLines: limit,
307
+ });
308
+
309
+ if (response.results.A.error) {
310
+ return yield* new ObservabilityToolError({
311
+ cause: new Error(response.results.A.error),
312
+ });
313
+ }
314
+
315
+ const logs: Array<{ timestamp: string; line: string; labels: Record<string, string> }> = [];
316
+ for (const frame of response.results.A.frames ?? []) {
317
+ const fields = frame.schema.fields;
318
+ const values = frame.data.values;
319
+ const timeIndex = fields.findIndex(
320
+ (field) => field.name === "timestamp" || field.type === "time",
321
+ );
322
+ const lineIndex = fields.findIndex(
323
+ (field) => field.name === "body" || field.name === "Line" || field.name === "line",
324
+ );
325
+ const labelsIndex = fields.findIndex(
326
+ (field) => field.name === "labels" || field.name === "labelTypes",
327
+ );
328
+
329
+ const timestamps = (timeIndex >= 0 ? values[timeIndex] : []) as Array<string | number>;
330
+ const lines = (lineIndex >= 0 ? values[lineIndex] : []) as string[];
331
+ const labelValues = (labelsIndex >= 0 ? values[labelsIndex] : []) as Array<
332
+ string | Record<string, string>
333
+ >;
334
+
335
+ for (const [index, line] of lines.entries()) {
336
+ logs.push({
337
+ timestamp: String(timestamps[index] ?? ""),
338
+ line,
339
+ labels: labelValues[index] ? parseLabel(labelValues[index]) : {},
340
+ });
341
+ }
342
+ }
343
+
344
+ const result = {
345
+ success: true,
346
+ message: `Found ${logs.length} log line(s) mentioning trace ${normalizedTraceId}`,
347
+ data: {
348
+ environment: env,
349
+ grafanaUrl: config.url,
350
+ lokiDatasourceUid: config.lokiUid,
351
+ query: logql,
352
+ logCount: logs.length,
353
+ logs: logs.toSorted((left, right) => right.timestamp.localeCompare(left.timestamp)),
354
+ },
355
+ executionTimeMs: Date.now() - startedAt,
356
+ };
357
+
358
+ yield* Console.log(formatOutput(result, format));
359
+ }).pipe(
360
+ Effect.catch((error) =>
361
+ Effect.gen(function* () {
362
+ const result = {
363
+ success: false,
364
+ message: "Failed to execute trace log lookup",
365
+ error: formatObservabilityError(error),
366
+ hint: "Check trace ID format and Grafana/Loki connectivity",
367
+ executionTimeMs: Date.now() - startedAt,
368
+ };
369
+ yield* Console.log(formatOutput(result, format));
370
+ }),
371
+ ),
372
+ );
373
+ },
374
+ ).pipe(Command.withDescription("Find Loki logs mentioning a trace ID"));
375
+
376
+ export const traceCommand = Command.make("trace", {}).pipe(
377
+ Command.withDescription("Tempo trace operations"),
378
+ Command.withSubcommands([getCommand, logsCommand]),
379
+ );
@@ -0,0 +1,119 @@
1
+ export type ObservabilityEnvConfig = {
2
+ url: string;
3
+ token?: string;
4
+ prometheusUid: string;
5
+ lokiUid: string;
6
+ tempoUid: string;
7
+ };
8
+
9
+ export type GrafanaDatasource = {
10
+ uid: string;
11
+ name: string;
12
+ type: string;
13
+ url?: string;
14
+ };
15
+
16
+ export type OtlpAnyValue = {
17
+ readonly stringValue?: string;
18
+ readonly boolValue?: boolean;
19
+ readonly intValue?: number | string;
20
+ readonly doubleValue?: number;
21
+ readonly arrayValue?: { readonly values?: readonly OtlpAnyValue[] };
22
+ readonly kvlistValue?: {
23
+ readonly values?: ReadonlyArray<{
24
+ readonly key: string;
25
+ readonly value?: OtlpAnyValue;
26
+ }>;
27
+ };
28
+ };
29
+
30
+ export type OtlpAttribute = {
31
+ readonly key: string;
32
+ readonly value?: OtlpAnyValue;
33
+ };
34
+
35
+ export type TempoTraceResponse = {
36
+ readonly batches?: ReadonlyArray<{
37
+ readonly resource?: {
38
+ readonly attributes?: ReadonlyArray<OtlpAttribute>;
39
+ };
40
+ readonly scopeSpans?: ReadonlyArray<{
41
+ readonly scope?: {
42
+ readonly name?: string;
43
+ };
44
+ readonly spans?: ReadonlyArray<{
45
+ readonly attributes?: ReadonlyArray<OtlpAttribute>;
46
+ readonly endTimeUnixNano?: string;
47
+ readonly kind?: string | number;
48
+ readonly name?: string;
49
+ readonly parentSpanId?: string;
50
+ readonly spanId?: string;
51
+ readonly startTimeUnixNano?: string;
52
+ readonly status?: {
53
+ readonly code?: string | number;
54
+ readonly message?: string;
55
+ };
56
+ readonly traceId?: string;
57
+ }>;
58
+ }>;
59
+ }>;
60
+ };
61
+
62
+ export type FlattenedSpan = {
63
+ readonly serviceName: string;
64
+ readonly scopeName?: string;
65
+ readonly name: string;
66
+ readonly kind?: string | number;
67
+ readonly traceId?: string;
68
+ readonly spanId?: string;
69
+ readonly parentSpanId?: string;
70
+ readonly startTimeUnixNano?: string;
71
+ readonly endTimeUnixNano?: string;
72
+ readonly durationMs?: number;
73
+ readonly statusCode?: string | number;
74
+ readonly statusMessage?: string;
75
+ readonly isError: boolean;
76
+ readonly attributes: Record<string, unknown>;
77
+ readonly resourceAttributes: Record<string, unknown>;
78
+ };
79
+
80
+ export type TraceSummary = {
81
+ readonly traceId: string;
82
+ readonly spanCount: number;
83
+ readonly serviceCount: number;
84
+ readonly services: string[];
85
+ readonly errorSpanCount: number;
86
+ readonly rootSpans: FlattenedSpan[];
87
+ readonly totalDurationMs?: number;
88
+ readonly startedAtUnixNano?: string;
89
+ readonly endedAtUnixNano?: string;
90
+ };
91
+
92
+ export type DsQueryOpts = {
93
+ instant?: boolean;
94
+ from?: string;
95
+ to?: string;
96
+ maxLines?: number;
97
+ intervalMs?: number;
98
+ maxDataPoints?: number;
99
+ step?: number;
100
+ };
101
+
102
+ export type DsQueryResponse = {
103
+ results: {
104
+ A: {
105
+ status?: number;
106
+ frames?: Array<{
107
+ schema: {
108
+ fields: Array<{
109
+ name: string;
110
+ type?: string;
111
+ labels?: Record<string, string>;
112
+ }>;
113
+ };
114
+ data: { values: unknown[][] };
115
+ }>;
116
+ error?: string;
117
+ };
118
+ };
119
+ };