@equipe-tech/observability 0.2.0 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -9
- package/dist/BrowserEvents.d.ts +89 -12
- package/dist/BrowserEvents.js +67 -13
- package/dist/Correlation.d.ts +51 -0
- package/dist/Correlation.js +95 -0
- package/dist/Metrics.d.ts +19 -3
- package/dist/Metrics.js +16 -0
- package/dist/MetricsRuntime.d.ts +45 -3
- package/dist/MetricsRuntime.js +478 -120
- package/dist/PolicyOtlpLogger.d.ts +18 -0
- package/dist/PolicyOtlpLogger.js +142 -0
- package/dist/RedactionPolicy.d.ts +2 -9
- package/dist/RedactionPolicy.js +2 -246
- package/dist/ResourceIdentity.d.ts +47 -0
- package/dist/ResourceIdentity.js +82 -0
- package/dist/Telemetry.d.ts +18 -3
- package/dist/Telemetry.js +43 -25
- package/dist/TelemetryConfig.d.ts +11 -4
- package/dist/TelemetryConfig.js +38 -10
- package/dist/audit/AuditCommitDocument.d.ts +41 -0
- package/dist/audit/AuditCommitDocument.js +58 -0
- package/dist/audit/AuditDigest.d.ts +11 -0
- package/dist/audit/AuditDigest.js +25 -0
- package/dist/audit/AuditOutbox.d.ts +75 -0
- package/dist/audit/AuditOutbox.js +104 -0
- package/dist/audit/AuditPublisher.d.ts +49 -0
- package/dist/audit/AuditPublisher.js +60 -0
- package/dist/audit/AuditRecord.d.ts +128 -0
- package/dist/audit/AuditRecord.js +172 -0
- package/dist/audit/CommittedAuditRecordInternal.d.ts +11 -0
- package/dist/audit/CommittedAuditRecordInternal.js +13 -0
- package/dist/audit/InvalidAuditRecord.d.ts +12 -0
- package/dist/audit/InvalidAuditRecord.js +16 -0
- package/dist/audit/index.d.ts +7 -0
- package/dist/audit/index.js +7 -0
- package/dist/browser/BrowserClient.d.ts +82 -3
- package/dist/browser/BrowserClient.js +322 -28
- package/dist/browser/BrowserEventLimits.d.ts +8 -0
- package/dist/browser/BrowserEventLimits.js +8 -0
- package/dist/browser/BrowserMetricRecorder.d.ts +17 -0
- package/dist/browser/BrowserMetricRecorder.js +35 -0
- package/dist/browser/BrowserTelemetry.d.ts +40 -0
- package/dist/browser/BrowserTelemetry.js +105 -0
- package/dist/browser/ClientPolicy.d.ts +3 -0
- package/dist/browser/ClientPolicy.js +52 -0
- package/dist/browser/client.d.ts +3 -2
- package/dist/browser/client.js +2 -1
- package/dist/browser/index.d.ts +4 -37
- package/dist/browser/index.js +3 -95
- package/dist/contract/ContractCompatibility.d.ts +31 -0
- package/dist/contract/ContractCompatibility.js +274 -0
- package/dist/contract/ContractIndex.d.ts +54 -0
- package/dist/contract/ContractIndex.js +178 -0
- package/dist/contract/ContractSurface.d.ts +111 -0
- package/dist/contract/ContractSurface.js +116 -0
- package/dist/contract/EventName.d.ts +6 -0
- package/dist/contract/EventName.js +55 -0
- package/dist/contract/EventProducer.d.ts +123 -0
- package/dist/contract/EventProducer.js +209 -0
- package/dist/contract/MetricContractError.d.ts +19 -0
- package/dist/contract/MetricContractError.js +19 -0
- package/dist/contract/MetricDefinition.d.ts +61 -0
- package/dist/contract/MetricDefinition.js +91 -0
- package/dist/contract/MetricProducer.d.ts +42 -0
- package/dist/contract/MetricProducer.js +123 -0
- package/dist/contract/OrganizationEvents.d.ts +165 -0
- package/dist/contract/OrganizationEvents.js +97 -0
- package/dist/contract/TelemetryContract.d.ts +95 -0
- package/dist/contract/TelemetryContract.js +451 -0
- package/dist/contract/TelemetryContractError.d.ts +44 -0
- package/dist/contract/TelemetryContractError.js +64 -0
- package/dist/contract/TelemetryEvent.d.ts +59 -0
- package/dist/contract/TelemetryEvent.js +54 -0
- package/dist/contract/index.d.ts +11 -0
- package/dist/contract/index.js +11 -0
- package/dist/effect/WideEvent.d.ts +3 -0
- package/dist/effect/WideEvent.js +20 -0
- package/dist/effect/WideEventSink.d.ts +3 -0
- package/dist/effect/WideEventSink.js +79 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.js +2 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +11 -2
- package/dist/metrics/InstrumentGrammar.d.ts +5 -0
- package/dist/metrics/InstrumentGrammar.js +12 -0
- package/dist/metrics/MetricScalar.d.ts +3 -0
- package/dist/metrics/MetricScalar.js +5 -0
- package/dist/node/AuditDigest.d.ts +3 -0
- package/dist/node/AuditDigest.js +6 -0
- package/dist/node/BrowserEventIngest.d.ts +7 -2
- package/dist/node/BrowserEventIngest.js +111 -19
- package/dist/node/LocalCollector.d.ts +20 -0
- package/dist/node/LocalCollector.js +188 -0
- package/dist/node/Observability.d.ts +49 -0
- package/dist/node/Observability.js +190 -0
- package/dist/node/Runtime.d.ts +4 -1
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +3 -0
- package/dist/policy/BrowserFieldPolicy.d.ts +7 -0
- package/dist/policy/BrowserFieldPolicy.js +298 -0
- package/dist/policy/DataPolicy.d.ts +19 -0
- package/dist/policy/DataPolicy.js +254 -0
- package/dist/policy/DataPolicyError.d.ts +22 -0
- package/dist/policy/DataPolicyError.js +25 -0
- package/dist/policy/Deduplication.d.ts +13 -0
- package/dist/policy/Deduplication.js +44 -0
- package/dist/policy/DefectEnvelope.d.ts +25 -0
- package/dist/policy/DefectEnvelope.js +52 -0
- package/dist/policy/MetricLabelPolicy.d.ts +5 -0
- package/dist/policy/MetricLabelPolicy.js +65 -0
- package/dist/policy/PolicyTransform.d.ts +16 -0
- package/dist/policy/PolicyTransform.js +136 -0
- package/dist/policy/PolicyVocabulary.d.ts +10 -0
- package/dist/policy/PolicyVocabulary.js +84 -0
- package/dist/policy/ResourceAttributePolicy.d.ts +9 -0
- package/dist/policy/ResourceAttributePolicy.js +33 -0
- package/dist/policy/entrypoint.d.ts +2 -0
- package/dist/policy/entrypoint.js +2 -0
- package/dist/policy/index.d.ts +17 -0
- package/dist/policy/index.js +9 -0
- package/dist/profile/DataPolicy.d.ts +2 -0
- package/dist/profile/DataPolicy.js +1 -0
- package/dist/profile/EnvironmentPolicy.d.ts +22 -0
- package/dist/profile/EnvironmentPolicy.js +41 -0
- package/dist/profile/LifecycleRegistry.d.ts +24 -0
- package/dist/profile/LifecycleRegistry.js +240 -0
- package/dist/profile/ObservabilityAdapter.d.ts +115 -0
- package/dist/profile/ObservabilityAdapter.js +50 -0
- package/dist/profile/ObservabilityConfig.d.ts +68 -0
- package/dist/profile/ObservabilityConfig.js +125 -0
- package/dist/profile/ObservabilityConfigError.d.ts +23 -0
- package/dist/profile/ObservabilityConfigError.js +39 -0
- package/dist/profile/ObservabilityProfile.d.ts +49 -0
- package/dist/profile/ObservabilityProfile.js +121 -0
- package/dist/profile/ReactWebProfile.d.ts +8 -0
- package/dist/profile/ReactWebProfile.js +7 -0
- package/dist/profile/index.d.ts +5 -0
- package/dist/profile/index.js +5 -0
- package/dist/testing/conformance/ConformanceAssertions.d.ts +5 -0
- package/dist/testing/conformance/ConformanceAssertions.js +31 -0
- package/dist/testing/conformance/ConformanceFailure.d.ts +35 -0
- package/dist/testing/conformance/ConformanceFailure.js +56 -0
- package/dist/testing/conformance/ConformanceModel.d.ts +97 -0
- package/dist/testing/conformance/ConformanceModel.js +8 -0
- package/dist/testing/conformance/ConformanceRegistry.d.ts +13 -0
- package/dist/testing/conformance/ConformanceRegistry.js +190 -0
- package/dist/testing/conformance/ConformanceRunner.d.ts +5 -0
- package/dist/testing/conformance/ConformanceRunner.js +198 -0
- package/dist/testing/conformance/TelemetryEvidence.d.ts +91 -0
- package/dist/testing/conformance/TelemetryEvidence.js +370 -0
- package/dist/testing/conformance/index.d.ts +10 -0
- package/dist/testing/conformance/index.js +6 -0
- package/dist/testing/contract.d.ts +81 -0
- package/dist/testing/contract.js +93 -0
- package/dist/testing/deployedCanary.d.ts +1 -0
- package/dist/testing/deployedCanary.js +1 -0
- package/dist/testing/index.d.ts +40 -3
- package/dist/testing/index.js +122 -5
- package/dist/trace/HttpServerOtlpTracer.d.ts +42 -0
- package/dist/trace/HttpServerOtlpTracer.js +301 -0
- package/package.json +16 -29
- package/dist/WideEvent.d.ts +0 -5
- package/dist/WideEvent.js +0 -6
- package/dist/nestjs/BrowserEventsController.d.ts +0 -21
- package/dist/nestjs/BrowserEventsController.js +0 -52
- package/dist/nestjs/HttpRoutePolicy.d.ts +0 -23
- package/dist/nestjs/HttpRoutePolicy.js +0 -179
- package/dist/nestjs/HttpServerOtlpTracer.d.ts +0 -15
- package/dist/nestjs/HttpServerOtlpTracer.js +0 -154
- package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +0 -15
- package/dist/nestjs/RequestWideEventTraceCorrelation.js +0 -13
- package/dist/nestjs/TelemetryInterceptor.d.ts +0 -31
- package/dist/nestjs/TelemetryInterceptor.js +0 -268
- package/dist/nestjs/TelemetryModule.d.ts +0 -53
- package/dist/nestjs/TelemetryModule.js +0 -428
- package/dist/nestjs/index.d.ts +0 -5
- package/dist/nestjs/index.js +0 -4
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Cause, Clock, Effect, Option, Schema } from "effect";
|
|
2
|
+
import { AdapterFailure, AdapterName, isOfficialAdapterRegistration, isTestingAdapterRegistration, registerOfficialAdapter, } from "./ObservabilityAdapter.js";
|
|
3
|
+
import { InvalidObservabilityConfig } from "./ObservabilityConfigError.js";
|
|
4
|
+
import { profileCapabilityRank, profileCapabilityRequirement, profileStageDeadlineMillis, } from "./ObservabilityProfile.js";
|
|
5
|
+
export class ObservabilityLifecycleError extends Schema.TaggedError()("ObservabilityLifecycleError", {
|
|
6
|
+
code: Schema.Literals(["OBS_OBSERVABILITY_STARTUP_FAILED", "OBS_OBSERVABILITY_CLOSED"]),
|
|
7
|
+
message: Schema.String,
|
|
8
|
+
adapter: Schema.Option(AdapterName),
|
|
9
|
+
cause: Schema.Defect(),
|
|
10
|
+
}) {
|
|
11
|
+
}
|
|
12
|
+
const expectedStage = (profile, capability) => capability === "metrics"
|
|
13
|
+
? "metrics"
|
|
14
|
+
: capability === "browser-ingest" && profile.runtime === "browser-global"
|
|
15
|
+
? "browser"
|
|
16
|
+
: "server";
|
|
17
|
+
const invalidAdapter = (code, message) => new InvalidObservabilityConfig({
|
|
18
|
+
code,
|
|
19
|
+
field: "adapters",
|
|
20
|
+
message,
|
|
21
|
+
rule: "one supported registration for each required external capability",
|
|
22
|
+
});
|
|
23
|
+
const reservedAdapterNames = new Set(["core-traces", "core-metrics"]);
|
|
24
|
+
const cleanupReserveMillis = 1_000;
|
|
25
|
+
const forcedCleanupBudgetMillis = 500;
|
|
26
|
+
const runtimeDisposalReserveMillis = 500;
|
|
27
|
+
const deadlineSafetyMillis = 50;
|
|
28
|
+
export const validateAdapterRegistrationKinds = (registrations, options) => {
|
|
29
|
+
for (const registration of registrations) {
|
|
30
|
+
const official = isOfficialAdapterRegistration(registration);
|
|
31
|
+
const testing = isTestingAdapterRegistration(registration);
|
|
32
|
+
if (!official && !testing) {
|
|
33
|
+
return Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_UNSUPPORTED", "The adapter registration is not authentic. Use the package registration factories."));
|
|
34
|
+
}
|
|
35
|
+
if (testing && !options.allowTesting) {
|
|
36
|
+
return Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_TESTING", `Testing adapter "${registration.adapter.name}" cannot run through an official factory. Use a factory from @equipe-tech/observability/testing.`));
|
|
37
|
+
}
|
|
38
|
+
if (reservedAdapterNames.has(registration.adapter.name)) {
|
|
39
|
+
return Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_UNSUPPORTED", `Adapter name "${registration.adapter.name}" is reserved for the built-in lifecycle registry. Choose another adapter name.`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Effect.void;
|
|
43
|
+
};
|
|
44
|
+
export const validateAdapterRegistrations = Effect.fn("validateAdapterRegistrations")(function* (profile, environment, registrations, options) {
|
|
45
|
+
yield* validateAdapterRegistrationKinds(registrations, options);
|
|
46
|
+
const names = new Set();
|
|
47
|
+
const capabilities = new Set();
|
|
48
|
+
for (const registration of registrations) {
|
|
49
|
+
const adapter = registration.adapter;
|
|
50
|
+
if (names.has(adapter.name)) {
|
|
51
|
+
return yield* Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_DUPLICATE", `Adapter "${adapter.name}" is registered more than once. Use one registration per adapter name.`));
|
|
52
|
+
}
|
|
53
|
+
names.add(adapter.name);
|
|
54
|
+
const requirement = profileCapabilityRequirement(profile, adapter.capability);
|
|
55
|
+
if (requirement === "forbidden" ||
|
|
56
|
+
adapter.stage !== expectedStage(profile, adapter.capability) ||
|
|
57
|
+
!profile.stages.includes(adapter.stage) ||
|
|
58
|
+
adapter.capability === "traces" ||
|
|
59
|
+
adapter.capability === "metrics") {
|
|
60
|
+
return yield* Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_UNSUPPORTED", `${registration.kind === "official" ? "Official" : "Testing"} adapter "${adapter.name}" cannot supply ${adapter.capability} for profile "${profile.name}".`));
|
|
61
|
+
}
|
|
62
|
+
if (capabilities.has(adapter.capability)) {
|
|
63
|
+
return yield* Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_DUPLICATE", `Capability "${adapter.capability}" is registered by more than one adapter. Use one registration per capability.`));
|
|
64
|
+
}
|
|
65
|
+
capabilities.add(adapter.capability);
|
|
66
|
+
}
|
|
67
|
+
const externalCapabilities = [
|
|
68
|
+
"events",
|
|
69
|
+
"defects",
|
|
70
|
+
"browser-ingest",
|
|
71
|
+
];
|
|
72
|
+
for (const capability of externalCapabilities) {
|
|
73
|
+
const requirement = profileCapabilityRequirement(profile, capability);
|
|
74
|
+
const required = requirement === "required" ||
|
|
75
|
+
(requirement === "required-in-production" && environment === "production");
|
|
76
|
+
if (required && !capabilities.has(capability)) {
|
|
77
|
+
return yield* Effect.fail(invalidAdapter("OBS_OBSERVABILITY_ADAPTER_MISSING", `Profile "${profile.name}" requires a ${options.allowTesting ? "registered" : "official"} ${capability} adapter in ${environment}.`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const adapterFailure = (operation, cause) => Option.getOrElse(Cause.findErrorOption(cause), () => new AdapterFailure({
|
|
82
|
+
code: "OBS_OBSERVABILITY_ADAPTER_FAILED",
|
|
83
|
+
message: `Observability adapter ${operation} failed without a typed failure. Fix the adapter implementation before retrying.`,
|
|
84
|
+
cause,
|
|
85
|
+
}));
|
|
86
|
+
const runtimeFailure = (cause) => new AdapterFailure({
|
|
87
|
+
code: "OBS_OBSERVABILITY_ADAPTER_FAILED",
|
|
88
|
+
message: "Observability runtime disposal failed. Inspect the defect before retrying.",
|
|
89
|
+
cause,
|
|
90
|
+
});
|
|
91
|
+
const ordered = (profile, started, stage) => started
|
|
92
|
+
.filter((entry) => entry.registration.adapter.stage === stage)
|
|
93
|
+
.toSorted((left, right) => profileCapabilityRank(profile, stage, left.registration.adapter.capability) -
|
|
94
|
+
profileCapabilityRank(profile, stage, right.registration.adapter.capability));
|
|
95
|
+
const runParticipant = Effect.fn("runObservabilityParticipant")(function* (entry, operation, budgetMillis) {
|
|
96
|
+
const adapter = entry.registration.adapter;
|
|
97
|
+
if (budgetMillis <= 0) {
|
|
98
|
+
return {
|
|
99
|
+
participant: "adapter",
|
|
100
|
+
adapter: adapter.name,
|
|
101
|
+
capability: adapter.capability,
|
|
102
|
+
stage: adapter.stage,
|
|
103
|
+
result: { kind: "deadline-exceeded", budgetMillis: 0 },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
107
|
+
const effect = operation === "flush" ? entry.handle.flush : entry.handle.close;
|
|
108
|
+
const result = yield* effect.pipe(Effect.exit, Effect.timeoutOption(budgetMillis));
|
|
109
|
+
const durationMillis = (yield* Clock.currentTimeMillis) - startedAt;
|
|
110
|
+
if (Option.isNone(result)) {
|
|
111
|
+
return {
|
|
112
|
+
participant: "adapter",
|
|
113
|
+
adapter: adapter.name,
|
|
114
|
+
capability: adapter.capability,
|
|
115
|
+
stage: adapter.stage,
|
|
116
|
+
result: { kind: "deadline-exceeded", budgetMillis },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (result.value._tag === "Failure") {
|
|
120
|
+
return {
|
|
121
|
+
participant: "adapter",
|
|
122
|
+
adapter: adapter.name,
|
|
123
|
+
capability: adapter.capability,
|
|
124
|
+
stage: adapter.stage,
|
|
125
|
+
result: { kind: "failed", error: adapterFailure(operation, result.value.cause) },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
participant: "adapter",
|
|
130
|
+
adapter: adapter.name,
|
|
131
|
+
capability: adapter.capability,
|
|
132
|
+
stage: adapter.stage,
|
|
133
|
+
result: { kind: "completed", durationMillis },
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
const runForcedClose = Effect.fn("runForcedObservabilityClose")(function* (entry, budgetMillis) {
|
|
137
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
138
|
+
const result = yield* entry.handle.close.pipe(Effect.exit, Effect.timeoutOption(Math.max(0, budgetMillis)));
|
|
139
|
+
const durationMillis = (yield* Clock.currentTimeMillis) - startedAt;
|
|
140
|
+
if (Option.isNone(result)) {
|
|
141
|
+
return { kind: "deadline-exceeded", budgetMillis };
|
|
142
|
+
}
|
|
143
|
+
if (result.value._tag === "Failure") {
|
|
144
|
+
return { kind: "failed", error: adapterFailure("close", result.value.cause) };
|
|
145
|
+
}
|
|
146
|
+
return { kind: "completed", durationMillis };
|
|
147
|
+
});
|
|
148
|
+
const runRuntimeDisposal = Effect.fn("runRuntimeDisposal")(function* (disposeRuntime, budgetMillis) {
|
|
149
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
150
|
+
const result = yield* disposeRuntime.pipe(Effect.exit, Effect.timeoutOption(Math.max(0, budgetMillis)));
|
|
151
|
+
const durationMillis = (yield* Clock.currentTimeMillis) - startedAt;
|
|
152
|
+
if (Option.isNone(result)) {
|
|
153
|
+
return { participant: "runtime-disposal", result: { kind: "deadline-exceeded", budgetMillis } };
|
|
154
|
+
}
|
|
155
|
+
if (result.value._tag === "Failure") {
|
|
156
|
+
return {
|
|
157
|
+
participant: "runtime-disposal",
|
|
158
|
+
result: { kind: "failed", error: runtimeFailure(result.value.cause) },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { participant: "runtime-disposal", result: { kind: "completed", durationMillis } };
|
|
162
|
+
});
|
|
163
|
+
const coreRegistration = (name, capability, stage, effect) => ({
|
|
164
|
+
registration: registerOfficialAdapter({
|
|
165
|
+
name: AdapterName.make(name),
|
|
166
|
+
capability,
|
|
167
|
+
stage,
|
|
168
|
+
start: () => Effect.die("Built-in lifecycle participants are already started."),
|
|
169
|
+
}),
|
|
170
|
+
handle: {
|
|
171
|
+
flush: effect,
|
|
172
|
+
close: effect,
|
|
173
|
+
eventLayer: Option.none(),
|
|
174
|
+
auditLayer: Option.none(),
|
|
175
|
+
degraded: () => false,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
export const createLifecycleRegistry = (profile, started, flusher, disposeRuntime) => {
|
|
179
|
+
const traces = coreRegistration("core-traces", "traces", "server", flusher.flush);
|
|
180
|
+
const metrics = coreRegistration("core-metrics", "metrics", "metrics", flusher.flush);
|
|
181
|
+
const participants = [...started, traces, metrics];
|
|
182
|
+
const run = Effect.fn("LifecycleRegistry.run")(function* (operation) {
|
|
183
|
+
const startedAt = yield* Clock.currentTimeMillis;
|
|
184
|
+
const operationalDeadlineMillis = Math.max(0, profile.shutdownDeadlineMillis - deadlineSafetyMillis);
|
|
185
|
+
const deadline = startedAt + operationalDeadlineMillis;
|
|
186
|
+
const cleanupReserve = operation === "close" ? Math.min(cleanupReserveMillis, operationalDeadlineMillis) : 0;
|
|
187
|
+
const runtimeReserve = Math.min(runtimeDisposalReserveMillis, cleanupReserve);
|
|
188
|
+
const gracefulDeadline = deadline - cleanupReserve;
|
|
189
|
+
const outcomes = [];
|
|
190
|
+
const forcedCleanup = [];
|
|
191
|
+
for (const stage of profile.stages) {
|
|
192
|
+
const now = yield* Clock.currentTimeMillis;
|
|
193
|
+
const totalRemaining = Math.max(0, gracefulDeadline - now);
|
|
194
|
+
const stageBudget = profileStageDeadlineMillis(profile, stage) ?? totalRemaining;
|
|
195
|
+
const stageDeadline = now + Math.min(totalRemaining, stageBudget);
|
|
196
|
+
for (const participant of ordered(profile, participants, stage)) {
|
|
197
|
+
const remaining = Math.max(0, stageDeadline - (yield* Clock.currentTimeMillis));
|
|
198
|
+
const outcome = yield* runParticipant(participant, operation, remaining);
|
|
199
|
+
const outcomeIndex = outcomes.push(outcome) - 1;
|
|
200
|
+
if (operation === "close" && outcome.result.kind === "deadline-exceeded") {
|
|
201
|
+
forcedCleanup.push({ outcomeIndex, entry: participant });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (operation === "close") {
|
|
206
|
+
const forcedDeadline = deadline - runtimeReserve;
|
|
207
|
+
const forcedBudget = Math.min(forcedCleanupBudgetMillis, Math.max(0, forcedDeadline - (yield* Clock.currentTimeMillis)));
|
|
208
|
+
const cleanupResults = yield* Effect.forEach(forcedCleanup, (candidate) => runForcedClose(candidate.entry, forcedBudget), { concurrency: "unbounded" });
|
|
209
|
+
for (let index = 0; index < forcedCleanup.length; index += 1) {
|
|
210
|
+
const candidate = forcedCleanup[index];
|
|
211
|
+
const cleanupResult = cleanupResults[index];
|
|
212
|
+
if (candidate === undefined || cleanupResult === undefined)
|
|
213
|
+
continue;
|
|
214
|
+
const outcome = outcomes[candidate.outcomeIndex];
|
|
215
|
+
if (outcome?.participant === "adapter" && outcome.result.kind === "deadline-exceeded") {
|
|
216
|
+
outcomes[candidate.outcomeIndex] = {
|
|
217
|
+
...outcome,
|
|
218
|
+
result: { ...outcome.result, forcedCleanup: cleanupResult },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const remaining = Math.max(0, deadline - (yield* Clock.currentTimeMillis));
|
|
223
|
+
outcomes.push(yield* runRuntimeDisposal(disposeRuntime, remaining));
|
|
224
|
+
}
|
|
225
|
+
const durationMillis = (yield* Clock.currentTimeMillis) - startedAt;
|
|
226
|
+
return {
|
|
227
|
+
operation,
|
|
228
|
+
outcomes,
|
|
229
|
+
durationMillis,
|
|
230
|
+
degraded: outcomes.some((outcome) => outcome.result.kind !== "completed") ||
|
|
231
|
+
started.some((entry) => entry.handle.degraded()),
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
return { run };
|
|
235
|
+
};
|
|
236
|
+
export const rollbackStartedAdapters = Effect.fn("rollbackStartedAdapters")(function* (started) {
|
|
237
|
+
for (const entry of started.toReversed()) {
|
|
238
|
+
yield* entry.handle.close.pipe(Effect.catch(() => Effect.void));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Effect, Layer, ManagedRuntime, Option, Schema } from "effect";
|
|
2
|
+
import type { OtlpExporter } from "effect/unstable/observability";
|
|
3
|
+
import type { CompiledAuditActionDefinition, CompiledEventDefinition } from "../contract/TelemetryContract.js";
|
|
4
|
+
import type { CompiledMetricDefinition } from "../contract/MetricDefinition.js";
|
|
5
|
+
import type { EventName } from "../contract/EventName.js";
|
|
6
|
+
import type { AuditPublisher } from "../audit/AuditPublisher.js";
|
|
7
|
+
import type { TelemetryEventSink } from "../contract/EventProducer.js";
|
|
8
|
+
import type { ResourceIdentity } from "../ResourceIdentity.js";
|
|
9
|
+
import type { TelemetryConfig } from "../TelemetryConfig.js";
|
|
10
|
+
import type { DataPolicy } from "./DataPolicy.js";
|
|
11
|
+
import type { SentryConfig } from "./ObservabilityConfig.js";
|
|
12
|
+
import { InvalidObservabilityConfig } from "./ObservabilityConfigError.js";
|
|
13
|
+
import type { AdapterCapability, LifecycleStage, ObservabilityProfile } from "./ObservabilityProfile.js";
|
|
14
|
+
export declare const AdapterName: Schema.brand<Schema.String, "AdapterName">;
|
|
15
|
+
export type AdapterName = typeof AdapterName.Type;
|
|
16
|
+
declare const AdapterFailure_base: Schema.Class<AdapterFailure, Schema.TaggedStruct<"AdapterFailure", {
|
|
17
|
+
readonly code: Schema.Literal<"OBS_OBSERVABILITY_ADAPTER_FAILED">;
|
|
18
|
+
readonly message: Schema.String;
|
|
19
|
+
readonly cause: Schema.Defect;
|
|
20
|
+
}>, import("effect/Cause").YieldableError>;
|
|
21
|
+
export declare class AdapterFailure extends AdapterFailure_base {
|
|
22
|
+
}
|
|
23
|
+
export type ContractRegistry = {
|
|
24
|
+
readonly version: number;
|
|
25
|
+
readonly eventNames: ReadonlyArray<EventName>;
|
|
26
|
+
readonly eventByAlias: ReadonlyMap<string, CompiledEventDefinition>;
|
|
27
|
+
readonly eventByName: ReadonlyMap<EventName, CompiledEventDefinition>;
|
|
28
|
+
readonly auditActionByAlias: ReadonlyMap<string, CompiledAuditActionDefinition>;
|
|
29
|
+
readonly auditActionByName: ReadonlyMap<string, CompiledAuditActionDefinition>;
|
|
30
|
+
readonly metricByAlias: ReadonlyMap<string, CompiledMetricDefinition>;
|
|
31
|
+
readonly metricByName: ReadonlyMap<string, CompiledMetricDefinition>;
|
|
32
|
+
};
|
|
33
|
+
export type ObservabilityAdapterContext = {
|
|
34
|
+
readonly profile: ObservabilityProfile;
|
|
35
|
+
readonly identity: ResourceIdentity;
|
|
36
|
+
readonly telemetryConfig: TelemetryConfig;
|
|
37
|
+
readonly contract: ContractRegistry;
|
|
38
|
+
readonly policy: DataPolicy;
|
|
39
|
+
readonly sentry: SentryConfig;
|
|
40
|
+
readonly runtime: ManagedRuntime.ManagedRuntime<OtlpExporter.Flusher, InvalidObservabilityConfig>;
|
|
41
|
+
};
|
|
42
|
+
export type ObservabilityAdapterHandle = {
|
|
43
|
+
readonly flush: Effect.Effect<void, AdapterFailure>;
|
|
44
|
+
readonly close: Effect.Effect<void, AdapterFailure>;
|
|
45
|
+
readonly eventLayer: Option.Option<Layer.Layer<TelemetryEventSink>>;
|
|
46
|
+
readonly auditLayer: Option.Option<Layer.Layer<AuditPublisher>>;
|
|
47
|
+
readonly degraded: () => boolean;
|
|
48
|
+
};
|
|
49
|
+
export type ObservabilityAdapter = {
|
|
50
|
+
readonly name: AdapterName;
|
|
51
|
+
readonly capability: AdapterCapability;
|
|
52
|
+
readonly stage: LifecycleStage;
|
|
53
|
+
readonly start: (context: ObservabilityAdapterContext) => Effect.Effect<ObservabilityAdapterHandle, AdapterFailure>;
|
|
54
|
+
};
|
|
55
|
+
declare const officialRegistrationBrand: unique symbol;
|
|
56
|
+
declare const testingRegistrationBrand: unique symbol;
|
|
57
|
+
export type OfficialAdapterRegistration = {
|
|
58
|
+
readonly kind: "official";
|
|
59
|
+
readonly adapter: ObservabilityAdapter;
|
|
60
|
+
readonly [officialRegistrationBrand]: true;
|
|
61
|
+
};
|
|
62
|
+
export type TestingAdapterRegistration = {
|
|
63
|
+
readonly kind: "testing";
|
|
64
|
+
readonly adapter: ObservabilityAdapter;
|
|
65
|
+
readonly [testingRegistrationBrand]: true;
|
|
66
|
+
};
|
|
67
|
+
export type AdapterRegistration = OfficialAdapterRegistration | TestingAdapterRegistration;
|
|
68
|
+
export declare const registerOfficialAdapter: (adapter: ObservabilityAdapter) => OfficialAdapterRegistration;
|
|
69
|
+
export declare const registerTestingAdapter: (adapter: ObservabilityAdapter) => TestingAdapterRegistration;
|
|
70
|
+
export declare const isOfficialAdapterRegistration: (registration: AdapterRegistration) => registration is OfficialAdapterRegistration;
|
|
71
|
+
export declare const isTestingAdapterRegistration: (registration: AdapterRegistration) => registration is TestingAdapterRegistration;
|
|
72
|
+
export type LifecycleCleanupResult = {
|
|
73
|
+
readonly kind: "completed";
|
|
74
|
+
readonly durationMillis: number;
|
|
75
|
+
} | {
|
|
76
|
+
readonly kind: "failed";
|
|
77
|
+
readonly error: AdapterFailure;
|
|
78
|
+
} | {
|
|
79
|
+
readonly kind: "deadline-exceeded";
|
|
80
|
+
readonly budgetMillis: number;
|
|
81
|
+
};
|
|
82
|
+
export type LifecycleOutcomeResult = {
|
|
83
|
+
readonly kind: "completed";
|
|
84
|
+
readonly durationMillis: number;
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: "failed";
|
|
87
|
+
readonly error: AdapterFailure;
|
|
88
|
+
} | {
|
|
89
|
+
readonly kind: "deadline-exceeded";
|
|
90
|
+
readonly budgetMillis: number;
|
|
91
|
+
readonly forcedCleanup?: LifecycleCleanupResult;
|
|
92
|
+
};
|
|
93
|
+
export type AdapterOutcome = {
|
|
94
|
+
readonly participant: "adapter";
|
|
95
|
+
readonly adapter: AdapterName;
|
|
96
|
+
readonly capability: AdapterCapability;
|
|
97
|
+
readonly stage: LifecycleStage;
|
|
98
|
+
readonly result: LifecycleOutcomeResult;
|
|
99
|
+
};
|
|
100
|
+
export type RuntimeDisposalOutcome = {
|
|
101
|
+
readonly participant: "runtime-disposal";
|
|
102
|
+
readonly result: LifecycleOutcomeResult;
|
|
103
|
+
};
|
|
104
|
+
export type LifecycleOutcome = AdapterOutcome | RuntimeDisposalOutcome;
|
|
105
|
+
export type LifecycleReport = {
|
|
106
|
+
readonly operation: "flush" | "close";
|
|
107
|
+
readonly outcomes: ReadonlyArray<LifecycleOutcome>;
|
|
108
|
+
readonly durationMillis: number;
|
|
109
|
+
readonly degraded: boolean;
|
|
110
|
+
};
|
|
111
|
+
export type StartedAdapter = {
|
|
112
|
+
readonly registration: AdapterRegistration;
|
|
113
|
+
readonly handle: ObservabilityAdapterHandle;
|
|
114
|
+
};
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Option, Predicate, Schema } from "effect";
|
|
2
|
+
import { InvalidObservabilityConfig } from "./ObservabilityConfigError.js";
|
|
3
|
+
export const AdapterName = Schema.String.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), Schema.isMaxLength(64)).pipe(Schema.brand("AdapterName"));
|
|
4
|
+
export class AdapterFailure extends Schema.TaggedError()("AdapterFailure", {
|
|
5
|
+
code: Schema.Literal("OBS_OBSERVABILITY_ADAPTER_FAILED"),
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
cause: Schema.Defect(),
|
|
8
|
+
}) {
|
|
9
|
+
}
|
|
10
|
+
const officialRegistrationBrand = Symbol("OfficialAdapterRegistration");
|
|
11
|
+
const testingRegistrationBrand = Symbol("TestingAdapterRegistration");
|
|
12
|
+
const officialRegistrations = new WeakSet();
|
|
13
|
+
const testingRegistrations = new WeakSet();
|
|
14
|
+
const AdapterRegistrationPayload = Schema.Struct({
|
|
15
|
+
name: AdapterName,
|
|
16
|
+
capability: Schema.Literals(["events", "traces", "metrics", "defects", "browser-ingest"]),
|
|
17
|
+
stage: Schema.Literals(["server", "metrics", "browser"]),
|
|
18
|
+
});
|
|
19
|
+
const parseAdapterRegistrationPayload = (adapter) => {
|
|
20
|
+
const decoded = Schema.decodeUnknownOption(AdapterRegistrationPayload)(adapter);
|
|
21
|
+
if (Option.isNone(decoded) || !Predicate.isFunction(adapter.start)) {
|
|
22
|
+
throw new InvalidObservabilityConfig({
|
|
23
|
+
code: "OBS_OBSERVABILITY_ADAPTER_UNSUPPORTED",
|
|
24
|
+
field: "adapters",
|
|
25
|
+
message: "The adapter payload is invalid. Use a valid adapter name, capability, lifecycle stage, and callable start.",
|
|
26
|
+
rule: "a schema-valid adapter payload with a callable start",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return Object.freeze(adapter);
|
|
30
|
+
};
|
|
31
|
+
export const registerOfficialAdapter = (adapter) => {
|
|
32
|
+
const registration = {
|
|
33
|
+
kind: "official",
|
|
34
|
+
adapter: parseAdapterRegistrationPayload(adapter),
|
|
35
|
+
[officialRegistrationBrand]: true,
|
|
36
|
+
};
|
|
37
|
+
officialRegistrations.add(registration);
|
|
38
|
+
return Object.freeze(registration);
|
|
39
|
+
};
|
|
40
|
+
export const registerTestingAdapter = (adapter) => {
|
|
41
|
+
const registration = {
|
|
42
|
+
kind: "testing",
|
|
43
|
+
adapter: parseAdapterRegistrationPayload(adapter),
|
|
44
|
+
[testingRegistrationBrand]: true,
|
|
45
|
+
};
|
|
46
|
+
testingRegistrations.add(registration);
|
|
47
|
+
return Object.freeze(registration);
|
|
48
|
+
};
|
|
49
|
+
export const isOfficialAdapterRegistration = (registration) => registration.kind === "official" && officialRegistrations.has(registration);
|
|
50
|
+
export const isTestingAdapterRegistration = (registration) => registration.kind === "testing" && testingRegistrations.has(registration);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { EnvironmentVariables } from "../TelemetryConfig.js";
|
|
3
|
+
import { TelemetryConfig } from "../TelemetryConfig.js";
|
|
4
|
+
import { EnvironmentAliasPolicy, type ResourceIdentity } from "../ResourceIdentity.js";
|
|
5
|
+
import type { ContractRegistry } from "./ObservabilityAdapter.js";
|
|
6
|
+
import { type DataPolicy, type DataPolicyInput } from "./DataPolicy.js";
|
|
7
|
+
import { DuplicateReleaseVariable, InvalidObservabilityConfig } from "./ObservabilityConfigError.js";
|
|
8
|
+
import { type DeploymentScope } from "./EnvironmentPolicy.js";
|
|
9
|
+
import { type NodeObservabilityProfile, type ProfileName } from "./ObservabilityProfile.js";
|
|
10
|
+
export type { DeploymentScope } from "./EnvironmentPolicy.js";
|
|
11
|
+
export type SentryConfig = {
|
|
12
|
+
readonly enabled: false;
|
|
13
|
+
} | {
|
|
14
|
+
readonly enabled: true;
|
|
15
|
+
readonly dsn: URL;
|
|
16
|
+
};
|
|
17
|
+
export type NodeObservabilityConfigDisabled = {
|
|
18
|
+
readonly enabled: false;
|
|
19
|
+
};
|
|
20
|
+
export type NodeObservabilityConfigEnabled = {
|
|
21
|
+
readonly enabled: true;
|
|
22
|
+
readonly profile: NodeObservabilityProfile;
|
|
23
|
+
readonly deployment: DeploymentScope;
|
|
24
|
+
readonly identity: ResourceIdentity;
|
|
25
|
+
readonly telemetry: TelemetryConfig;
|
|
26
|
+
readonly evlog: {
|
|
27
|
+
readonly contract: ContractRegistry;
|
|
28
|
+
readonly policy: DataPolicy;
|
|
29
|
+
};
|
|
30
|
+
readonly sentry: SentryConfig;
|
|
31
|
+
};
|
|
32
|
+
export type NodeObservabilityConfig = NodeObservabilityConfigDisabled | NodeObservabilityConfigEnabled;
|
|
33
|
+
export type NodeObservabilityConfigInput = {
|
|
34
|
+
readonly enabled: false;
|
|
35
|
+
} | {
|
|
36
|
+
readonly enabled: true;
|
|
37
|
+
readonly profile: ProfileName;
|
|
38
|
+
readonly service: {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly version: string;
|
|
41
|
+
readonly environment: string;
|
|
42
|
+
readonly instance?: string | undefined;
|
|
43
|
+
};
|
|
44
|
+
readonly telemetry: {
|
|
45
|
+
readonly endpoint: URL;
|
|
46
|
+
readonly environmentAlias?: typeof EnvironmentAliasPolicy.Type | undefined;
|
|
47
|
+
};
|
|
48
|
+
readonly evlog: {
|
|
49
|
+
readonly contract: ContractRegistry;
|
|
50
|
+
readonly policy: DataPolicyInput;
|
|
51
|
+
};
|
|
52
|
+
readonly sentry: {
|
|
53
|
+
readonly enabled: false;
|
|
54
|
+
} | {
|
|
55
|
+
readonly enabled: true;
|
|
56
|
+
readonly dsn: URL;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
export type EnvBootstrapInput = {
|
|
60
|
+
readonly enabled?: boolean | undefined;
|
|
61
|
+
readonly profile: ProfileName;
|
|
62
|
+
readonly env: EnvironmentVariables;
|
|
63
|
+
readonly contract: ContractRegistry;
|
|
64
|
+
readonly policy: DataPolicyInput;
|
|
65
|
+
readonly environmentAlias?: typeof EnvironmentAliasPolicy.Type | undefined;
|
|
66
|
+
};
|
|
67
|
+
export declare const parseNodeObservabilityConfig: (input: NodeObservabilityConfigInput) => Effect.Effect<NodeObservabilityConfig, InvalidObservabilityConfig, never>;
|
|
68
|
+
export declare const nodeObservabilityConfigFromEnv: (input: EnvBootstrapInput) => Effect.Effect<NodeObservabilityConfig, DuplicateReleaseVariable | InvalidObservabilityConfig, never>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { OtlpEndpoint, TelemetryConfig } from "../TelemetryConfig.js";
|
|
3
|
+
import { parseResourceIdentity, } from "../ResourceIdentity.js";
|
|
4
|
+
import { parseDataPolicy } from "./DataPolicy.js";
|
|
5
|
+
import { InvalidObservabilityConfig, } from "./ObservabilityConfigError.js";
|
|
6
|
+
import { deploymentScopeFromEndpoint, rejectSecondReleaseVariables, resolveEnvironmentPolicy, } from "./EnvironmentPolicy.js";
|
|
7
|
+
import { observabilityProfiles, } from "./ObservabilityProfile.js";
|
|
8
|
+
const decodeServiceName = Schema.decodeUnknownEffect(Schema.NonEmptyString);
|
|
9
|
+
const decodeServiceVersion = Schema.decodeUnknownEffect(Schema.Union([Schema.NonEmptyString, Schema.Undefined]));
|
|
10
|
+
const decodeDeploymentEnvironment = Schema.decodeUnknownEffect(Schema.Union([Schema.NonEmptyString, Schema.Undefined]));
|
|
11
|
+
const decodeServiceInstanceId = Schema.decodeUnknownEffect(Schema.Union([Schema.String, Schema.Undefined]));
|
|
12
|
+
const decodeOtlpEndpoint = Schema.decodeUnknownEffect(Schema.Union([OtlpEndpoint, Schema.Undefined]));
|
|
13
|
+
const decodeSentryDsn = Schema.decodeUnknownEffect(Schema.Union([Schema.URLFromString, Schema.Undefined]));
|
|
14
|
+
const decodeTelemetryConfig = Schema.decodeUnknownEffect(TelemetryConfig);
|
|
15
|
+
const invalid = (field, message, rule, cause) => new InvalidObservabilityConfig({
|
|
16
|
+
code: field === "profile"
|
|
17
|
+
? "OBS_OBSERVABILITY_PROFILE_UNSUPPORTED_RUNTIME"
|
|
18
|
+
: "OBS_OBSERVABILITY_CONFIG_INVALID",
|
|
19
|
+
field,
|
|
20
|
+
message,
|
|
21
|
+
rule,
|
|
22
|
+
cause,
|
|
23
|
+
});
|
|
24
|
+
const nodeProfile = (name) => {
|
|
25
|
+
const selected = observabilityProfiles[name];
|
|
26
|
+
if (selected.runtime !== "node-global") {
|
|
27
|
+
return Effect.fail(invalid("profile", `Profile "${name}" does not own a Node global runtime. Use nestjs-api, worker, or cli.`, "a profile that owns a Node global runtime"));
|
|
28
|
+
}
|
|
29
|
+
return Effect.succeed(selected);
|
|
30
|
+
};
|
|
31
|
+
const sentryFor = (profile, environment, dsn) => {
|
|
32
|
+
if (profile.defects === "required-in-production" &&
|
|
33
|
+
environment === "production" &&
|
|
34
|
+
dsn === undefined) {
|
|
35
|
+
return Effect.fail(invalid("SENTRY_DSN", `Profile "${profile.name}" requires SENTRY_DSN in production. Set SENTRY_DSN or use a non-production environment.`, "a Sentry DSN URL for production"));
|
|
36
|
+
}
|
|
37
|
+
return Effect.succeed(dsn === undefined ? { enabled: false } : { enabled: true, dsn });
|
|
38
|
+
};
|
|
39
|
+
export const parseNodeObservabilityConfig = Effect.fn("parseNodeObservabilityConfig")(function* (input) {
|
|
40
|
+
if (!input.enabled) {
|
|
41
|
+
return { enabled: false };
|
|
42
|
+
}
|
|
43
|
+
const profile = yield* nodeProfile(input.profile);
|
|
44
|
+
const identity = yield* parseResourceIdentity({
|
|
45
|
+
serviceName: input.service.name,
|
|
46
|
+
serviceVersion: input.service.version,
|
|
47
|
+
environment: input.service.environment,
|
|
48
|
+
instance: Option.fromNullishOr(input.service.instance),
|
|
49
|
+
}).pipe(Effect.mapError((cause) => invalid(cause.field === "service.name"
|
|
50
|
+
? "OTEL_SERVICE_NAME"
|
|
51
|
+
: cause.field === "service.version"
|
|
52
|
+
? "OTEL_SERVICE_VERSION"
|
|
53
|
+
: cause.field === "service.instance.id"
|
|
54
|
+
? "OTEL_SERVICE_INSTANCE_ID"
|
|
55
|
+
: "OTEL_DEPLOYMENT_ENVIRONMENT", cause.message, cause.rule, cause)));
|
|
56
|
+
const telemetry = yield* decodeTelemetryConfig({
|
|
57
|
+
identity,
|
|
58
|
+
environmentAlias: input.telemetry.environmentAlias ?? "omitted",
|
|
59
|
+
otlpEndpoint: input.telemetry.endpoint.href,
|
|
60
|
+
}).pipe(Effect.mapError((cause) => invalid("OTEL_EXPORTER_OTLP_ENDPOINT", "The OTLP endpoint is invalid. Use an HTTP or HTTPS URL without credentials.", "an HTTP or HTTPS URL without credentials", cause)));
|
|
61
|
+
const deployment = deploymentScopeFromEndpoint(telemetry.otlpEndpoint);
|
|
62
|
+
const policy = yield* parseDataPolicy(input.evlog.policy).pipe(Effect.mapError((cause) => invalid("policy", "The data policy is invalid. Fix every reported policy issue before starting observability.", "a compiled additive data policy", cause)));
|
|
63
|
+
const sentry = yield* sentryFor(profile, input.service.environment, input.sentry.enabled ? input.sentry.dsn : undefined);
|
|
64
|
+
return {
|
|
65
|
+
enabled: true,
|
|
66
|
+
profile,
|
|
67
|
+
deployment,
|
|
68
|
+
identity,
|
|
69
|
+
telemetry,
|
|
70
|
+
evlog: { contract: input.evlog.contract, policy },
|
|
71
|
+
sentry,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
export const nodeObservabilityConfigFromEnv = Effect.fn("nodeObservabilityConfigFromEnv")(function* (input) {
|
|
75
|
+
if (input.enabled === false) {
|
|
76
|
+
return { enabled: false };
|
|
77
|
+
}
|
|
78
|
+
yield* rejectSecondReleaseVariables(input.env);
|
|
79
|
+
const profile = yield* nodeProfile(input.profile);
|
|
80
|
+
const serviceName = yield* decodeServiceName(input.env.OTEL_SERVICE_NAME).pipe(Effect.mapError((cause) => invalid("OTEL_SERVICE_NAME", "OTEL_SERVICE_NAME is invalid. Set a non-empty service name.", "a non-empty service name", cause)));
|
|
81
|
+
const serviceVersion = yield* decodeServiceVersion(input.env.OTEL_SERVICE_VERSION).pipe(Effect.mapError((cause) => invalid("OTEL_SERVICE_VERSION", "OTEL_SERVICE_VERSION is invalid. Set a non-empty release identity.", "a non-empty release identity", cause)));
|
|
82
|
+
const environment = yield* decodeDeploymentEnvironment(input.env.OTEL_DEPLOYMENT_ENVIRONMENT).pipe(Effect.mapError((cause) => invalid("OTEL_DEPLOYMENT_ENVIRONMENT", "OTEL_DEPLOYMENT_ENVIRONMENT is invalid. Set a non-empty environment name.", "a non-empty environment name", cause)));
|
|
83
|
+
const serviceInstanceId = yield* decodeServiceInstanceId(input.env.OTEL_SERVICE_INSTANCE_ID).pipe(Effect.mapError((cause) => invalid("OTEL_SERVICE_INSTANCE_ID", "OTEL_SERVICE_INSTANCE_ID is invalid. Set a string instance identifier.", "a string instance identifier", cause)));
|
|
84
|
+
const endpoint = yield* decodeOtlpEndpoint(input.env.OTEL_EXPORTER_OTLP_ENDPOINT).pipe(Effect.mapError((cause) => invalid("OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT is invalid. Use an HTTP or HTTPS URL without credentials.", "an HTTP or HTTPS URL without credentials", cause)));
|
|
85
|
+
const dsn = yield* decodeSentryDsn(input.env.SENTRY_DSN).pipe(Effect.mapError((cause) => invalid("SENTRY_DSN", "SENTRY_DSN is invalid. Set a valid URL.", "a valid URL", cause)));
|
|
86
|
+
const resolution = resolveEnvironmentPolicy({
|
|
87
|
+
endpoint,
|
|
88
|
+
serviceVersion,
|
|
89
|
+
environment,
|
|
90
|
+
});
|
|
91
|
+
if (resolution.kind === "missing-remote-identity") {
|
|
92
|
+
if (resolution.missing === "service-version" ||
|
|
93
|
+
resolution.missing === "service-version-and-environment") {
|
|
94
|
+
return yield* invalid("OTEL_SERVICE_VERSION", "A remote OTLP endpoint requires OTEL_SERVICE_VERSION. Set it to the deployed release.", "an explicit release identity for a remote endpoint");
|
|
95
|
+
}
|
|
96
|
+
return yield* invalid("OTEL_DEPLOYMENT_ENVIRONMENT", "A remote OTLP endpoint requires OTEL_DEPLOYMENT_ENVIRONMENT. Set it to the deployed environment.", "an explicit environment for a remote endpoint");
|
|
97
|
+
}
|
|
98
|
+
const identity = yield* parseResourceIdentity({
|
|
99
|
+
serviceName,
|
|
100
|
+
serviceVersion: resolution.serviceVersion,
|
|
101
|
+
environment: resolution.environment,
|
|
102
|
+
instance: serviceInstanceId === "" ? Option.none() : Option.fromNullishOr(serviceInstanceId),
|
|
103
|
+
}).pipe(Effect.mapError((cause) => invalid(cause.field === "service.name"
|
|
104
|
+
? "OTEL_SERVICE_NAME"
|
|
105
|
+
: cause.field === "service.version"
|
|
106
|
+
? "OTEL_SERVICE_VERSION"
|
|
107
|
+
: cause.field === "service.instance.id"
|
|
108
|
+
? "OTEL_SERVICE_INSTANCE_ID"
|
|
109
|
+
: "OTEL_DEPLOYMENT_ENVIRONMENT", cause.message, cause.rule, cause)));
|
|
110
|
+
const policy = yield* parseDataPolicy(input.policy).pipe(Effect.mapError((cause) => invalid("policy", "The data policy is invalid. Fix every reported policy issue before starting observability.", "a compiled additive data policy", cause)));
|
|
111
|
+
const sentry = yield* sentryFor(profile, resolution.environment, dsn);
|
|
112
|
+
return {
|
|
113
|
+
enabled: true,
|
|
114
|
+
profile,
|
|
115
|
+
deployment: resolution.deployment,
|
|
116
|
+
identity,
|
|
117
|
+
telemetry: new TelemetryConfig({
|
|
118
|
+
identity,
|
|
119
|
+
environmentAlias: input.environmentAlias,
|
|
120
|
+
otlpEndpoint: resolution.endpoint,
|
|
121
|
+
}),
|
|
122
|
+
evlog: { contract: input.contract, policy },
|
|
123
|
+
sentry,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export declare const ObservabilityConfigField: Schema.Literals<readonly ["profile", "adapters", "policy", "policy.blockedValuePatterns", "OTEL_SERVICE_NAME", "OTEL_SERVICE_VERSION", "OTEL_SERVICE_INSTANCE_ID", "OTEL_DEPLOYMENT_ENVIRONMENT", "OTEL_EXPORTER_OTLP_ENDPOINT", "SENTRY_DSN"]>;
|
|
3
|
+
export type ObservabilityConfigField = typeof ObservabilityConfigField.Type;
|
|
4
|
+
declare const InvalidObservabilityConfig_base: Schema.Class<InvalidObservabilityConfig, Schema.TaggedStruct<"InvalidObservabilityConfig", {
|
|
5
|
+
readonly code: Schema.Literals<readonly ["OBS_OBSERVABILITY_CONFIG_INVALID", "OBS_OBSERVABILITY_PROFILE_UNSUPPORTED_RUNTIME", "OBS_OBSERVABILITY_ADAPTER_UNSUPPORTED", "OBS_OBSERVABILITY_ADAPTER_MISSING", "OBS_OBSERVABILITY_ADAPTER_DUPLICATE", "OBS_OBSERVABILITY_ADAPTER_TESTING"]>;
|
|
6
|
+
readonly message: Schema.String;
|
|
7
|
+
readonly field: Schema.Literals<readonly ["profile", "adapters", "policy", "policy.blockedValuePatterns", "OTEL_SERVICE_NAME", "OTEL_SERVICE_VERSION", "OTEL_SERVICE_INSTANCE_ID", "OTEL_DEPLOYMENT_ENVIRONMENT", "OTEL_EXPORTER_OTLP_ENDPOINT", "SENTRY_DSN"]>;
|
|
8
|
+
readonly rule: Schema.String;
|
|
9
|
+
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
10
|
+
}>, import("effect/Cause").YieldableError>;
|
|
11
|
+
export declare class InvalidObservabilityConfig extends InvalidObservabilityConfig_base {
|
|
12
|
+
}
|
|
13
|
+
export declare const SecondReleaseVariable: Schema.Literals<readonly ["SENTRY_RELEASE", "OTEL_SERVICE_RELEASE"]>;
|
|
14
|
+
export type SecondReleaseVariable = typeof SecondReleaseVariable.Type;
|
|
15
|
+
export declare const secondReleaseVariables: ReadonlyArray<SecondReleaseVariable>;
|
|
16
|
+
declare const DuplicateReleaseVariable_base: Schema.Class<DuplicateReleaseVariable, Schema.TaggedStruct<"DuplicateReleaseVariable", {
|
|
17
|
+
readonly code: Schema.Literal<"OBS_TELEMETRY_DUPLICATE_RELEASE_VARIABLE">;
|
|
18
|
+
readonly message: Schema.String;
|
|
19
|
+
readonly variable: Schema.Literals<readonly ["SENTRY_RELEASE", "OTEL_SERVICE_RELEASE"]>;
|
|
20
|
+
}>, import("effect/Cause").YieldableError>;
|
|
21
|
+
export declare class DuplicateReleaseVariable extends DuplicateReleaseVariable_base {
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const ObservabilityConfigField = Schema.Literals([
|
|
3
|
+
"profile",
|
|
4
|
+
"adapters",
|
|
5
|
+
"policy",
|
|
6
|
+
"policy.blockedValuePatterns",
|
|
7
|
+
"OTEL_SERVICE_NAME",
|
|
8
|
+
"OTEL_SERVICE_VERSION",
|
|
9
|
+
"OTEL_SERVICE_INSTANCE_ID",
|
|
10
|
+
"OTEL_DEPLOYMENT_ENVIRONMENT",
|
|
11
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
12
|
+
"SENTRY_DSN",
|
|
13
|
+
]);
|
|
14
|
+
export class InvalidObservabilityConfig extends Schema.TaggedError()("InvalidObservabilityConfig", {
|
|
15
|
+
code: Schema.Literals([
|
|
16
|
+
"OBS_OBSERVABILITY_CONFIG_INVALID",
|
|
17
|
+
"OBS_OBSERVABILITY_PROFILE_UNSUPPORTED_RUNTIME",
|
|
18
|
+
"OBS_OBSERVABILITY_ADAPTER_UNSUPPORTED",
|
|
19
|
+
"OBS_OBSERVABILITY_ADAPTER_MISSING",
|
|
20
|
+
"OBS_OBSERVABILITY_ADAPTER_DUPLICATE",
|
|
21
|
+
"OBS_OBSERVABILITY_ADAPTER_TESTING",
|
|
22
|
+
]),
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
field: ObservabilityConfigField,
|
|
25
|
+
rule: Schema.String,
|
|
26
|
+
cause: Schema.Defect().pipe(Schema.optionalKey),
|
|
27
|
+
}) {
|
|
28
|
+
}
|
|
29
|
+
export const SecondReleaseVariable = Schema.Literals(["SENTRY_RELEASE", "OTEL_SERVICE_RELEASE"]);
|
|
30
|
+
export const secondReleaseVariables = [
|
|
31
|
+
"SENTRY_RELEASE",
|
|
32
|
+
"OTEL_SERVICE_RELEASE",
|
|
33
|
+
];
|
|
34
|
+
export class DuplicateReleaseVariable extends Schema.TaggedError()("DuplicateReleaseVariable", {
|
|
35
|
+
code: Schema.Literal("OBS_TELEMETRY_DUPLICATE_RELEASE_VARIABLE"),
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
variable: SecondReleaseVariable,
|
|
38
|
+
}) {
|
|
39
|
+
}
|