deepline 0.3.124 → 0.3.126
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/bundling-sources/sdk/src/client.ts +10 -0
- package/dist/bundling-sources/sdk/src/compat.ts +17 -2
- package/dist/bundling-sources/sdk/src/play.ts +3 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +61 -10
- package/dist/bundling-sources/shared_libs/observability/redaction.ts +3 -0
- package/dist/bundling-sources/shared_libs/observability/runtime-console.ts +174 -0
- package/dist/bundling-sources/shared_libs/observability/telemetry.ts +26 -2
- package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/contracts.ts +20 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts +28 -11
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-telemetry.ts +121 -1
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +91 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +87 -41
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/scheduler-capacity.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +1 -0
- package/dist/bundling-sources/shared_libs/security/secret-redaction.ts +56 -1
- package/dist/cli/index.js +528 -466
- package/dist/cli/index.mjs +528 -466
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/install-integrity.json +1 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -1086,8 +1086,12 @@ export type MonitorJobReconciliationState =
|
|
|
1086
1086
|
|
|
1087
1087
|
export type MonitorJobProjection = {
|
|
1088
1088
|
id: string;
|
|
1089
|
+
/** Durable provider operation identity used to trace Fleet/deploy work. */
|
|
1090
|
+
operation_id?: string;
|
|
1089
1091
|
kind: 'batch_deploy' | 'fleet_deploy';
|
|
1090
1092
|
state: MonitorJobState;
|
|
1093
|
+
/** Terminal semantic result; `blocked` is a deterministic Fleet/input refusal, not a provider outage. */
|
|
1094
|
+
outcome?: 'blocked' | 'failed';
|
|
1091
1095
|
phase: MonitorJobPhase;
|
|
1092
1096
|
target: {
|
|
1093
1097
|
monitor_key?: string;
|
|
@@ -1135,6 +1139,8 @@ export type MonitorJobResult = {
|
|
|
1135
1139
|
ok?: boolean;
|
|
1136
1140
|
job?: MonitorJobProjection;
|
|
1137
1141
|
batch?: MonitorNamedBatchProjection;
|
|
1142
|
+
/** Same durable operation identity as `job.operation_id`, for log reads. */
|
|
1143
|
+
operation_id?: string;
|
|
1138
1144
|
logs?: Array<{
|
|
1139
1145
|
level: string;
|
|
1140
1146
|
phase: string;
|
|
@@ -1754,6 +1760,10 @@ export type TargetBillingStatusResult = {
|
|
|
1754
1760
|
recharge_state: string;
|
|
1755
1761
|
next_action: string | null;
|
|
1756
1762
|
pending_plan_sku: string | null;
|
|
1763
|
+
/** Saved transition boundary; optional for older servers. */
|
|
1764
|
+
pending_plan_starts_at?: string | null;
|
|
1765
|
+
/** Next cycle boundary from the current Contract’s retained cadence. */
|
|
1766
|
+
current_period_end?: string | null;
|
|
1757
1767
|
as_of: string | null;
|
|
1758
1768
|
};
|
|
1759
1769
|
|
|
@@ -395,18 +395,33 @@ export async function checkSdkCompatibility(
|
|
|
395
395
|
}
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
+
/**
|
|
399
|
+
* A CI job (`CI` or `GITHUB_ACTIONS`): nobody is at the keyboard to act on an
|
|
400
|
+
* advisory, and the pinned version is deliberate. Agent sessions are also
|
|
401
|
+
* non-TTY but are not CI, and they rely on these advisories, so this keys on
|
|
402
|
+
* the CI environment rather than on a missing terminal.
|
|
403
|
+
*/
|
|
404
|
+
export function isCiEnvironment(): boolean {
|
|
405
|
+
return ['CI', 'GITHUB_ACTIONS'].some((name) => {
|
|
406
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
407
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
398
411
|
export function enforceSdkCompatibilityResponse(
|
|
399
412
|
response: SdkCompatibilityResponse | null,
|
|
400
413
|
): void {
|
|
401
414
|
if (!response) {
|
|
402
415
|
return;
|
|
403
416
|
}
|
|
417
|
+
// A version the server no longer supports fails everywhere, CI included.
|
|
404
418
|
if (response.update_required) {
|
|
405
419
|
throw new Error(response.message);
|
|
406
420
|
}
|
|
407
421
|
if (
|
|
408
|
-
response.status === 'deprecated' ||
|
|
409
|
-
|
|
422
|
+
(response.status === 'deprecated' ||
|
|
423
|
+
response.status === 'update_available') &&
|
|
424
|
+
!isCiEnvironment()
|
|
410
425
|
) {
|
|
411
426
|
process.stderr.write(`${response.message}\n`);
|
|
412
427
|
}
|
|
@@ -195,7 +195,9 @@ export type PlayFetchResponse = PlayAuthoringFetchResponse;
|
|
|
195
195
|
* `runtime: { timeout: '90m', size: 'standard' }`; duration values are whole minutes or hours, up to `4h`.
|
|
196
196
|
* It differs from `ctx.tools.execute({ timeoutMs })`, which limits one provider call.
|
|
197
197
|
*
|
|
198
|
-
* @example Webhook with HMAC verification
|
|
198
|
+
* @example Webhook with HMAC verification. `secretEnv` is the Deepline Secret
|
|
199
|
+
* name configured with `deepline secrets set`, retained as the legacy field
|
|
200
|
+
* name for SDK compatibility.
|
|
199
201
|
* ```typescript
|
|
200
202
|
* definePlay('webhook-handler', handler, {
|
|
201
203
|
* webhook: {
|
|
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
|
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
202
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
203
|
-
version: '0.3.
|
|
203
|
+
version: '0.3.126',
|
|
204
204
|
updateSummary:
|
|
205
205
|
'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
|
|
206
206
|
packageCapabilities: {
|
|
@@ -21,9 +21,10 @@ export const PROVIDER_MONITOR_BATCH_OPERATION_KINDS = [
|
|
|
21
21
|
// operation is the durable, observable hand-off to the fenced Fleet
|
|
22
22
|
// executor; it is not a second lifecycle verb or a raw provider create.
|
|
23
23
|
'converge',
|
|
24
|
-
// A named Batch
|
|
25
|
-
// durable operation. It is intentionally distinct from manifest
|
|
26
|
-
// omission/removal semantics belong to the Batch desired
|
|
24
|
+
// A named Batch coordinates a bounded mixed plan (sync/start/stop) under
|
|
25
|
+
// one durable operation. It is intentionally distinct from manifest
|
|
26
|
+
// deploy: omission/removal semantics belong to the Batch desired set, not
|
|
27
|
+
// to monitor lifecycle ownership.
|
|
27
28
|
'named_batch',
|
|
28
29
|
] as const;
|
|
29
30
|
|
|
@@ -127,7 +128,60 @@ export const PROVIDER_MONITOR_BATCH_RESULT_STATES = [
|
|
|
127
128
|
export type ProviderMonitorBatchResultState =
|
|
128
129
|
(typeof PROVIDER_MONITOR_BATCH_RESULT_STATES)[number];
|
|
129
130
|
|
|
130
|
-
const
|
|
131
|
+
export const PROVIDER_MONITOR_BATCH_IDEMPOTENCY_KEY_MAX_LENGTH = 96;
|
|
132
|
+
const IDEMPOTENCY_KEY = new RegExp(
|
|
133
|
+
`^[A-Za-z0-9][A-Za-z0-9._-]{0,${PROVIDER_MONITOR_BATCH_IDEMPOTENCY_KEY_MAX_LENGTH - 1}}$`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
export function validateProviderMonitorBatchIdempotencyKey(
|
|
137
|
+
value: string,
|
|
138
|
+
): string {
|
|
139
|
+
const idempotencyKey = value.trim();
|
|
140
|
+
if (!IDEMPOTENCY_KEY.test(idempotencyKey)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`idempotencyKey must be 1-${PROVIDER_MONITOR_BATCH_IDEMPOTENCY_KEY_MAX_LENGTH} characters of letters, numbers, dot, underscore, or hyphen.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return idempotencyKey;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fleet control is the only durable monitor ownership boundary. A Batch is a
|
|
150
|
+
* deploy mechanism, so it never becomes a customer-facing owner or conflict.
|
|
151
|
+
* Keep this classifier in the provider-agnostic package so Fleet refusals use
|
|
152
|
+
* one durable result vocabulary without importing integration-layer errors.
|
|
153
|
+
*/
|
|
154
|
+
export type ProviderMonitorOwnershipConflict = {
|
|
155
|
+
code: 'monitor_owned_by_fleet';
|
|
156
|
+
message: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export function providerMonitorOwnershipConflict(
|
|
160
|
+
error: unknown,
|
|
161
|
+
): ProviderMonitorOwnershipConflict | null {
|
|
162
|
+
const value =
|
|
163
|
+
error && typeof error === 'object'
|
|
164
|
+
? (error as { code?: unknown; message?: unknown })
|
|
165
|
+
: undefined;
|
|
166
|
+
const code = typeof value?.code === 'string' ? value.code : '';
|
|
167
|
+
const message =
|
|
168
|
+
typeof value?.message === 'string' ? value.message : String(error);
|
|
169
|
+
const text = `${code} ${message}`.toUpperCase();
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
text.includes('MONITOR_MANAGED_BY_FLEET') ||
|
|
173
|
+
text.includes('MONITOR_FLEET_MONITOR_OWNED_BY_OTHER_FLEET') ||
|
|
174
|
+
text.includes('FLEET_SCOPE_REQUIRED') ||
|
|
175
|
+
/MANAGED BY (?:MONITOR )?FLEET/.test(text)
|
|
176
|
+
) {
|
|
177
|
+
return {
|
|
178
|
+
code: 'monitor_owned_by_fleet',
|
|
179
|
+
message:
|
|
180
|
+
'This monitor is managed by a Fleet. Change the Fleet definition instead.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
131
185
|
|
|
132
186
|
function canonicalJson(value: unknown): string {
|
|
133
187
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
@@ -173,14 +227,11 @@ export function providerMonitorBatchOperationId(input: {
|
|
|
173
227
|
idempotencyKey: string;
|
|
174
228
|
}): string {
|
|
175
229
|
const orgId = input.orgId.trim();
|
|
176
|
-
const idempotencyKey = input.idempotencyKey.trim();
|
|
177
230
|
if (!orgId)
|
|
178
231
|
throw new Error('A monitor batch operation requires an organization.');
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
);
|
|
183
|
-
}
|
|
232
|
+
const idempotencyKey = validateProviderMonitorBatchIdempotencyKey(
|
|
233
|
+
input.idempotencyKey,
|
|
234
|
+
);
|
|
184
235
|
return `monitor-batch:${orgId}:${idempotencyKey}`;
|
|
185
236
|
}
|
|
186
237
|
|
|
@@ -23,6 +23,8 @@ const INLINE_SECRET_PATTERN = new RegExp(
|
|
|
23
23
|
`\\b(${INLINE_SECRET_FIELD_NAME})(\\s*[:=]\\s*)([^\\s,;]+)`,
|
|
24
24
|
'gi',
|
|
25
25
|
);
|
|
26
|
+
const POSTGRES_CREDENTIAL_PATTERN =
|
|
27
|
+
/\b(postgres(?:ql)?:\/\/[^:\s/@]+:)[^@\s]+@/gi;
|
|
26
28
|
|
|
27
29
|
/** Whether a structured telemetry field name must never retain its value. */
|
|
28
30
|
export function isSensitiveTelemetryFieldName(value: string): boolean {
|
|
@@ -59,6 +61,7 @@ function redactStructuredValue(
|
|
|
59
61
|
function redactPlainTelemetryText(value: string): string {
|
|
60
62
|
return value
|
|
61
63
|
.replace(PRIVATE_KEY_PATTERN, '[REDACTED]')
|
|
64
|
+
.replace(POSTGRES_CREDENTIAL_PATTERN, '$1[REDACTED]@')
|
|
62
65
|
.replace(AUTHORIZATION_CREDENTIAL_PATTERN, '$1$2$3 [REDACTED]')
|
|
63
66
|
.replace(BEARER_CREDENTIAL_PATTERN, 'Bearer [REDACTED]')
|
|
64
67
|
.replace(BASIC_CREDENTIAL_PATTERN, 'Basic [REDACTED]')
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export type RuntimeConsoleLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
|
|
3
|
+
type RuntimeConsoleMethod = (...args: unknown[]) => void;
|
|
4
|
+
type RuntimeConsoleTarget = Console;
|
|
5
|
+
|
|
6
|
+
const RUNTIME_CONSOLE_PATCHED = Symbol.for(
|
|
7
|
+
'deepline.runtime-console-logging.v1',
|
|
8
|
+
);
|
|
9
|
+
const MAX_VALUE_DEPTH = 5;
|
|
10
|
+
const MAX_STRING_LENGTH = 8_000;
|
|
11
|
+
|
|
12
|
+
function trim(value: string): string {
|
|
13
|
+
return value.length <= MAX_STRING_LENGTH
|
|
14
|
+
? value
|
|
15
|
+
: `${value.slice(0, MAX_STRING_LENGTH)}…`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function safeValue(
|
|
19
|
+
value: unknown,
|
|
20
|
+
seen = new WeakSet<object>(),
|
|
21
|
+
depth = 0,
|
|
22
|
+
): unknown {
|
|
23
|
+
if (
|
|
24
|
+
value === null ||
|
|
25
|
+
typeof value === 'boolean' ||
|
|
26
|
+
typeof value === 'number'
|
|
27
|
+
) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'string') return trim(value);
|
|
31
|
+
if (typeof value === 'bigint') return String(value);
|
|
32
|
+
if (value instanceof Error) {
|
|
33
|
+
return {
|
|
34
|
+
name: value.name || 'Error',
|
|
35
|
+
message: trim(value.message),
|
|
36
|
+
...(value.stack ? { stack: trim(value.stack) } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (typeof value !== 'object') return String(value);
|
|
40
|
+
if (depth >= MAX_VALUE_DEPTH) return '[max-depth]';
|
|
41
|
+
if (seen.has(value)) return '[circular]';
|
|
42
|
+
seen.add(value);
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
const result = value
|
|
45
|
+
.slice(0, 100)
|
|
46
|
+
.map((item) => safeValue(item, seen, depth + 1));
|
|
47
|
+
if (value.length > 100) result.push('[truncated]');
|
|
48
|
+
seen.delete(value);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
const result: Record<string, unknown> = {};
|
|
52
|
+
for (const [key, child] of Object.entries(value).slice(0, 100)) {
|
|
53
|
+
result[key] = safeValue(child, seen, depth + 1);
|
|
54
|
+
}
|
|
55
|
+
if (Object.keys(value).length > 100) result.__truncated__ = true;
|
|
56
|
+
seen.delete(value);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parsedObject(value: unknown): Record<string, unknown> | null {
|
|
61
|
+
if (typeof value !== 'string') return null;
|
|
62
|
+
try {
|
|
63
|
+
const parsed: unknown = JSON.parse(value);
|
|
64
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
65
|
+
? (parsed as Record<string, unknown>)
|
|
66
|
+
: null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function consolePayload(
|
|
73
|
+
level: RuntimeConsoleLevel,
|
|
74
|
+
args: readonly unknown[],
|
|
75
|
+
): Record<string, unknown> {
|
|
76
|
+
const parsed = args.length === 1 ? parsedObject(args[0]) : null;
|
|
77
|
+
if (parsed) {
|
|
78
|
+
return {
|
|
79
|
+
...(safeValue(parsed) as Record<string, unknown>),
|
|
80
|
+
level,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const payload: Record<string, unknown> = { level };
|
|
85
|
+
const text: string[] = [];
|
|
86
|
+
for (const arg of args) {
|
|
87
|
+
if (typeof arg === 'string') {
|
|
88
|
+
text.push(arg);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg instanceof Error) {
|
|
92
|
+
payload.error = safeValue(arg);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const value = safeValue(arg);
|
|
96
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
97
|
+
for (const [key, child] of Object.entries(value)) {
|
|
98
|
+
if (key === 'level') {
|
|
99
|
+
payload.consoleLevel = child;
|
|
100
|
+
} else if (key === 'message' && text.length === 0) {
|
|
101
|
+
payload.message ??= child;
|
|
102
|
+
} else if (key === 'message') {
|
|
103
|
+
payload.contextMessage = child;
|
|
104
|
+
} else {
|
|
105
|
+
payload[key] = child;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
text.push(String(value));
|
|
111
|
+
}
|
|
112
|
+
if (text.length > 0 && payload.message === undefined) {
|
|
113
|
+
payload.message = trim(text.join(' '));
|
|
114
|
+
}
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Preserve the native console API while making Fly log records one-line and
|
|
120
|
+
* explicitly severity-bearing. Fly's log stream labels stdout and stderr as
|
|
121
|
+
* info, so the level has to travel in the application payload.
|
|
122
|
+
*/
|
|
123
|
+
export function installRuntimeConsoleLogging(
|
|
124
|
+
target: RuntimeConsoleTarget = console,
|
|
125
|
+
): void {
|
|
126
|
+
const metadata = target as Console & { [key: symbol]: unknown };
|
|
127
|
+
if (metadata[RUNTIME_CONSOLE_PATCHED] === true) return;
|
|
128
|
+
Object.defineProperty(metadata, RUNTIME_CONSOLE_PATCHED, {
|
|
129
|
+
configurable: false,
|
|
130
|
+
enumerable: false,
|
|
131
|
+
value: true,
|
|
132
|
+
writable: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const methods: Array<
|
|
136
|
+
readonly ['debug' | 'info' | 'warn' | 'error' | 'log', RuntimeConsoleLevel]
|
|
137
|
+
> = [
|
|
138
|
+
['debug', 'debug'],
|
|
139
|
+
['info', 'info'],
|
|
140
|
+
['warn', 'warn'],
|
|
141
|
+
['error', 'error'],
|
|
142
|
+
['log', 'info'],
|
|
143
|
+
];
|
|
144
|
+
for (const [method, level] of methods) {
|
|
145
|
+
const original = target[method] as RuntimeConsoleMethod;
|
|
146
|
+
target[method] = ((...args: unknown[]) => {
|
|
147
|
+
original(JSON.stringify(consolePayload(level, args)));
|
|
148
|
+
}) as Console[typeof method];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createRateLimitedRuntimeLogger(input: {
|
|
153
|
+
log: RuntimeConsoleMethod;
|
|
154
|
+
intervalMs?: number;
|
|
155
|
+
now?: () => number;
|
|
156
|
+
}): RuntimeConsoleMethod {
|
|
157
|
+
const intervalMs = input.intervalMs ?? 60_000;
|
|
158
|
+
const now = input.now ?? Date.now;
|
|
159
|
+
let lastLoggedAt = Number.NEGATIVE_INFINITY;
|
|
160
|
+
let suppressed = 0;
|
|
161
|
+
|
|
162
|
+
return (...args: unknown[]) => {
|
|
163
|
+
const current = now();
|
|
164
|
+
if (current - lastLoggedAt < intervalMs) {
|
|
165
|
+
suppressed += 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const extra =
|
|
169
|
+
suppressed > 0 ? { suppressedSinceLastLog: suppressed } : null;
|
|
170
|
+
suppressed = 0;
|
|
171
|
+
lastLoggedAt = current;
|
|
172
|
+
input.log(...args, ...(extra ? [extra] : []));
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -64,8 +64,10 @@ type TelemetryEventBase = {
|
|
|
64
64
|
export type TelemetryLogEvent = TelemetryEventBase & {
|
|
65
65
|
telemetryKind: 'log';
|
|
66
66
|
telemetryLevel: TelemetryLevel;
|
|
67
|
+
errorCode?: string;
|
|
67
68
|
errorType?: string;
|
|
68
69
|
errorMessage?: string;
|
|
70
|
+
errorStack?: string;
|
|
69
71
|
};
|
|
70
72
|
|
|
71
73
|
export type TelemetryMetricEvent = TelemetryEventBase & {
|
|
@@ -80,8 +82,10 @@ export type TelemetrySpanEvent = TelemetryEventBase & {
|
|
|
80
82
|
telemetryLevel: 'info' | 'warn' | 'error';
|
|
81
83
|
spanDurationMs: number;
|
|
82
84
|
spanOutcome: TelemetrySpanOutcome;
|
|
85
|
+
errorCode?: string;
|
|
83
86
|
errorType?: string;
|
|
84
87
|
errorMessage?: string;
|
|
88
|
+
errorStack?: string;
|
|
85
89
|
};
|
|
86
90
|
|
|
87
91
|
export type TelemetryEvent =
|
|
@@ -293,12 +297,30 @@ function normalizeContext(context: TelemetryContext | undefined) {
|
|
|
293
297
|
|
|
294
298
|
function errorFields(
|
|
295
299
|
error: unknown,
|
|
296
|
-
|
|
300
|
+
options: { includeStack?: boolean } = {},
|
|
301
|
+
): Partial<
|
|
302
|
+
Pick<
|
|
303
|
+
TelemetrySpanEvent,
|
|
304
|
+
'errorCode' | 'errorType' | 'errorMessage' | 'errorStack'
|
|
305
|
+
>
|
|
306
|
+
> {
|
|
297
307
|
if (error === undefined || error === null) return {};
|
|
298
308
|
if (error instanceof Error) {
|
|
309
|
+
const code =
|
|
310
|
+
typeof (error as Error & { code?: unknown }).code === 'string'
|
|
311
|
+
? (error as Error & { code: string }).code.trim()
|
|
312
|
+
: '';
|
|
299
313
|
return {
|
|
314
|
+
...(code ? { errorCode: code } : {}),
|
|
300
315
|
errorType: error.name || 'Error',
|
|
301
316
|
errorMessage: redactTelemetryString(error.message),
|
|
317
|
+
...(options.includeStack !== false && error.stack
|
|
318
|
+
? {
|
|
319
|
+
errorStack: String(
|
|
320
|
+
normalizeFieldValue('errorStack', error.stack) ?? error.stack,
|
|
321
|
+
),
|
|
322
|
+
}
|
|
323
|
+
: {}),
|
|
302
324
|
};
|
|
303
325
|
}
|
|
304
326
|
return {
|
|
@@ -420,7 +442,9 @@ export function createTelemetry(input: CreateTelemetryInput): Telemetry {
|
|
|
420
442
|
triage: errorTriage(fields),
|
|
421
443
|
telemetryKind: 'log',
|
|
422
444
|
telemetryLevel: 'error',
|
|
423
|
-
...errorFields(error
|
|
445
|
+
...errorFields(error, {
|
|
446
|
+
includeStack: fields?.errorStack === undefined,
|
|
447
|
+
}),
|
|
424
448
|
});
|
|
425
449
|
},
|
|
426
450
|
counter: (name, value = 1, fields) =>
|
|
@@ -77,7 +77,9 @@ function blocked(code: PlayAdmissionBlockedCode): PlayAdmissionFailure {
|
|
|
77
77
|
message:
|
|
78
78
|
code === PLAY_SECRET_UNAVAILABLE
|
|
79
79
|
? 'The Play configuration is unavailable. Repair it, then retry this delivery.'
|
|
80
|
-
:
|
|
80
|
+
: code === PLAY_RUN_LAUNCH_CONFLICT
|
|
81
|
+
? 'This run identity is already bound to a different launch. Repair the trigger identity before retrying this delivery.'
|
|
82
|
+
: 'This delivery needs attention before it can be retried.',
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -55,6 +55,26 @@ export class PlayRunLaunchConflictError extends Error {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Recognize the conflict across the mixed-version admission boundary.
|
|
60
|
+
*
|
|
61
|
+
* The Postgres adapter normally converts the database message into the typed
|
|
62
|
+
* error above. During a rolling deploy an older scheduler may still return the
|
|
63
|
+
* original message, so callers must recognize both forms before wrapping it as
|
|
64
|
+
* a generic scheduler failure.
|
|
65
|
+
*/
|
|
66
|
+
export function isPlayRunLaunchConflictError(error: unknown): boolean {
|
|
67
|
+
if (!(error instanceof Error)) return false;
|
|
68
|
+
if (
|
|
69
|
+
error instanceof PlayRunLaunchConflictError ||
|
|
70
|
+
(error as Error & { code?: unknown }).code === 'PLAY_RUN_LAUNCH_CONFLICT' ||
|
|
71
|
+
error.name === 'PlayRunLaunchConflictError'
|
|
72
|
+
) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return /already exists with a different launch/i.test(error.message);
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
/** A fenced replay named a Run that was never durably admitted. */
|
|
59
79
|
export class PlayRunAdmissionReplayMissingError extends Error {
|
|
60
80
|
readonly code = 'PLAY_RUN_ADMISSION_REPLAY_MISSING';
|
|
@@ -3,6 +3,7 @@ export { createPostgresPlayRunAdmissionPorts } from '../../play-runs/admission/p
|
|
|
3
3
|
export {
|
|
4
4
|
PlayRunAdmissionReplayMissingError,
|
|
5
5
|
PlayRunLaunchConflictError,
|
|
6
|
+
isPlayRunLaunchConflictError,
|
|
6
7
|
} from '../../play-runs/admission/contracts';
|
|
7
8
|
export type {
|
|
8
9
|
PlayRunAdmissionInput,
|
package/dist/bundling-sources/shared_libs/play-runtime/play-runs/admission/postgres-adapter.ts
CHANGED
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
PlayRunAdmissionReplayMissingError,
|
|
7
7
|
PlayRunLaunchConflictError,
|
|
8
|
+
isPlayRunLaunchConflictError,
|
|
8
9
|
type PlayRunAdmissionInput,
|
|
9
10
|
type PlayRunAdmissionReceipt,
|
|
10
11
|
} from '../../play-runs/admission/contracts';
|
|
11
12
|
import type { PlayRunAdmissionPorts } from '../../play-runs/admission/ports';
|
|
13
|
+
import { emitPlayRunLaunchConflictTelemetry } from '../../runtime-telemetry';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* PostgreSQL/Absurd implementation of the admission seam. The delegated
|
|
@@ -36,14 +38,33 @@ export function createPostgresPlayRunAdmissionPorts(input: {
|
|
|
36
38
|
);
|
|
37
39
|
} catch (error) {
|
|
38
40
|
if (isLaunchConflict(error)) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const conflict =
|
|
42
|
+
error instanceof PlayRunLaunchConflictError
|
|
43
|
+
? error
|
|
44
|
+
: new PlayRunLaunchConflictError(admission.launch.runId, {
|
|
45
|
+
cause: error,
|
|
46
|
+
});
|
|
47
|
+
emitPlayRunLaunchConflictTelemetry({
|
|
48
|
+
orgId: admission.launch.orgId,
|
|
49
|
+
runId: admission.launch.runId,
|
|
50
|
+
playName: admission.launch.playName,
|
|
51
|
+
triggerSource: admission.launch.triggerSource,
|
|
52
|
+
workflowFamilyKey: admission.launch.workflowFamilyKey,
|
|
53
|
+
revisionId: admission.launch.revisionId,
|
|
54
|
+
artifactHash: admission.launch.artifactHash,
|
|
55
|
+
graphHash: admission.launch.graphHash,
|
|
56
|
+
inputSha256: admission.launch.inputSha256,
|
|
57
|
+
error: conflict,
|
|
41
58
|
});
|
|
59
|
+
throw conflict;
|
|
42
60
|
}
|
|
43
61
|
if (isExistingOnlyReplayMissing(error)) {
|
|
44
|
-
throw new PlayRunAdmissionReplayMissingError(
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
throw new PlayRunAdmissionReplayMissingError(
|
|
63
|
+
admission.launch.runId,
|
|
64
|
+
{
|
|
65
|
+
cause: error,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
47
68
|
}
|
|
48
69
|
throw error;
|
|
49
70
|
}
|
|
@@ -54,14 +75,10 @@ export function createPostgresPlayRunAdmissionPorts(input: {
|
|
|
54
75
|
|
|
55
76
|
function isExistingOnlyReplayMissing(error: unknown): boolean {
|
|
56
77
|
return (
|
|
57
|
-
error instanceof Error &&
|
|
58
|
-
/existing-only admission/i.test(error.message)
|
|
78
|
+
error instanceof Error && /existing-only admission/i.test(error.message)
|
|
59
79
|
);
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
function isLaunchConflict(error: unknown): boolean {
|
|
63
|
-
return (
|
|
64
|
-
error instanceof Error &&
|
|
65
|
-
/already exists with a different launch/i.test(error.message)
|
|
66
|
-
);
|
|
83
|
+
return isPlayRunLaunchConflictError(error);
|
|
67
84
|
}
|