@opengeni/observability 0.3.0 → 0.4.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/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- type AttributeValue = string | number | boolean | null | undefined;
2
- type Attributes = Record<string, AttributeValue>;
3
- type ObservabilitySettings = {
1
+ export type AttributeValue = string | number | boolean | null | undefined;
2
+ export type Attributes = Record<string, AttributeValue>;
3
+ export type ObservabilitySettings = {
4
4
  serviceName: string;
5
5
  environment: string;
6
6
  deploymentRevision?: string | undefined;
@@ -9,12 +9,12 @@ type ObservabilitySettings = {
9
9
  observabilityOtlpEndpoint?: string | undefined;
10
10
  observabilityOtlpHeaders: string;
11
11
  };
12
- type ObservabilityOptions = {
12
+ export type ObservabilityOptions = {
13
13
  component: string;
14
14
  now?: () => number;
15
15
  exporter?: (url: string, body: unknown, headers: Record<string, string>) => Promise<void>;
16
16
  };
17
- type Span = {
17
+ export type Span = {
18
18
  traceId: string;
19
19
  spanId: string;
20
20
  end: (input?: {
@@ -22,9 +22,15 @@ type Span = {
22
22
  error?: unknown;
23
23
  }) => void;
24
24
  };
25
- type MetricLabels = Record<string, AttributeValue>;
26
- declare function createObservability(settings: ObservabilitySettings, options: ObservabilityOptions): Observability;
27
- declare class Observability {
25
+ export type MetricLabels = Record<string, AttributeValue>;
26
+ export type SandboxOperationMetricObservation = {
27
+ backend: string;
28
+ op: string;
29
+ outcome: "ok" | "failed";
30
+ durationMs: number;
31
+ };
32
+ export declare function createObservability(settings: ObservabilitySettings, options: ObservabilityOptions): Observability;
33
+ export declare class Observability {
28
34
  private readonly settings;
29
35
  private readonly options;
30
36
  private readonly registry;
@@ -86,14 +92,24 @@ declare class Observability {
86
92
  private assertRegistration;
87
93
  private exportSpan;
88
94
  }
89
- type StartupDependencyRetryEvent = {
95
+ /**
96
+ * Convert routed sandbox-provider observations into one bounded Prometheus
97
+ * contract shared by API-direct and worker-turn execution. Provider/session
98
+ * identifiers, commands, paths, and other request data can never become labels:
99
+ * a future backend or operation is deliberately collapsed to `unknown` until
100
+ * this allowlist is reviewed.
101
+ *
102
+ * The returned callback is fail-safe. Metrics must never change the provider
103
+ * operation's result; a registration/exporter error is counted best-effort and
104
+ * then swallowed at this telemetry boundary.
105
+ */
106
+ export declare function sandboxOperationMetricObserver(observability: Observability): (observation: SandboxOperationMetricObservation) => void;
107
+ export type StartupDependencyRetryEvent = {
90
108
  label: string;
91
109
  attempt: number;
92
110
  attempts: number;
93
111
  delayMs: number;
94
112
  error: unknown;
95
113
  };
96
- declare function logStartupDependencyRetry(observability: Observability, event: StartupDependencyRetryEvent): void;
97
- declare function parseHeaders(value: string): Record<string, string>;
98
-
99
- export { type AttributeValue, type Attributes, type MetricLabels, Observability, type ObservabilityOptions, type ObservabilitySettings, type Span, type StartupDependencyRetryEvent, createObservability, logStartupDependencyRetry, parseHeaders };
114
+ export declare function logStartupDependencyRetry(observability: Observability, event: StartupDependencyRetryEvent): void;
115
+ export declare function parseHeaders(value: string): Record<string, string>;
package/dist/index.js CHANGED
@@ -1,13 +1,45 @@
1
1
  // src/index.ts
2
- import {
3
- collectDefaultMetrics,
4
- Counter,
5
- Gauge,
6
- Histogram,
7
- Registry
8
- } from "prom-client";
2
+ import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from "prom-client";
3
+ import { SandboxBackend } from "@opengeni/contracts";
9
4
  var httpHistogramBuckets = [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
10
- var durationHistogramBuckets = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 900, 1800, 3600];
5
+ var durationHistogramBuckets = [
6
+ 0.01,
7
+ 0.05,
8
+ 0.1,
9
+ 0.25,
10
+ 0.5,
11
+ 1,
12
+ 2.5,
13
+ 5,
14
+ 10,
15
+ 30,
16
+ 60,
17
+ 120,
18
+ 300,
19
+ 900,
20
+ 1800,
21
+ 3600
22
+ ];
23
+ var SANDBOX_OPERATION_BACKENDS = /* @__PURE__ */ new Set([...SandboxBackend.options, "unprovisioned"]);
24
+ var SANDBOX_OPERATION_NAMES = /* @__PURE__ */ new Set([
25
+ "desktopInput",
26
+ "screenshot",
27
+ "exec",
28
+ "execCommand",
29
+ "writeStdin",
30
+ "cancelExecCommand",
31
+ "readFile",
32
+ "writeFile",
33
+ "listDir",
34
+ "pathExists",
35
+ "viewImage",
36
+ "materializeEntry",
37
+ "editor.createFile",
38
+ "editor.updateFile",
39
+ "editor.deleteFile",
40
+ "resolveExposedPort",
41
+ "serializeSessionState"
42
+ ]);
11
43
  function createObservability(settings, options) {
12
44
  return new Observability(settings, options);
13
45
  }
@@ -40,8 +72,6 @@ var Observability = class {
40
72
  });
41
73
  }
42
74
  }
43
- settings;
44
- options;
45
75
  registry = new Registry();
46
76
  counters = /* @__PURE__ */ new Map();
47
77
  gauges = /* @__PURE__ */ new Map();
@@ -105,7 +135,8 @@ var Observability = class {
105
135
  return;
106
136
  }
107
137
  ended = true;
108
- const errorAttributes = input.error ? errorToAttributes(input.error) : {};
138
+ const sanitizedError = input.error !== void 0 && input.error !== null ? sanitizeSpanError(input.error) : void 0;
139
+ const errorAttributes = sanitizedError ? errorToAttributes(sanitizedError) : {};
109
140
  this.exportSpan({
110
141
  traceId,
111
142
  spanId,
@@ -117,7 +148,7 @@ var Observability = class {
117
148
  ...input.attributes,
118
149
  ...errorAttributes
119
150
  },
120
- error: input.error
151
+ ...sanitizedError ? { error: sanitizedError } : {}
121
152
  });
122
153
  }
123
154
  };
@@ -171,7 +202,11 @@ var Observability = class {
171
202
  return;
172
203
  }
173
204
  const labels = normalizeLabels(input.labels);
174
- const counter = this.counter(input.name, input.help ?? `${input.name} counter.`, Object.keys(labels));
205
+ const counter = this.counter(
206
+ input.name,
207
+ input.help ?? `${input.name} counter.`,
208
+ Object.keys(labels)
209
+ );
175
210
  counter.inc(labels, input.amount ?? 1);
176
211
  }
177
212
  setGauge(input) {
@@ -261,33 +296,69 @@ var Observability = class {
261
296
  }
262
297
  const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\/$/, "")}/v1/traces`;
263
298
  const body = {
264
- resourceSpans: [{
265
- resource: {
266
- attributes: otlpAttributes(this.resourceAttributes)
267
- },
268
- scopeSpans: [{
269
- scope: {
270
- name: "@opengeni/observability",
271
- version: "0.1.0"
299
+ resourceSpans: [
300
+ {
301
+ resource: {
302
+ attributes: otlpAttributes(this.resourceAttributes)
272
303
  },
273
- spans: [{
274
- traceId: span.traceId,
275
- spanId: span.spanId,
276
- name: span.name,
277
- kind: 1,
278
- startTimeUnixNano: millisToNanos(span.startMs),
279
- endTimeUnixNano: millisToNanos(span.endMs),
280
- attributes: otlpAttributes(span.attributes),
281
- status: span.error ? { code: 2, message: errorMessage(span.error) } : { code: 1 }
282
- }]
283
- }]
284
- }]
304
+ scopeSpans: [
305
+ {
306
+ scope: {
307
+ name: "@opengeni/observability",
308
+ version: "0.1.0"
309
+ },
310
+ spans: [
311
+ {
312
+ traceId: span.traceId,
313
+ spanId: span.spanId,
314
+ name: span.name,
315
+ kind: 1,
316
+ startTimeUnixNano: millisToNanos(span.startMs),
317
+ endTimeUnixNano: millisToNanos(span.endMs),
318
+ attributes: otlpAttributes(span.attributes),
319
+ status: span.error ? { code: 2, message: span.error.statusMessage } : { code: 1 }
320
+ }
321
+ ]
322
+ }
323
+ ]
324
+ }
325
+ ]
285
326
  };
286
- void this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders)).catch((error) => {
287
- this.warn("OTLP span export failed", { error: errorMessage(error), endpoint });
288
- });
327
+ void this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders)).catch(
328
+ (error) => {
329
+ this.warn("OTLP span export failed", { error: errorMessage(error), endpoint });
330
+ }
331
+ );
289
332
  }
290
333
  };
334
+ function sandboxOperationMetricObserver(observability) {
335
+ return (observation) => {
336
+ const backend = SANDBOX_OPERATION_BACKENDS.has(observation.backend) ? observation.backend : "unknown";
337
+ const op = SANDBOX_OPERATION_NAMES.has(observation.op) ? observation.op : "unknown";
338
+ try {
339
+ observability.incrementCounter({
340
+ name: "opengeni_sandbox_operations_total",
341
+ help: "Physical routed sandbox provider operations by backend, operation, and outcome.",
342
+ labels: { backend, op, outcome: observation.outcome }
343
+ });
344
+ observability.observeHistogram({
345
+ name: "opengeni_sandbox_operation_duration_seconds",
346
+ help: "Physical routed sandbox provider-operation duration in seconds.",
347
+ labels: { backend, op },
348
+ value: Math.max(0, observation.durationMs) / 1e3
349
+ });
350
+ } catch {
351
+ try {
352
+ observability.incrementCounter({
353
+ name: "opengeni_observability_observer_errors_total",
354
+ help: "Observability observer failures isolated from product execution.",
355
+ labels: { observer: "sandbox_operation" }
356
+ });
357
+ } catch {
358
+ }
359
+ }
360
+ };
361
+ }
291
362
  function logStartupDependencyRetry(observability, event) {
292
363
  const message = event.error instanceof Error ? event.error.message : String(event.error);
293
364
  observability.warn("Startup dependency connection failed; retrying", {
@@ -307,12 +378,29 @@ function buildVersion() {
307
378
  return process.env.OPENGENI_VERSION ?? process.env.npm_package_version ?? "dev";
308
379
  }
309
380
  function cleanAttributes(attributes) {
310
- return Object.fromEntries(Object.entries(attributes).filter(([, value]) => value !== void 0));
381
+ return Object.fromEntries(
382
+ Object.entries(attributes).filter(([, value]) => value !== void 0)
383
+ );
384
+ }
385
+ function sanitizeSpanError(error) {
386
+ const type = error instanceof Error && /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(error.name) ? error.name : "Error";
387
+ const statusCode = errorStatusCode(error);
388
+ return {
389
+ type,
390
+ ...statusCode === void 0 ? {} : { statusCode },
391
+ statusMessage: statusCode === void 0 ? "operation failed" : `HTTP ${statusCode}`
392
+ };
393
+ }
394
+ function errorStatusCode(error) {
395
+ if (typeof error !== "object" || error === null) return void 0;
396
+ const value = error.status;
397
+ const statusCode = Number.isInteger(value) && typeof value === "number" ? value : error.statusCode;
398
+ return Number.isInteger(statusCode) && typeof statusCode === "number" && statusCode >= 100 && statusCode <= 599 ? statusCode : void 0;
311
399
  }
312
400
  function errorToAttributes(error) {
313
401
  return {
314
- "error.type": error instanceof Error ? error.name : "Error",
315
- "error.message": errorMessage(error)
402
+ "error.type": error.type,
403
+ ...error.statusCode === void 0 ? {} : { "error.status_code": error.statusCode }
316
404
  };
317
405
  }
318
406
  function errorMessage(error) {
@@ -331,7 +419,12 @@ function otlpValue(value) {
331
419
  if (typeof value === "boolean") {
332
420
  return { boolValue: value };
333
421
  }
334
- return { stringValue: value === null ? "" : value };
422
+ return { stringValue: value === null ? "" : boundedOtlpString(value) };
423
+ }
424
+ function boundedOtlpString(value) {
425
+ const bytes = new TextEncoder().encode(value);
426
+ if (bytes.byteLength <= 512) return value;
427
+ return `${new TextDecoder().decode(bytes.slice(0, 509))}\u2026`;
335
428
  }
336
429
  function millisToNanos(ms) {
337
430
  return String(BigInt(Math.round(ms)) * 1000000n);
@@ -370,6 +463,7 @@ export {
370
463
  Observability,
371
464
  createObservability,
372
465
  logStartupDependencyRetry,
373
- parseHeaders
466
+ parseHeaders,
467
+ sandboxOperationMetricObserver
374
468
  };
375
469
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n collectDefaultMetrics,\n Counter,\n Gauge,\n Histogram,\n Registry,\n} from \"prom-client\";\n\nexport type AttributeValue = string | number | boolean | null | undefined;\nexport type Attributes = Record<string, AttributeValue>;\n\nexport type ObservabilitySettings = {\n serviceName: string;\n environment: string;\n deploymentRevision?: string | undefined;\n observabilityStructuredLogs: boolean;\n observabilityMetricsEnabled: boolean;\n observabilityOtlpEndpoint?: string | undefined;\n observabilityOtlpHeaders: string;\n};\n\nexport type ObservabilityOptions = {\n component: string;\n now?: () => number;\n exporter?: (url: string, body: unknown, headers: Record<string, string>) => Promise<void>;\n};\n\nexport type Span = {\n traceId: string;\n spanId: string;\n end: (input?: { attributes?: Attributes; error?: unknown }) => void;\n};\n\nexport type MetricLabels = Record<string, AttributeValue>;\n\nconst httpHistogramBuckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];\nconst durationHistogramBuckets = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 900, 1800, 3600];\n\nexport function createObservability(settings: ObservabilitySettings, options: ObservabilityOptions): Observability {\n return new Observability(settings, options);\n}\n\ntype MetricRegistration = {\n kind: \"counter\" | \"gauge\" | \"histogram\";\n labelNames: string[];\n};\n\nexport class Observability {\n private readonly registry = new Registry();\n private readonly counters = new Map<string, Counter<string>>();\n private readonly gauges = new Map<string, Gauge<string>>();\n private readonly histograms = new Map<string, Histogram<string>>();\n private readonly registrations = new Map<string, MetricRegistration>();\n private readonly now: () => number;\n private readonly exporter: (url: string, body: unknown, headers: Record<string, string>) => Promise<void>;\n private readonly resourceAttributes: Attributes;\n\n constructor(private readonly settings: ObservabilitySettings, private readonly options: ObservabilityOptions) {\n this.now = options.now ?? Date.now;\n this.exporter = options.exporter ?? defaultExporter;\n this.resourceAttributes = {\n \"service.name\": settings.serviceName,\n \"deployment.environment\": settings.environment,\n \"opengeni.component\": options.component,\n };\n this.registry.setDefaultLabels({\n service: settings.serviceName,\n environment: settings.environment,\n component: options.component,\n });\n if (settings.observabilityMetricsEnabled) {\n collectDefaultMetrics({ register: this.registry, prefix: \"opengeni_\" });\n this.setGauge({\n name: \"opengeni_build_info\",\n help: \"OpenGeni build information.\",\n labels: {\n version: buildVersion(),\n revision: settings.deploymentRevision ?? \"dev\",\n },\n value: 1,\n });\n }\n }\n\n debug(message: string, attributes: Attributes = {}): void {\n this.log(\"debug\", message, attributes);\n }\n\n info(message: string, attributes: Attributes = {}): void {\n this.log(\"info\", message, attributes);\n }\n\n warn(message: string, attributes: Attributes = {}): void {\n this.log(\"warn\", message, attributes);\n }\n\n error(message: string, attributes: Attributes = {}): void {\n this.log(\"error\", message, attributes);\n }\n\n log(level: \"debug\" | \"info\" | \"warn\" | \"error\", message: string, attributes: Attributes = {}): void {\n if (!this.settings.observabilityStructuredLogs) {\n const line = attributes.error ? `${message}: ${String(attributes.error)}` : message;\n if (level === \"warn\") {\n console.warn(line);\n } else if (level === \"error\") {\n console.error(line);\n } else {\n console.log(line);\n }\n return;\n }\n const record = {\n timestamp: new Date(this.now()).toISOString(),\n level,\n message,\n service: this.settings.serviceName,\n environment: this.settings.environment,\n component: this.options.component,\n ...cleanAttributes(attributes),\n };\n const serialized = JSON.stringify(record);\n if (level === \"warn\") {\n console.warn(serialized);\n } else if (level === \"error\") {\n console.error(serialized);\n } else {\n console.log(serialized);\n }\n }\n\n startSpan(name: string, attributes: Attributes = {}): Span {\n const traceId = randomHex(16);\n const spanId = randomHex(8);\n const startMs = this.now();\n let ended = false;\n return {\n traceId,\n spanId,\n end: (input = {}) => {\n if (ended) {\n return;\n }\n ended = true;\n const errorAttributes = input.error ? errorToAttributes(input.error) : {};\n this.exportSpan({\n traceId,\n spanId,\n name,\n startMs,\n endMs: this.now(),\n attributes: {\n ...attributes,\n ...input.attributes,\n ...errorAttributes,\n },\n error: input.error,\n });\n },\n };\n }\n\n recordHttpRequest(input: { method: string; route: string; status: number; durationSeconds: number }): void {\n this.incrementCounter({\n name: \"opengeni_http_requests_total\",\n help: \"Total HTTP requests handled by OpenGeni.\",\n labels: {\n method: input.method,\n route: input.route,\n status: String(input.status),\n component: this.options.component,\n },\n });\n this.observeHistogram({\n name: \"opengeni_http_request_duration_seconds\",\n help: \"HTTP request duration in seconds.\",\n buckets: httpHistogramBuckets,\n value: input.durationSeconds,\n labels: {\n method: input.method,\n route: input.route,\n component: this.options.component,\n },\n });\n }\n\n recordWorkerActivity(input: { activity: string; status: string; durationSeconds: number }): void {\n this.incrementCounter({\n name: \"opengeni_worker_activity_runs_total\",\n help: \"Total worker activity executions.\",\n labels: {\n activity: input.activity,\n status: input.status,\n component: this.options.component,\n },\n });\n this.observeHistogram({\n name: \"opengeni_worker_activity_duration_seconds\",\n help: \"Worker activity duration in seconds.\",\n buckets: durationHistogramBuckets,\n value: input.durationSeconds,\n labels: {\n activity: input.activity,\n component: this.options.component,\n },\n });\n }\n\n incrementCounter(input: { name: string; help?: string; labels?: MetricLabels; amount?: number }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const counter = this.counter(input.name, input.help ?? `${input.name} counter.`, Object.keys(labels));\n counter.inc(labels as never, input.amount ?? 1);\n }\n\n setGauge(input: { name: string; help?: string; labels?: MetricLabels; value: number }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const gauge = this.gauge(input.name, input.help ?? `${input.name} gauge.`, Object.keys(labels));\n gauge.set(labels as never, input.value);\n }\n\n incrementGauge(input: { name: string; help?: string; labels?: MetricLabels; amount?: number }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const gauge = this.gauge(input.name, input.help ?? `${input.name} gauge.`, Object.keys(labels));\n gauge.inc(labels as never, input.amount ?? 1);\n }\n\n observeHistogram(input: { name: string; help?: string; labels?: MetricLabels; value: number; buckets?: number[] }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const histogram = this.histogram(\n input.name,\n input.help ?? `${input.name} histogram.`,\n Object.keys(labels),\n input.buckets ?? durationHistogramBuckets,\n );\n histogram.observe(labels as never, input.value);\n }\n\n async prometheusMetrics(): Promise<string> {\n if (!this.settings.observabilityMetricsEnabled) {\n return \"\";\n }\n return await this.registry.metrics();\n }\n\n private counter(name: string, help: string, labelNames: string[]): Counter<string> {\n const existing = this.counters.get(name);\n if (existing) {\n this.assertRegistration(name, \"counter\", labelNames);\n return existing;\n }\n this.register(name, \"counter\", labelNames);\n const metric = new Counter({ name, help, labelNames, registers: [this.registry] });\n this.counters.set(name, metric);\n return metric;\n }\n\n private gauge(name: string, help: string, labelNames: string[]): Gauge<string> {\n const existing = this.gauges.get(name);\n if (existing) {\n this.assertRegistration(name, \"gauge\", labelNames);\n return existing;\n }\n this.register(name, \"gauge\", labelNames);\n const metric = new Gauge({ name, help, labelNames, registers: [this.registry] });\n this.gauges.set(name, metric);\n return metric;\n }\n\n private histogram(name: string, help: string, labelNames: string[], buckets: number[]): Histogram<string> {\n const existing = this.histograms.get(name);\n if (existing) {\n this.assertRegistration(name, \"histogram\", labelNames);\n return existing;\n }\n this.register(name, \"histogram\", labelNames);\n const metric = new Histogram({ name, help, labelNames, buckets, registers: [this.registry] });\n this.histograms.set(name, metric);\n return metric;\n }\n\n private register(name: string, kind: MetricRegistration[\"kind\"], labelNames: string[]): void {\n const sorted = [...labelNames].sort();\n this.registrations.set(name, { kind, labelNames: sorted });\n }\n\n private assertRegistration(name: string, kind: MetricRegistration[\"kind\"], labelNames: string[]): void {\n const registration = this.registrations.get(name);\n const sorted = [...labelNames].sort();\n if (\n !registration\n || registration.kind !== kind\n || registration.labelNames.length !== sorted.length\n || registration.labelNames.some((label, index) => label !== sorted[index])\n ) {\n throw new Error(\n `Metric ${name} was already registered as ${registration?.kind ?? \"unknown\"} `\n + `with labels [${registration?.labelNames.join(\",\") ?? \"\"}], not ${kind} [${sorted.join(\",\")}]`,\n );\n }\n }\n\n private exportSpan(span: {\n traceId: string;\n spanId: string;\n name: string;\n startMs: number;\n endMs: number;\n attributes: Attributes;\n error?: unknown;\n }): void {\n if (!this.settings.observabilityOtlpEndpoint) {\n return;\n }\n const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\\/$/, \"\")}/v1/traces`;\n const body = {\n resourceSpans: [{\n resource: {\n attributes: otlpAttributes(this.resourceAttributes),\n },\n scopeSpans: [{\n scope: {\n name: \"@opengeni/observability\",\n version: \"0.1.0\",\n },\n spans: [{\n traceId: span.traceId,\n spanId: span.spanId,\n name: span.name,\n kind: 1,\n startTimeUnixNano: millisToNanos(span.startMs),\n endTimeUnixNano: millisToNanos(span.endMs),\n attributes: otlpAttributes(span.attributes),\n status: span.error ? { code: 2, message: errorMessage(span.error) } : { code: 1 },\n }],\n }],\n }],\n };\n void this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders)).catch((error) => {\n this.warn(\"OTLP span export failed\", { error: errorMessage(error), endpoint });\n });\n }\n}\n\nexport type StartupDependencyRetryEvent = {\n label: string;\n attempt: number;\n attempts: number;\n delayMs: number;\n error: unknown;\n};\n\nexport function logStartupDependencyRetry(observability: Observability, event: StartupDependencyRetryEvent): void {\n const message = event.error instanceof Error ? event.error.message : String(event.error);\n observability.warn(\"Startup dependency connection failed; retrying\", {\n dependency: event.label,\n attempt: event.attempt,\n attempts: event.attempts,\n delayMs: event.delayMs,\n error: message,\n });\n}\n\nfunction normalizeLabels(labels: MetricLabels = {}): Record<string, string> {\n return Object.fromEntries(\n Object.entries(labels)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]): [string, string] => [key, String(value)])\n .sort((left, right) => left[0].localeCompare(right[0])),\n );\n}\n\nfunction buildVersion(): string {\n return process.env.OPENGENI_VERSION\n ?? process.env.npm_package_version\n ?? \"dev\";\n}\n\nfunction cleanAttributes(attributes: Attributes): Record<string, string | number | boolean | null> {\n return Object.fromEntries(Object.entries(attributes).filter(([, value]) => value !== undefined)) as Record<string, string | number | boolean | null>;\n}\n\nfunction errorToAttributes(error: unknown): Attributes {\n return {\n \"error.type\": error instanceof Error ? error.name : \"Error\",\n \"error.message\": errorMessage(error),\n };\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction otlpAttributes(attributes: Attributes): Array<{ key: string; value: Record<string, string | number | boolean> }> {\n return Object.entries(cleanAttributes(attributes)).map(([key, value]) => ({\n key,\n value: otlpValue(value),\n }));\n}\n\nfunction otlpValue(value: string | number | boolean | null): Record<string, string | number | boolean> {\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };\n }\n if (typeof value === \"boolean\") {\n return { boolValue: value };\n }\n return { stringValue: value === null ? \"\" : value };\n}\n\nfunction millisToNanos(ms: number): string {\n return String(BigInt(Math.round(ms)) * 1_000_000n);\n}\n\nfunction randomHex(bytes: number): string {\n const values = crypto.getRandomValues(new Uint8Array(bytes));\n return Array.from(values, (value) => value.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport function parseHeaders(value: string): Record<string, string> {\n if (!value.trim()) {\n return {};\n }\n const entries: Array<[string, string]> = value.split(\",\").map((pair): [string, string] => {\n const separator = pair.indexOf(\"=\");\n if (separator === -1) {\n return [pair.trim(), \"\"];\n }\n return [pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()];\n }).filter(([key]) => key.length > 0);\n return Object.fromEntries(entries);\n}\n\nasync function defaultExporter(url: string, body: unknown, headers: Record<string, string>): Promise<void> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n });\n if (!response.ok) {\n throw new Error(`OTLP endpoint returned HTTP ${response.status}`);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from \"prom-client\";\nimport { SandboxBackend } from \"@opengeni/contracts\";\n\nexport type AttributeValue = string | number | boolean | null | undefined;\nexport type Attributes = Record<string, AttributeValue>;\n\nexport type ObservabilitySettings = {\n serviceName: string;\n environment: string;\n deploymentRevision?: string | undefined;\n observabilityStructuredLogs: boolean;\n observabilityMetricsEnabled: boolean;\n observabilityOtlpEndpoint?: string | undefined;\n observabilityOtlpHeaders: string;\n};\n\nexport type ObservabilityOptions = {\n component: string;\n now?: () => number;\n exporter?: (url: string, body: unknown, headers: Record<string, string>) => Promise<void>;\n};\n\nexport type Span = {\n traceId: string;\n spanId: string;\n end: (input?: { attributes?: Attributes; error?: unknown }) => void;\n};\n\nexport type MetricLabels = Record<string, AttributeValue>;\n\nconst httpHistogramBuckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];\nconst durationHistogramBuckets = [\n 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 900, 1800, 3600,\n];\n\nconst SANDBOX_OPERATION_BACKENDS = new Set<string>([...SandboxBackend.options, \"unprovisioned\"]);\n\nconst SANDBOX_OPERATION_NAMES = new Set([\n \"desktopInput\",\n \"screenshot\",\n \"exec\",\n \"execCommand\",\n \"writeStdin\",\n \"cancelExecCommand\",\n \"readFile\",\n \"writeFile\",\n \"listDir\",\n \"pathExists\",\n \"viewImage\",\n \"materializeEntry\",\n \"editor.createFile\",\n \"editor.updateFile\",\n \"editor.deleteFile\",\n \"resolveExposedPort\",\n \"serializeSessionState\",\n]);\n\nexport type SandboxOperationMetricObservation = {\n backend: string;\n op: string;\n outcome: \"ok\" | \"failed\";\n durationMs: number;\n};\n\nexport function createObservability(\n settings: ObservabilitySettings,\n options: ObservabilityOptions,\n): Observability {\n return new Observability(settings, options);\n}\n\ntype MetricRegistration = {\n kind: \"counter\" | \"gauge\" | \"histogram\";\n labelNames: string[];\n};\n\nexport class Observability {\n private readonly registry = new Registry();\n private readonly counters = new Map<string, Counter<string>>();\n private readonly gauges = new Map<string, Gauge<string>>();\n private readonly histograms = new Map<string, Histogram<string>>();\n private readonly registrations = new Map<string, MetricRegistration>();\n private readonly now: () => number;\n private readonly exporter: (\n url: string,\n body: unknown,\n headers: Record<string, string>,\n ) => Promise<void>;\n private readonly resourceAttributes: Attributes;\n\n constructor(\n private readonly settings: ObservabilitySettings,\n private readonly options: ObservabilityOptions,\n ) {\n this.now = options.now ?? Date.now;\n this.exporter = options.exporter ?? defaultExporter;\n this.resourceAttributes = {\n \"service.name\": settings.serviceName,\n \"deployment.environment\": settings.environment,\n \"opengeni.component\": options.component,\n };\n this.registry.setDefaultLabels({\n service: settings.serviceName,\n environment: settings.environment,\n component: options.component,\n });\n if (settings.observabilityMetricsEnabled) {\n collectDefaultMetrics({ register: this.registry, prefix: \"opengeni_\" });\n this.setGauge({\n name: \"opengeni_build_info\",\n help: \"OpenGeni build information.\",\n labels: {\n version: buildVersion(),\n revision: settings.deploymentRevision ?? \"dev\",\n },\n value: 1,\n });\n }\n }\n\n debug(message: string, attributes: Attributes = {}): void {\n this.log(\"debug\", message, attributes);\n }\n\n info(message: string, attributes: Attributes = {}): void {\n this.log(\"info\", message, attributes);\n }\n\n warn(message: string, attributes: Attributes = {}): void {\n this.log(\"warn\", message, attributes);\n }\n\n error(message: string, attributes: Attributes = {}): void {\n this.log(\"error\", message, attributes);\n }\n\n log(\n level: \"debug\" | \"info\" | \"warn\" | \"error\",\n message: string,\n attributes: Attributes = {},\n ): void {\n if (!this.settings.observabilityStructuredLogs) {\n const line = attributes.error ? `${message}: ${String(attributes.error)}` : message;\n if (level === \"warn\") {\n console.warn(line);\n } else if (level === \"error\") {\n console.error(line);\n } else {\n console.log(line);\n }\n return;\n }\n const record = {\n timestamp: new Date(this.now()).toISOString(),\n level,\n message,\n service: this.settings.serviceName,\n environment: this.settings.environment,\n component: this.options.component,\n ...cleanAttributes(attributes),\n };\n const serialized = JSON.stringify(record);\n if (level === \"warn\") {\n console.warn(serialized);\n } else if (level === \"error\") {\n console.error(serialized);\n } else {\n console.log(serialized);\n }\n }\n\n startSpan(name: string, attributes: Attributes = {}): Span {\n const traceId = randomHex(16);\n const spanId = randomHex(8);\n const startMs = this.now();\n let ended = false;\n return {\n traceId,\n spanId,\n end: (input = {}) => {\n if (ended) {\n return;\n }\n ended = true;\n const sanitizedError =\n input.error !== undefined && input.error !== null\n ? sanitizeSpanError(input.error)\n : undefined;\n const errorAttributes = sanitizedError ? errorToAttributes(sanitizedError) : {};\n this.exportSpan({\n traceId,\n spanId,\n name,\n startMs,\n endMs: this.now(),\n attributes: {\n ...attributes,\n ...input.attributes,\n ...errorAttributes,\n },\n ...(sanitizedError ? { error: sanitizedError } : {}),\n });\n },\n };\n }\n\n recordHttpRequest(input: {\n method: string;\n route: string;\n status: number;\n durationSeconds: number;\n }): void {\n this.incrementCounter({\n name: \"opengeni_http_requests_total\",\n help: \"Total HTTP requests handled by OpenGeni.\",\n labels: {\n method: input.method,\n route: input.route,\n status: String(input.status),\n component: this.options.component,\n },\n });\n this.observeHistogram({\n name: \"opengeni_http_request_duration_seconds\",\n help: \"HTTP request duration in seconds.\",\n buckets: httpHistogramBuckets,\n value: input.durationSeconds,\n labels: {\n method: input.method,\n route: input.route,\n component: this.options.component,\n },\n });\n }\n\n recordWorkerActivity(input: { activity: string; status: string; durationSeconds: number }): void {\n this.incrementCounter({\n name: \"opengeni_worker_activity_runs_total\",\n help: \"Total worker activity executions.\",\n labels: {\n activity: input.activity,\n status: input.status,\n component: this.options.component,\n },\n });\n this.observeHistogram({\n name: \"opengeni_worker_activity_duration_seconds\",\n help: \"Worker activity duration in seconds.\",\n buckets: durationHistogramBuckets,\n value: input.durationSeconds,\n labels: {\n activity: input.activity,\n component: this.options.component,\n },\n });\n }\n\n incrementCounter(input: {\n name: string;\n help?: string;\n labels?: MetricLabels;\n amount?: number;\n }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const counter = this.counter(\n input.name,\n input.help ?? `${input.name} counter.`,\n Object.keys(labels),\n );\n counter.inc(labels as never, input.amount ?? 1);\n }\n\n setGauge(input: { name: string; help?: string; labels?: MetricLabels; value: number }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const gauge = this.gauge(input.name, input.help ?? `${input.name} gauge.`, Object.keys(labels));\n gauge.set(labels as never, input.value);\n }\n\n incrementGauge(input: {\n name: string;\n help?: string;\n labels?: MetricLabels;\n amount?: number;\n }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const gauge = this.gauge(input.name, input.help ?? `${input.name} gauge.`, Object.keys(labels));\n gauge.inc(labels as never, input.amount ?? 1);\n }\n\n observeHistogram(input: {\n name: string;\n help?: string;\n labels?: MetricLabels;\n value: number;\n buckets?: number[];\n }): void {\n if (!this.settings.observabilityMetricsEnabled) {\n return;\n }\n const labels = normalizeLabels(input.labels);\n const histogram = this.histogram(\n input.name,\n input.help ?? `${input.name} histogram.`,\n Object.keys(labels),\n input.buckets ?? durationHistogramBuckets,\n );\n histogram.observe(labels as never, input.value);\n }\n\n async prometheusMetrics(): Promise<string> {\n if (!this.settings.observabilityMetricsEnabled) {\n return \"\";\n }\n return await this.registry.metrics();\n }\n\n private counter(name: string, help: string, labelNames: string[]): Counter<string> {\n const existing = this.counters.get(name);\n if (existing) {\n this.assertRegistration(name, \"counter\", labelNames);\n return existing;\n }\n this.register(name, \"counter\", labelNames);\n const metric = new Counter({ name, help, labelNames, registers: [this.registry] });\n this.counters.set(name, metric);\n return metric;\n }\n\n private gauge(name: string, help: string, labelNames: string[]): Gauge<string> {\n const existing = this.gauges.get(name);\n if (existing) {\n this.assertRegistration(name, \"gauge\", labelNames);\n return existing;\n }\n this.register(name, \"gauge\", labelNames);\n const metric = new Gauge({ name, help, labelNames, registers: [this.registry] });\n this.gauges.set(name, metric);\n return metric;\n }\n\n private histogram(\n name: string,\n help: string,\n labelNames: string[],\n buckets: number[],\n ): Histogram<string> {\n const existing = this.histograms.get(name);\n if (existing) {\n this.assertRegistration(name, \"histogram\", labelNames);\n return existing;\n }\n this.register(name, \"histogram\", labelNames);\n const metric = new Histogram({ name, help, labelNames, buckets, registers: [this.registry] });\n this.histograms.set(name, metric);\n return metric;\n }\n\n private register(name: string, kind: MetricRegistration[\"kind\"], labelNames: string[]): void {\n const sorted = [...labelNames].sort();\n this.registrations.set(name, { kind, labelNames: sorted });\n }\n\n private assertRegistration(\n name: string,\n kind: MetricRegistration[\"kind\"],\n labelNames: string[],\n ): void {\n const registration = this.registrations.get(name);\n const sorted = [...labelNames].sort();\n if (\n !registration ||\n registration.kind !== kind ||\n registration.labelNames.length !== sorted.length ||\n registration.labelNames.some((label, index) => label !== sorted[index])\n ) {\n throw new Error(\n `Metric ${name} was already registered as ${registration?.kind ?? \"unknown\"} ` +\n `with labels [${registration?.labelNames.join(\",\") ?? \"\"}], not ${kind} [${sorted.join(\",\")}]`,\n );\n }\n }\n\n private exportSpan(span: {\n traceId: string;\n spanId: string;\n name: string;\n startMs: number;\n endMs: number;\n attributes: Attributes;\n error?: SanitizedSpanError;\n }): void {\n if (!this.settings.observabilityOtlpEndpoint) {\n return;\n }\n const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\\/$/, \"\")}/v1/traces`;\n const body = {\n resourceSpans: [\n {\n resource: {\n attributes: otlpAttributes(this.resourceAttributes),\n },\n scopeSpans: [\n {\n scope: {\n name: \"@opengeni/observability\",\n version: \"0.1.0\",\n },\n spans: [\n {\n traceId: span.traceId,\n spanId: span.spanId,\n name: span.name,\n kind: 1,\n startTimeUnixNano: millisToNanos(span.startMs),\n endTimeUnixNano: millisToNanos(span.endMs),\n attributes: otlpAttributes(span.attributes),\n status: span.error ? { code: 2, message: span.error.statusMessage } : { code: 1 },\n },\n ],\n },\n ],\n },\n ],\n };\n void this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders)).catch(\n (error) => {\n this.warn(\"OTLP span export failed\", { error: errorMessage(error), endpoint });\n },\n );\n }\n}\n\n/**\n * Convert routed sandbox-provider observations into one bounded Prometheus\n * contract shared by API-direct and worker-turn execution. Provider/session\n * identifiers, commands, paths, and other request data can never become labels:\n * a future backend or operation is deliberately collapsed to `unknown` until\n * this allowlist is reviewed.\n *\n * The returned callback is fail-safe. Metrics must never change the provider\n * operation's result; a registration/exporter error is counted best-effort and\n * then swallowed at this telemetry boundary.\n */\nexport function sandboxOperationMetricObserver(\n observability: Observability,\n): (observation: SandboxOperationMetricObservation) => void {\n return (observation) => {\n const backend = SANDBOX_OPERATION_BACKENDS.has(observation.backend)\n ? observation.backend\n : \"unknown\";\n const op = SANDBOX_OPERATION_NAMES.has(observation.op) ? observation.op : \"unknown\";\n try {\n observability.incrementCounter({\n name: \"opengeni_sandbox_operations_total\",\n help: \"Physical routed sandbox provider operations by backend, operation, and outcome.\",\n labels: { backend, op, outcome: observation.outcome },\n });\n observability.observeHistogram({\n name: \"opengeni_sandbox_operation_duration_seconds\",\n help: \"Physical routed sandbox provider-operation duration in seconds.\",\n labels: { backend, op },\n value: Math.max(0, observation.durationMs) / 1_000,\n });\n } catch {\n try {\n observability.incrementCounter({\n name: \"opengeni_observability_observer_errors_total\",\n help: \"Observability observer failures isolated from product execution.\",\n labels: { observer: \"sandbox_operation\" },\n });\n } catch {\n // The metrics registry itself is unhealthy. Product execution remains\n // authoritative and must not inherit an observability failure.\n }\n }\n };\n}\n\nexport type StartupDependencyRetryEvent = {\n label: string;\n attempt: number;\n attempts: number;\n delayMs: number;\n error: unknown;\n};\n\nexport function logStartupDependencyRetry(\n observability: Observability,\n event: StartupDependencyRetryEvent,\n): void {\n const message = event.error instanceof Error ? event.error.message : String(event.error);\n observability.warn(\"Startup dependency connection failed; retrying\", {\n dependency: event.label,\n attempt: event.attempt,\n attempts: event.attempts,\n delayMs: event.delayMs,\n error: message,\n });\n}\n\nfunction normalizeLabels(labels: MetricLabels = {}): Record<string, string> {\n return Object.fromEntries(\n Object.entries(labels)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]): [string, string] => [key, String(value)])\n .sort((left, right) => left[0].localeCompare(right[0])),\n );\n}\n\nfunction buildVersion(): string {\n return process.env.OPENGENI_VERSION ?? process.env.npm_package_version ?? \"dev\";\n}\n\nfunction cleanAttributes(attributes: Attributes): Record<string, string | number | boolean | null> {\n return Object.fromEntries(\n Object.entries(attributes).filter(([, value]) => value !== undefined),\n ) as Record<string, string | number | boolean | null>;\n}\n\ntype SanitizedSpanError = {\n type: string;\n statusCode?: number;\n statusMessage: string;\n};\n\nfunction sanitizeSpanError(error: unknown): SanitizedSpanError {\n const type =\n error instanceof Error && /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(error.name)\n ? error.name\n : \"Error\";\n const statusCode = errorStatusCode(error);\n return {\n type,\n ...(statusCode === undefined ? {} : { statusCode }),\n statusMessage: statusCode === undefined ? \"operation failed\" : `HTTP ${statusCode}`,\n };\n}\n\nfunction errorStatusCode(error: unknown): number | undefined {\n if (typeof error !== \"object\" || error === null) return undefined;\n const value = (error as { status?: unknown; statusCode?: unknown }).status;\n const statusCode =\n Number.isInteger(value) && typeof value === \"number\"\n ? value\n : (error as { statusCode?: unknown }).statusCode;\n return Number.isInteger(statusCode) &&\n typeof statusCode === \"number\" &&\n statusCode >= 100 &&\n statusCode <= 599\n ? statusCode\n : undefined;\n}\n\nfunction errorToAttributes(error: SanitizedSpanError): Attributes {\n return {\n \"error.type\": error.type,\n ...(error.statusCode === undefined ? {} : { \"error.status_code\": error.statusCode }),\n };\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction otlpAttributes(\n attributes: Attributes,\n): Array<{ key: string; value: Record<string, string | number | boolean> }> {\n return Object.entries(cleanAttributes(attributes)).map(([key, value]) => ({\n key,\n value: otlpValue(value),\n }));\n}\n\nfunction otlpValue(\n value: string | number | boolean | null,\n): Record<string, string | number | boolean> {\n if (typeof value === \"number\") {\n return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };\n }\n if (typeof value === \"boolean\") {\n return { boolValue: value };\n }\n return { stringValue: value === null ? \"\" : boundedOtlpString(value) };\n}\n\nfunction boundedOtlpString(value: string): string {\n const bytes = new TextEncoder().encode(value);\n if (bytes.byteLength <= 512) return value;\n return `${new TextDecoder().decode(bytes.slice(0, 509))}…`;\n}\n\nfunction millisToNanos(ms: number): string {\n return String(BigInt(Math.round(ms)) * 1_000_000n);\n}\n\nfunction randomHex(bytes: number): string {\n const values = crypto.getRandomValues(new Uint8Array(bytes));\n return Array.from(values, (value) => value.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nexport function parseHeaders(value: string): Record<string, string> {\n if (!value.trim()) {\n return {};\n }\n const entries: Array<[string, string]> = value\n .split(\",\")\n .map((pair): [string, string] => {\n const separator = pair.indexOf(\"=\");\n if (separator === -1) {\n return [pair.trim(), \"\"];\n }\n return [pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()];\n })\n .filter(([key]) => key.length > 0);\n return Object.fromEntries(entries);\n}\n\nasync function defaultExporter(\n url: string,\n body: unknown,\n headers: Record<string, string>,\n): Promise<void> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...headers,\n },\n body: JSON.stringify(body),\n });\n if (!response.ok) {\n throw new Error(`OTLP endpoint returned HTTP ${response.status}`);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@opengeni/observability",
3
- "version": "0.3.0",
4
- "type": "module",
3
+ "version": "0.4.0",
5
4
  "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
8
+ "directory": "packages/observability"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "type": "module",
6
15
  "main": "./dist/index.js",
7
16
  "module": "./dist/index.js",
8
17
  "types": "./dist/index.d.ts",
@@ -12,25 +21,17 @@
12
21
  "import": "./dist/index.js"
13
22
  }
14
23
  },
