@senad-d/observme 0.1.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/.env.example +57 -0
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/ObservMe-Production-Docs/00-README.md +79 -0
- package/ObservMe-Production-Docs/01-requirements-and-scope.md +207 -0
- package/ObservMe-Production-Docs/02-reference-architecture.md +306 -0
- package/ObservMe-Production-Docs/03-pi-event-and-session-model.md +266 -0
- package/ObservMe-Production-Docs/04-telemetry-semantic-conventions.md +722 -0
- package/ObservMe-Production-Docs/05-otel-pipeline-and-collector.md +355 -0
- package/ObservMe-Production-Docs/06-security-privacy-redaction.md +294 -0
- package/ObservMe-Production-Docs/07-extension-implementation-blueprint.md +447 -0
- package/ObservMe-Production-Docs/08-query-grafana-integration.md +276 -0
- package/ObservMe-Production-Docs/09-dashboards-alerts-slos.md +640 -0
- package/ObservMe-Production-Docs/10-testing-release-operations.md +319 -0
- package/ObservMe-Production-Docs/11-deployment-runbooks.md +203 -0
- package/ObservMe-Production-Docs/12-configuration-reference.md +337 -0
- package/ObservMe-Production-Docs/13-source-notes.md +33 -0
- package/ObservMe-Production-Docs/pi-session-format.md +427 -0
- package/README.md +356 -0
- package/SECURITY.md +45 -0
- package/dashboards/observme-agent-node-graphs.json +250 -0
- package/dashboards/observme-agents.json +1880 -0
- package/dashboards/observme-alerts.yaml +113 -0
- package/dashboards/observme-branches-compactions.json +1042 -0
- package/dashboards/observme-cost.json +1254 -0
- package/dashboards/observme-errors.json +1659 -0
- package/dashboards/observme-export-health.json +1802 -0
- package/dashboards/observme-latency.json +1494 -0
- package/dashboards/observme-llm-conversations.json +730 -0
- package/dashboards/observme-logs-llm.json +644 -0
- package/dashboards/observme-models.json +933 -0
- package/dashboards/observme-overview.json +2129 -0
- package/dashboards/observme-slo-health.json +737 -0
- package/dashboards/observme-slos.yaml +56 -0
- package/dashboards/observme-tools.json +902 -0
- package/dashboards/observme-trace-journey.json +1793 -0
- package/docs/STRUCTURE.md +49 -0
- package/docs/agent-subagent-observability-requirements.md +997 -0
- package/docs/compatibility-matrix.md +39 -0
- package/docs/configuration-tui-design-standard.md +767 -0
- package/docs/configuration.md +38 -0
- package/docs/review-validation.md +115 -0
- package/docs/validation-flow.md +117 -0
- package/examples/collector.yaml +123 -0
- package/examples/observme.yaml +131 -0
- package/img/demo.gif +0 -0
- package/img/icon.svg +47 -0
- package/package.json +103 -0
- package/src/commands/obs-agents-runtime.ts +150 -0
- package/src/commands/obs-agents.ts +491 -0
- package/src/commands/obs-args.ts +63 -0
- package/src/commands/obs-backfill.ts +1334 -0
- package/src/commands/obs-command-support.ts +43 -0
- package/src/commands/obs-cost.ts +228 -0
- package/src/commands/obs-diagnostics.ts +22 -0
- package/src/commands/obs-errors.ts +156 -0
- package/src/commands/obs-health.ts +301 -0
- package/src/commands/obs-link.ts +90 -0
- package/src/commands/obs-logs.ts +194 -0
- package/src/commands/obs-loki-summary.ts +184 -0
- package/src/commands/obs-session.ts +259 -0
- package/src/commands/obs-status.ts +359 -0
- package/src/commands/obs-tools.ts +274 -0
- package/src/commands/obs-trace.ts +411 -0
- package/src/commands/obs.ts +211 -0
- package/src/config/bootstrap-project-config.ts +300 -0
- package/src/config/defaults.ts +143 -0
- package/src/config/load-config.ts +631 -0
- package/src/config/project-paths.ts +61 -0
- package/src/config/schema.ts +405 -0
- package/src/config/validate.ts +456 -0
- package/src/constants.ts +4 -0
- package/src/diagnostics/sanitize.ts +6 -0
- package/src/extension.ts +38 -0
- package/src/otel/logs.ts +160 -0
- package/src/otel/metrics.ts +165 -0
- package/src/otel/otlp-endpoint.ts +10 -0
- package/src/otel/sdk.ts +114 -0
- package/src/otel/shutdown.ts +102 -0
- package/src/otel/traces.ts +166 -0
- package/src/pi/agent-lineage.ts +378 -0
- package/src/pi/agent-tree-tracker.ts +258 -0
- package/src/pi/event-handlers/agent-turn.ts +155 -0
- package/src/pi/event-handlers/lifecycle.ts +642 -0
- package/src/pi/event-handlers/llm.ts +115 -0
- package/src/pi/event-handlers/session-events.ts +159 -0
- package/src/pi/event-handlers/tool-bash.ts +275 -0
- package/src/pi/handler-internals.ts +2154 -0
- package/src/pi/handler-runtime.ts +633 -0
- package/src/pi/handler-types.ts +261 -0
- package/src/pi/handlers.ts +75 -0
- package/src/pi/subagent-spawn.ts +975 -0
- package/src/pi/subagent-types.ts +29 -0
- package/src/privacy/content-capture.ts +104 -0
- package/src/privacy/hash.ts +93 -0
- package/src/privacy/redact.ts +619 -0
- package/src/privacy/secret-patterns.ts +185 -0
- package/src/privacy/truncate.ts +69 -0
- package/src/query/grafana-readiness.ts +164 -0
- package/src/query/grafana-transport.ts +481 -0
- package/src/query/grafana.ts +371 -0
- package/src/query/loki.ts +332 -0
- package/src/query/prometheus.ts +388 -0
- package/src/query/tempo.ts +332 -0
- package/src/safety/sensitive-input.ts +208 -0
- package/src/semconv/attributes.ts +279 -0
- package/src/semconv/metrics.ts +146 -0
- package/src/semconv/spans.ts +19 -0
- package/src/semconv/values.ts +13 -0
- package/src/util/bounded-map.ts +97 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Span } from "@opentelemetry/api";
|
|
2
|
+
import type { AgentWaitReason, SubagentSpawnReason } from "../semconv/values.ts";
|
|
3
|
+
|
|
4
|
+
export type TestableSpan = Span & {
|
|
5
|
+
readonly name?: string;
|
|
6
|
+
readonly attributes?: Record<string, unknown>;
|
|
7
|
+
readonly parentSpan?: Span;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface SubagentSpawnState {
|
|
11
|
+
readonly span: TestableSpan;
|
|
12
|
+
readonly childAgentId: string;
|
|
13
|
+
readonly startedAtMs: number;
|
|
14
|
+
readonly labels: Record<string, string>;
|
|
15
|
+
readonly spawnReason: SubagentSpawnReason;
|
|
16
|
+
readonly traceContextPropagated: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AgentWaitJoinState {
|
|
20
|
+
readonly span: TestableSpan;
|
|
21
|
+
readonly startedAtMs: number;
|
|
22
|
+
readonly labels: Record<string, string>;
|
|
23
|
+
readonly reason: AgentWaitReason;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChildFailureAccountingState {
|
|
27
|
+
readonly failureRecorded: boolean;
|
|
28
|
+
readonly recoveryRecorded: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
2
|
+
import { COMMON_SPAN_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
3
|
+
import { createEnvTenantSaltSource } from "./hash.ts";
|
|
4
|
+
import { redactValue } from "./redact.ts";
|
|
5
|
+
import type { ContentLimitKind, TruncationAttributes } from "./truncate.ts";
|
|
6
|
+
import { limitForContentKind, truncateContent } from "./truncate.ts";
|
|
7
|
+
|
|
8
|
+
export type ContentCaptureMode = "omitted" | "redacted" | "unsafe" | "dropped";
|
|
9
|
+
|
|
10
|
+
export interface ContentCapturePolicyRequest {
|
|
11
|
+
readonly captureEnabled: boolean;
|
|
12
|
+
readonly value?: string;
|
|
13
|
+
readonly kind: ContentLimitKind;
|
|
14
|
+
readonly config: ObservMeConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ContentCapturePolicyResult {
|
|
18
|
+
readonly mode: ContentCaptureMode;
|
|
19
|
+
readonly value?: string;
|
|
20
|
+
readonly captured: boolean;
|
|
21
|
+
readonly redactionFailures: number;
|
|
22
|
+
readonly truncated: boolean;
|
|
23
|
+
readonly originalLength?: number;
|
|
24
|
+
readonly attributes: TruncationAttributes;
|
|
25
|
+
readonly errors: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function applyContentCapturePolicy(request: ContentCapturePolicyRequest): ContentCapturePolicyResult {
|
|
29
|
+
if (!request.captureEnabled || request.value === undefined || request.value.length === 0) return omittedContentCapture();
|
|
30
|
+
if (request.config.privacy.redactionEnabled) return redactedContentCapture(request.value, request.kind, request.config);
|
|
31
|
+
if (request.config.privacy.allowUnsafeCapture) return unsafeContentCapture(request.value, request.kind, request.config);
|
|
32
|
+
return droppedContentCapture(["content capture requires redaction unless privacy.allowUnsafeCapture is true"], 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function redactedContentCapture(value: string, kind: ContentLimitKind, config: ObservMeConfig): ContentCapturePolicyResult {
|
|
36
|
+
const result = redactValue(value, {
|
|
37
|
+
pathMode: config.privacy.pathMode,
|
|
38
|
+
customRedactionPatterns: config.privacy.customRedactionPatterns,
|
|
39
|
+
maxOutputChars: limitForContentKind(kind, config.limits),
|
|
40
|
+
tenantSaltSource: createEnvTenantSaltSource(config),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result.dropped || result.value === undefined) return droppedContentCapture(result.errors, result.failureMetrics.redactionFailures || 1);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
mode: "redacted",
|
|
47
|
+
value: result.value,
|
|
48
|
+
captured: true,
|
|
49
|
+
redactionFailures: 0,
|
|
50
|
+
truncated: result.truncated,
|
|
51
|
+
originalLength: result.originalLength,
|
|
52
|
+
attributes: truncationAttributes(result.truncated, result.originalLength),
|
|
53
|
+
errors: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function unsafeContentCapture(value: string, kind: ContentLimitKind, config: ObservMeConfig): ContentCapturePolicyResult {
|
|
58
|
+
const result = truncateContent(value, kind, config.limits);
|
|
59
|
+
return {
|
|
60
|
+
mode: "unsafe",
|
|
61
|
+
value: result.value,
|
|
62
|
+
captured: true,
|
|
63
|
+
redactionFailures: 0,
|
|
64
|
+
truncated: result.truncated,
|
|
65
|
+
originalLength: result.originalLength,
|
|
66
|
+
attributes: result.attributes,
|
|
67
|
+
errors: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function omittedContentCapture(): ContentCapturePolicyResult {
|
|
72
|
+
return {
|
|
73
|
+
mode: "omitted",
|
|
74
|
+
captured: false,
|
|
75
|
+
redactionFailures: 0,
|
|
76
|
+
truncated: false,
|
|
77
|
+
attributes: {},
|
|
78
|
+
errors: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function droppedContentCapture(errors: readonly string[], redactionFailures: number): ContentCapturePolicyResult {
|
|
83
|
+
return {
|
|
84
|
+
mode: "dropped",
|
|
85
|
+
captured: false,
|
|
86
|
+
redactionFailures: normalizeRedactionFailures(redactionFailures),
|
|
87
|
+
truncated: false,
|
|
88
|
+
attributes: {},
|
|
89
|
+
errors,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function truncationAttributes(truncated: boolean, originalLength: number | undefined): TruncationAttributes {
|
|
94
|
+
if (!truncated) return {};
|
|
95
|
+
return {
|
|
96
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_TRUNCATED]: true,
|
|
97
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_ORIGINAL_LENGTH]: originalLength,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeRedactionFailures(value: number): number {
|
|
102
|
+
if (!Number.isFinite(value) || value < 1) return 1;
|
|
103
|
+
return Math.trunc(value);
|
|
104
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export interface EnvTenantSaltSource {
|
|
4
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
5
|
+
readonly envName: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TenantSaltConfig {
|
|
9
|
+
readonly privacy: {
|
|
10
|
+
readonly tenantSaltEnv: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SecureRuntimeTenantSaltSource {
|
|
15
|
+
readonly secureRuntimeConfig: {
|
|
16
|
+
readonly tenantSalt: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TenantSaltSource = EnvTenantSaltSource | SecureRuntimeTenantSaltSource;
|
|
21
|
+
|
|
22
|
+
const registeredTenantSaltEnvironments = new WeakMap<TenantSaltConfig, Readonly<Record<string, string | undefined>>>();
|
|
23
|
+
|
|
24
|
+
export function registerTenantSaltEnvironment<T extends TenantSaltConfig>(
|
|
25
|
+
config: T,
|
|
26
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
27
|
+
): T {
|
|
28
|
+
registeredTenantSaltEnvironments.set(config, env);
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function inheritTenantSaltEnvironment<T extends TenantSaltConfig>(target: T, source: TenantSaltConfig): T {
|
|
33
|
+
return registerTenantSaltEnvironment(target, tenantSaltEnvironmentForConfig(source));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sha256(value: string, source: TenantSaltSource): string {
|
|
37
|
+
return createHash("sha256").update(`${resolveTenantSalt(source)}\0${value}`).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hmac_sha256(value: string, source: TenantSaltSource): string {
|
|
41
|
+
return createHmac("sha256", resolveTenantSalt(source)).update(value).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createEnvTenantSaltSource(
|
|
45
|
+
config: TenantSaltConfig,
|
|
46
|
+
env: Readonly<Record<string, string | undefined>> = tenantSaltEnvironmentForConfig(config),
|
|
47
|
+
): EnvTenantSaltSource {
|
|
48
|
+
return { env, envName: config.privacy.tenantSaltEnv };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function trySha256(
|
|
52
|
+
value: string,
|
|
53
|
+
config: TenantSaltConfig,
|
|
54
|
+
env: Readonly<Record<string, string | undefined>> = tenantSaltEnvironmentForConfig(config),
|
|
55
|
+
): string | undefined {
|
|
56
|
+
const salt = readOptionalTenantSaltFromEnv(createEnvTenantSaltSource(config, env));
|
|
57
|
+
if (salt === undefined) return undefined;
|
|
58
|
+
return createHash("sha256").update(`${salt}\0${value}`).digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function tenantSaltEnvironmentForConfig(config: TenantSaltConfig): Readonly<Record<string, string | undefined>> {
|
|
62
|
+
return registeredTenantSaltEnvironments.get(config) ?? process.env;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readOptionalTenantSaltFromEnv(source: EnvTenantSaltSource): string | undefined {
|
|
66
|
+
if (source.envName.length === 0) return undefined;
|
|
67
|
+
|
|
68
|
+
const salt = source.env[source.envName];
|
|
69
|
+
if (salt === undefined || salt.length === 0) return undefined;
|
|
70
|
+
return salt;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveTenantSalt(source: TenantSaltSource): string {
|
|
74
|
+
const salt = readTenantSalt(source);
|
|
75
|
+
if (salt.length === 0) throw new Error("tenant salt must not be empty");
|
|
76
|
+
return salt;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readTenantSalt(source: TenantSaltSource): string {
|
|
80
|
+
if (isEnvTenantSaltSource(source)) return readTenantSaltFromEnv(source);
|
|
81
|
+
return source.secureRuntimeConfig.tenantSalt;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readTenantSaltFromEnv(source: EnvTenantSaltSource): string {
|
|
85
|
+
if (source.envName.length === 0) throw new Error("tenant salt env name must not be empty");
|
|
86
|
+
const salt = source.env[source.envName];
|
|
87
|
+
if (salt === undefined) throw new Error(`tenant salt env var ${source.envName} is not set`);
|
|
88
|
+
return salt;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isEnvTenantSaltSource(source: TenantSaltSource): source is EnvTenantSaltSource {
|
|
92
|
+
return "env" in source;
|
|
93
|
+
}
|