15
- "files": [
16
- "dist",
17
- "src"
18
- ],
19
24
  "publishConfig": {
20
25
  "access": "public",
21
26
  "provenance": true
22
27
  },
23
28
  "scripts": {
24
- "build": "tsup",
29
+ "build": "bun ../../scripts/build-typescript-package.ts",
25
30
  "typecheck": "tsc --noEmit",
26
31
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
27
32
  },
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
31
- "directory": "packages/observability"
32
- },
33
33
  "dependencies": {
34
+ "@opengeni/contracts": "^0.28.1",
34
35
  "prom-client": "^15.1.3"
35
36
  }
36
37
  }
package/src/index.ts CHANGED
@@ -1,10 +1,5 @@
1
- import {
2
- collectDefaultMetrics,
3
- Counter,
4
- Gauge,
5
- Histogram,
6
- Registry,
7
- } from "prom-client";
1
+ import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from "prom-client";
2
+ import { SandboxBackend } from "@opengeni/contracts";
8
3
 
9
4
  export type AttributeValue = string | number | boolean | null | undefined;
10
5
  export type Attributes = Record<string, AttributeValue>;
@@ -34,9 +29,43 @@ export type Span = {
34
29
  export type MetricLabels = Record<string, AttributeValue>;
35
30
 
36
31
  const httpHistogramBuckets = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
37
- const durationHistogramBuckets = [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 900, 1800, 3600];
32
+ const durationHistogramBuckets = [
33
+ 0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300, 900, 1800, 3600,
34
+ ];
35
+
36
+ const SANDBOX_OPERATION_BACKENDS = new Set<string>([...SandboxBackend.options, "unprovisioned"]);
37
+
38
+ const SANDBOX_OPERATION_NAMES = new Set([
39
+ "desktopInput",
40
+ "screenshot",
41
+ "exec",
42
+ "execCommand",
43
+ "writeStdin",
44
+ "cancelExecCommand",
45
+ "readFile",
46
+ "writeFile",
47
+ "listDir",
48
+ "pathExists",
49
+ "viewImage",
50
+ "materializeEntry",
51
+ "editor.createFile",
52
+ "editor.updateFile",
53
+ "editor.deleteFile",
54
+ "resolveExposedPort",
55
+ "serializeSessionState",
56
+ ]);
57
+
58
+ export type SandboxOperationMetricObservation = {
59
+ backend: string;
60
+ op: string;
61
+ outcome: "ok" | "failed";
62
+ durationMs: number;
63
+ };
38
64
 
39
- export function createObservability(settings: ObservabilitySettings, options: ObservabilityOptions): Observability {
65
+ export function createObservability(
66
+ settings: ObservabilitySettings,
67
+ options: ObservabilityOptions,
68
+ ): Observability {
40
69
  return new Observability(settings, options);
41
70
  }
42
71
 
@@ -52,10 +81,17 @@ export class Observability {
52
81
  private readonly histograms = new Map<string, Histogram<string>>();
53
82
  private readonly registrations = new Map<string, MetricRegistration>();
54
83
  private readonly now: () => number;
55
- private readonly exporter: (url: string, body: unknown, headers: Record<string, string>) => Promise<void>;
84
+ private readonly exporter: (
85
+ url: string,
86
+ body: unknown,
87
+ headers: Record<string, string>,
88
+ ) => Promise<void>;
56
89
  private readonly resourceAttributes: Attributes;
57
90
 
58
- constructor(private readonly settings: ObservabilitySettings, private readonly options: ObservabilityOptions) {
91
+ constructor(
92
+ private readonly settings: ObservabilitySettings,
93
+ private readonly options: ObservabilityOptions,
94
+ ) {
59
95
  this.now = options.now ?? Date.now;
60
96
  this.exporter = options.exporter ?? defaultExporter;
61
97
  this.resourceAttributes = {
@@ -98,7 +134,11 @@ export class Observability {
98
134
  this.log("error", message, attributes);
99
135
  }
100
136
 
101
- log(level: "debug" | "info" | "warn" | "error", message: string, attributes: Attributes = {}): void {
137
+ log(
138
+ level: "debug" | "info" | "warn" | "error",
139
+ message: string,
140
+ attributes: Attributes = {},
141
+ ): void {
102
142
  if (!this.settings.observabilityStructuredLogs) {
103
143
  const line = attributes.error ? `${message}: ${String(attributes.error)}` : message;
104
144
  if (level === "warn") {
@@ -142,7 +182,11 @@ export class Observability {
142
182
  return;
143
183
  }
144
184
  ended = true;
145
- const errorAttributes = input.error ? errorToAttributes(input.error) : {};
185
+ const sanitizedError =
186
+ input.error !== undefined && input.error !== null
187
+ ? sanitizeSpanError(input.error)
188
+ : undefined;
189
+ const errorAttributes = sanitizedError ? errorToAttributes(sanitizedError) : {};
146
190
  this.exportSpan({
147
191
  traceId,
148
192
  spanId,
@@ -154,13 +198,18 @@ export class Observability {
154
198
  ...input.attributes,
155
199
  ...errorAttributes,
156
200
  },
157
- error: input.error,
201
+ ...(sanitizedError ? { error: sanitizedError } : {}),
158
202
  });
159
203
  },
160
204
  };
161
205
  }
162
206
 
163
- recordHttpRequest(input: { method: string; route: string; status: number; durationSeconds: number }): void {
207
+ recordHttpRequest(input: {
208
+ method: string;
209
+ route: string;
210
+ status: number;
211
+ durationSeconds: number;
212
+ }): void {
164
213
  this.incrementCounter({
165
214
  name: "opengeni_http_requests_total",
166
215
  help: "Total HTTP requests handled by OpenGeni.",
@@ -206,12 +255,21 @@ export class Observability {
206
255
  });
207
256
  }
208
257
 
209
- incrementCounter(input: { name: string; help?: string; labels?: MetricLabels; amount?: number }): void {
258
+ incrementCounter(input: {
259
+ name: string;
260
+ help?: string;
261
+ labels?: MetricLabels;
262
+ amount?: number;
263
+ }): void {
210
264
  if (!this.settings.observabilityMetricsEnabled) {
211
265
  return;
212
266
  }
213
267
  const labels = normalizeLabels(input.labels);
214
- const counter = this.counter(input.name, input.help ?? `${input.name} counter.`, Object.keys(labels));
268
+ const counter = this.counter(
269
+ input.name,
270
+ input.help ?? `${input.name} counter.`,
271
+ Object.keys(labels),
272
+ );
215
273
  counter.inc(labels as never, input.amount ?? 1);
216
274
  }
217
275
 
@@ -224,7 +282,12 @@ export class Observability {
224
282
  gauge.set(labels as never, input.value);
225
283
  }
226
284
 
227
- incrementGauge(input: { name: string; help?: string; labels?: MetricLabels; amount?: number }): void {
285
+ incrementGauge(input: {
286
+ name: string;
287
+ help?: string;
288
+ labels?: MetricLabels;
289
+ amount?: number;
290
+ }): void {
228
291
  if (!this.settings.observabilityMetricsEnabled) {
229
292
  return;
230
293
  }
@@ -233,7 +296,13 @@ export class Observability {
233
296
  gauge.inc(labels as never, input.amount ?? 1);
234
297
  }
235
298
 
236
- observeHistogram(input: { name: string; help?: string; labels?: MetricLabels; value: number; buckets?: number[] }): void {
299
+ observeHistogram(input: {
300
+ name: string;
301
+ help?: string;
302
+ labels?: MetricLabels;
303
+ value: number;
304
+ buckets?: number[];
305
+ }): void {
237
306
  if (!this.settings.observabilityMetricsEnabled) {
238
307
  return;
239
308
  }
@@ -278,7 +347,12 @@ export class Observability {
278
347
  return metric;
279
348
  }
280
349
 
281
- private histogram(name: string, help: string, labelNames: string[], buckets: number[]): Histogram<string> {
350
+ private histogram(
351
+ name: string,
352
+ help: string,
353
+ labelNames: string[],
354
+ buckets: number[],
355
+ ): Histogram<string> {
282
356
  const existing = this.histograms.get(name);
283
357
  if (existing) {
284
358
  this.assertRegistration(name, "histogram", labelNames);
@@ -295,18 +369,22 @@ export class Observability {
295
369
  this.registrations.set(name, { kind, labelNames: sorted });
296
370
  }
297
371
 
298
- private assertRegistration(name: string, kind: MetricRegistration["kind"], labelNames: string[]): void {
372
+ private assertRegistration(
373
+ name: string,
374
+ kind: MetricRegistration["kind"],
375
+ labelNames: string[],
376
+ ): void {
299
377
  const registration = this.registrations.get(name);
300
378
  const sorted = [...labelNames].sort();
301
379
  if (
302
- !registration
303
- || registration.kind !== kind
304
- || registration.labelNames.length !== sorted.length
305
- || registration.labelNames.some((label, index) => label !== sorted[index])
380
+ !registration ||
381
+ registration.kind !== kind ||
382
+ registration.labelNames.length !== sorted.length ||
383
+ registration.labelNames.some((label, index) => label !== sorted[index])
306
384
  ) {
307
385
  throw new Error(
308
- `Metric ${name} was already registered as ${registration?.kind ?? "unknown"} `
309
- + `with labels [${registration?.labelNames.join(",") ?? ""}], not ${kind} [${sorted.join(",")}]`,
386
+ `Metric ${name} was already registered as ${registration?.kind ?? "unknown"} ` +
387
+ `with labels [${registration?.labelNames.join(",") ?? ""}], not ${kind} [${sorted.join(",")}]`,
310
388
  );
311
389
  }
312
390
  }
@@ -318,41 +396,95 @@ export class Observability {
318
396
  startMs: number;
319
397
  endMs: number;
320
398
  attributes: Attributes;
321
- error?: unknown;
399
+ error?: SanitizedSpanError;
322
400
  }): void {
323
401
  if (!this.settings.observabilityOtlpEndpoint) {
324
402
  return;
325
403
  }
326
404
  const endpoint = `${this.settings.observabilityOtlpEndpoint.replace(/\/$/, "")}/v1/traces`;
327
405
  const body = {
328
- resourceSpans: [{
329
- resource: {
330
- attributes: otlpAttributes(this.resourceAttributes),
331
- },
332
- scopeSpans: [{
333
- scope: {
334
- name: "@opengeni/observability",
335
- version: "0.1.0",
406
+ resourceSpans: [
407
+ {
408
+ resource: {
409
+ attributes: otlpAttributes(this.resourceAttributes),
336
410
  },
337
- spans: [{
338
- traceId: span.traceId,
339
- spanId: span.spanId,
340
- name: span.name,
341
- kind: 1,
342
- startTimeUnixNano: millisToNanos(span.startMs),
343
- endTimeUnixNano: millisToNanos(span.endMs),
344
- attributes: otlpAttributes(span.attributes),
345
- status: span.error ? { code: 2, message: errorMessage(span.error) } : { code: 1 },
346
- }],
347
- }],
348
- }],
411
+ scopeSpans: [
412
+ {
413
+ scope: {
414
+ name: "@opengeni/observability",
415
+ version: "0.1.0",
416
+ },
417
+ spans: [
418
+ {
419
+ traceId: span.traceId,
420
+ spanId: span.spanId,
421
+ name: span.name,
422
+ kind: 1,
423
+ startTimeUnixNano: millisToNanos(span.startMs),
424
+ endTimeUnixNano: millisToNanos(span.endMs),
425
+ attributes: otlpAttributes(span.attributes),
426
+ status: span.error ? { code: 2, message: span.error.statusMessage } : { code: 1 },
427
+ },
428
+ ],
429
+ },
430
+ ],
431
+ },
432
+ ],
349
433
  };
350
- void this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders)).catch((error) => {
351
- this.warn("OTLP span export failed", { error: errorMessage(error), endpoint });
352
- });
434
+ void this.exporter(endpoint, body, parseHeaders(this.settings.observabilityOtlpHeaders)).catch(
435
+ (error) => {
436
+ this.warn("OTLP span export failed", { error: errorMessage(error), endpoint });
437
+ },
438
+ );
353
439
  }
354
440
  }
355
441
 
442
+ /**
443
+ * Convert routed sandbox-provider observations into one bounded Prometheus
444
+ * contract shared by API-direct and worker-turn execution. Provider/session
445
+ * identifiers, commands, paths, and other request data can never become labels:
446
+ * a future backend or operation is deliberately collapsed to `unknown` until
447
+ * this allowlist is reviewed.
448
+ *
449
+ * The returned callback is fail-safe. Metrics must never change the provider
450
+ * operation's result; a registration/exporter error is counted best-effort and
451
+ * then swallowed at this telemetry boundary.
452
+ */
453
+ export function sandboxOperationMetricObserver(
454
+ observability: Observability,
455
+ ): (observation: SandboxOperationMetricObservation) => void {
456
+ return (observation) => {
457
+ const backend = SANDBOX_OPERATION_BACKENDS.has(observation.backend)
458
+ ? observation.backend
459
+ : "unknown";
460
+ const op = SANDBOX_OPERATION_NAMES.has(observation.op) ? observation.op : "unknown";
461
+ try {
462
+ observability.incrementCounter({
463
+ name: "opengeni_sandbox_operations_total",
464
+ help: "Physical routed sandbox provider operations by backend, operation, and outcome.",
465
+ labels: { backend, op, outcome: observation.outcome },
466
+ });
467
+ observability.observeHistogram({
468
+ name: "opengeni_sandbox_operation_duration_seconds",
469
+ help: "Physical routed sandbox provider-operation duration in seconds.",
470
+ labels: { backend, op },
471
+ value: Math.max(0, observation.durationMs) / 1_000,
472
+ });
473
+ } catch {
474
+ try {
475
+ observability.incrementCounter({
476
+ name: "opengeni_observability_observer_errors_total",
477
+ help: "Observability observer failures isolated from product execution.",
478
+ labels: { observer: "sandbox_operation" },
479
+ });
480
+ } catch {
481
+ // The metrics registry itself is unhealthy. Product execution remains
482
+ // authoritative and must not inherit an observability failure.
483
+ }
484
+ }
485
+ };
486
+ }
487
+
356
488
  export type StartupDependencyRetryEvent = {
357
489
  label: string;
358
490
  attempt: number;
@@ -361,7 +493,10 @@ export type StartupDependencyRetryEvent = {
361
493
  error: unknown;
362
494
  };
363
495
 
364
- export function logStartupDependencyRetry(observability: Observability, event: StartupDependencyRetryEvent): void {
496
+ export function logStartupDependencyRetry(
497
+ observability: Observability,
498
+ event: StartupDependencyRetryEvent,
499
+ ): void {
365
500
  const message = event.error instanceof Error ? event.error.message : String(event.error);
366
501
  observability.warn("Startup dependency connection failed; retrying", {
367
502
  dependency: event.label,
@@ -382,19 +517,53 @@ function normalizeLabels(labels: MetricLabels = {}): Record<string, string> {
382
517
  }
383
518
 
384
519
  function buildVersion(): string {
385
- return process.env.OPENGENI_VERSION
386
- ?? process.env.npm_package_version
387
- ?? "dev";
520
+ return process.env.OPENGENI_VERSION ?? process.env.npm_package_version ?? "dev";
388
521
  }
389
522
 
390
523
  function cleanAttributes(attributes: Attributes): Record<string, string | number | boolean | null> {
391
- return Object.fromEntries(Object.entries(attributes).filter(([, value]) => value !== undefined)) as Record<string, string | number | boolean | null>;
524
+ return Object.fromEntries(
525
+ Object.entries(attributes).filter(([, value]) => value !== undefined),
526
+ ) as Record<string, string | number | boolean | null>;
527
+ }
528
+
529
+ type SanitizedSpanError = {
530
+ type: string;
531
+ statusCode?: number;
532
+ statusMessage: string;
533
+ };
534
+
535
+ function sanitizeSpanError(error: unknown): SanitizedSpanError {
536
+ const type =
537
+ error instanceof Error && /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(error.name)
538
+ ? error.name
539
+ : "Error";
540
+ const statusCode = errorStatusCode(error);
541
+ return {
542
+ type,
543
+ ...(statusCode === undefined ? {} : { statusCode }),
544
+ statusMessage: statusCode === undefined ? "operation failed" : `HTTP ${statusCode}`,
545
+ };
546
+ }
547
+
548
+ function errorStatusCode(error: unknown): number | undefined {
549
+ if (typeof error !== "object" || error === null) return undefined;
550
+ const value = (error as { status?: unknown; statusCode?: unknown }).status;
551
+ const statusCode =
552
+ Number.isInteger(value) && typeof value === "number"
553
+ ? value
554
+ : (error as { statusCode?: unknown }).statusCode;
555
+ return Number.isInteger(statusCode) &&
556
+ typeof statusCode === "number" &&
557
+ statusCode >= 100 &&
558
+ statusCode <= 599
559
+ ? statusCode
560
+ : undefined;
392
561
  }
393
562
 
394
- function errorToAttributes(error: unknown): Attributes {
563
+ function errorToAttributes(error: SanitizedSpanError): Attributes {
395
564
  return {
396
- "error.type": error instanceof Error ? error.name : "Error",
397
- "error.message": errorMessage(error),
565
+ "error.type": error.type,
566
+ ...(error.statusCode === undefined ? {} : { "error.status_code": error.statusCode }),
398
567
  };
399
568
  }
400
569
 
@@ -402,21 +571,31 @@ function errorMessage(error: unknown): string {
402
571
  return error instanceof Error ? error.message : String(error);
403
572
  }
404
573
 
405
- function otlpAttributes(attributes: Attributes): Array<{ key: string; value: Record<string, string | number | boolean> }> {
574
+ function otlpAttributes(
575
+ attributes: Attributes,
576
+ ): Array<{ key: string; value: Record<string, string | number | boolean> }> {
406
577
  return Object.entries(cleanAttributes(attributes)).map(([key, value]) => ({
407
578
  key,
408
579
  value: otlpValue(value),
409
580
  }));
410
581
  }
411
582
 
412
- function otlpValue(value: string | number | boolean | null): Record<string, string | number | boolean> {
583
+ function otlpValue(
584
+ value: string | number | boolean | null,
585
+ ): Record<string, string | number | boolean> {
413
586
  if (typeof value === "number") {
414
587
  return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
415
588
  }
416
589
  if (typeof value === "boolean") {
417
590
  return { boolValue: value };
418
591
  }
419
- return { stringValue: value === null ? "" : value };
592
+ return { stringValue: value === null ? "" : boundedOtlpString(value) };
593
+ }
594
+
595
+ function boundedOtlpString(value: string): string {
596
+ const bytes = new TextEncoder().encode(value);
597
+ if (bytes.byteLength <= 512) return value;
598
+ return `${new TextDecoder().decode(bytes.slice(0, 509))}…`;
420
599
  }
421
600
 
422
601
  function millisToNanos(ms: number): string {
@@ -432,17 +611,24 @@ export function parseHeaders(value: string): Record<string, string> {
432
611
  if (!value.trim()) {
433
612
  return {};
434
613
  }
435
- const entries: Array<[string, string]> = value.split(",").map((pair): [string, string] => {
436
- const separator = pair.indexOf("=");
437
- if (separator === -1) {
438
- return [pair.trim(), ""];
439
- }
440
- return [pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()];
441
- }).filter(([key]) => key.length > 0);
614
+ const entries: Array<[string, string]> = value
615
+ .split(",")
616
+ .map((pair): [string, string] => {
617
+ const separator = pair.indexOf("=");
618
+ if (separator === -1) {
619
+ return [pair.trim(), ""];
620
+ }
621
+ return [pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()];
622
+ })
623
+ .filter(([key]) => key.length > 0);
442
624
  return Object.fromEntries(entries);
443
625
  }
444
626
 
445
- async function defaultExporter(url: string, body: unknown, headers: Record<string, string>): Promise<void> {
627
+ async function defaultExporter(
628
+ url: string,
629
+ body: unknown,
630
+ headers: Record<string, string>,
631
+ ): Promise<void> {
446
632
  const response = await fetch(url, {
447
633
  method: "POST",
448
634
  headers: {