@shipfox/api-logs 5.0.0 → 7.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/.turbo/turbo-build.log +14 -14
- package/CHANGELOG.md +46 -0
- package/dist/api/object-storage.d.ts.map +1 -1
- package/dist/api/object-storage.js +28 -2
- package/dist/api/object-storage.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -10
- package/dist/config.js.map +1 -1
- package/dist/core/append-logs.d.ts +2 -6
- package/dist/core/append-logs.d.ts.map +1 -1
- package/dist/core/append-logs.js +7 -16
- package/dist/core/append-logs.js.map +1 -1
- package/dist/core/reap-stale-open-streams.d.ts.map +1 -1
- package/dist/core/reap-stale-open-streams.js +7 -0
- package/dist/core/reap-stale-open-streams.js.map +1 -1
- package/dist/core/retention.d.ts.map +1 -1
- package/dist/core/retention.js +25 -0
- package/dist/core/retention.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -59
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/append-logs.d.ts +2 -1
- package/dist/presentation/routes/append-logs.d.ts.map +1 -1
- package/dist/presentation/routes/append-logs.js +61 -61
- package/dist/presentation/routes/append-logs.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -1
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +25 -23
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/temporal/activities/compact-stream.d.ts.map +1 -1
- package/dist/temporal/activities/compact-stream.js +17 -1
- package/dist/temporal/activities/compact-stream.js.map +1 -1
- package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -1
- package/dist/temporal/activities/compaction-reconcile.js +7 -0
- package/dist/temporal/activities/compaction-reconcile.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +17164 -3306
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -30
- package/src/api/object-storage.ts +21 -2
- package/src/config.test.ts +8 -32
- package/src/config.ts +10 -15
- package/src/core/append-logs.test.ts +13 -19
- package/src/core/append-logs.ts +12 -20
- package/src/core/reap-stale-open-streams.ts +2 -0
- package/src/core/retention.ts +5 -0
- package/src/index.ts +63 -51
- package/src/presentation/routes/append-logs.test.ts +3 -2
- package/src/presentation/routes/append-logs.ts +54 -50
- package/src/presentation/routes/index.ts +22 -19
- package/src/presentation/routes/read-logs.test.ts +3 -2
- package/src/temporal/activities/compact-stream.ts +13 -1
- package/src/temporal/activities/compaction-reconcile.ts +2 -0
- package/test/env.ts +1 -3
- package/test/fixtures/lease-token.ts +2 -1
- package/test/fixtures/workflows-client.ts +17 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -5,62 +5,66 @@ import {
|
|
|
5
5
|
appendLogsResponseSchema,
|
|
6
6
|
offsetGapResponseSchema,
|
|
7
7
|
} from '@shipfox/api-logs-dto';
|
|
8
|
+
import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
|
|
8
9
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
9
10
|
import {z} from 'zod';
|
|
10
11
|
import {appendLogs} from '#core/append-logs.js';
|
|
11
12
|
import {LeaseStreamMismatchError, MalformedLogChunkError, OffsetGapError} from '#core/errors.js';
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
export function createAppendLogsRoute(workflows: WorkflowsModuleClient) {
|
|
15
|
+
return defineRoute({
|
|
16
|
+
method: 'POST',
|
|
17
|
+
path: '/steps/:stepId/logs',
|
|
18
|
+
description: 'Append a chunk of logs for a step attempt of the leased job.',
|
|
19
|
+
schema: {
|
|
20
|
+
params: z.object({stepId: z.string().uuid()}),
|
|
21
|
+
querystring: appendLogsQuerySchema,
|
|
22
|
+
response: {
|
|
23
|
+
200: appendLogsResponseSchema,
|
|
24
|
+
409: offsetGapResponseSchema,
|
|
25
|
+
},
|
|
23
26
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const {attempt, offset} = request.query;
|
|
27
|
+
errorHandler: (error) => {
|
|
28
|
+
if (error instanceof OffsetGapError) {
|
|
29
|
+
throw new ClientError('Append offset is ahead of the committed length', 'offset-gap', {
|
|
30
|
+
status: 409,
|
|
31
|
+
details: {committed_length: error.committedLength},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (error instanceof MalformedLogChunkError) {
|
|
35
|
+
throw new ClientError(error.message, 'malformed-log-chunk', {status: 400});
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof LeaseStreamMismatchError) {
|
|
38
|
+
throw new ClientError(error.message, 'lease-stream-mismatch', {status: 403});
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
},
|
|
42
|
+
handler: async (request) => {
|
|
43
|
+
const leasedJob = requireLeasedJobContext(request);
|
|
44
|
+
const {stepId} = request.params;
|
|
45
|
+
const {attempt, offset} = request.query;
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
}
|
|
47
|
+
if (leasedJob.currentStepId !== stepId || leasedJob.currentStepAttempt !== attempt) {
|
|
48
|
+
throw new ClientError('Step not found for leased job execution', 'step-not-found', {
|
|
49
|
+
status: 404,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
const result = await appendLogs(
|
|
54
|
+
{
|
|
55
|
+
jobId: leasedJob.jobId,
|
|
56
|
+
workspaceId: leasedJob.workspaceId,
|
|
57
|
+
projectId: leasedJob.projectId,
|
|
58
|
+
workflowRunAttemptId: leasedJob.workflowRunAttemptId,
|
|
59
|
+
stepId,
|
|
60
|
+
attempt,
|
|
61
|
+
offset,
|
|
62
|
+
body: (request.body as Buffer | undefined) ?? Buffer.alloc(0),
|
|
63
|
+
},
|
|
64
|
+
workflows,
|
|
65
|
+
);
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
});
|
|
67
|
+
return {committed_length: result.committedLength, capped: result.capped};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import {AUTH_LEASED_JOB, AUTH_USER} from '@shipfox/api-auth-context';
|
|
2
|
+
import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
|
|
2
3
|
import {createRawBodyPlugin, type RouteGroup} from '@shipfox/node-fastify';
|
|
3
4
|
import {config} from '#config.js';
|
|
4
|
-
import {
|
|
5
|
+
import {createAppendLogsRoute} from './append-logs.js';
|
|
5
6
|
import {readLogsRoute} from './read-logs.js';
|
|
6
7
|
|
|
7
8
|
// Keep the lease-authed append in its own Fastify scope so the raw NDJSON parser does not
|
|
8
9
|
// disturb the JSON read route (or workflow routes). The body limit also bounds one-append
|
|
9
10
|
// budget overshoot. The read route is session-authed and workspace-scoped via the row.
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
export function createLogsRoutes(workflows: WorkflowsModuleClient): RouteGroup[] {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
prefix: '/runs/jobs/current',
|
|
15
|
+
auth: AUTH_LEASED_JOB,
|
|
16
|
+
plugins: [
|
|
17
|
+
createRawBodyPlugin({
|
|
18
|
+
contentType: 'application/x-ndjson',
|
|
19
|
+
bodyLimit: config.LOG_APPEND_BODY_LIMIT_BYTES,
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
routes: [createAppendLogsRoute(workflows)],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
prefix: '/steps',
|
|
26
|
+
auth: AUTH_USER,
|
|
27
|
+
routes: [readLogsRoute],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
@@ -35,9 +35,10 @@ import {
|
|
|
35
35
|
compactStreamActivity,
|
|
36
36
|
} from '#temporal/activities/compact-stream.js';
|
|
37
37
|
import {ndjsonBody, outputLine, recordLine} from '#test/fixtures/ndjson.js';
|
|
38
|
+
import {createTestWorkflowsClient} from '#test/fixtures/workflows-client.js';
|
|
38
39
|
import {findStream} from '#test/queries.js';
|
|
39
40
|
import {onStepAttemptTerminated} from '../subscribers/on-step-attempt-terminated.js';
|
|
40
|
-
import {
|
|
41
|
+
import {createLogsRoutes} from './index.js';
|
|
41
42
|
|
|
42
43
|
// AUTH_USER stub: a `Bearer user` request is a member of whatever workspace it names in the
|
|
43
44
|
// `x-test-workspace` header, so each test grants or withholds access against the arranged
|
|
@@ -159,7 +160,7 @@ describe('GET /steps/:stepId/attempts/:attempt/logs', () => {
|
|
|
159
160
|
beforeAll(async () => {
|
|
160
161
|
app = await createApp({
|
|
161
162
|
auth: [fakeUserAuth, stubLeaseAuth],
|
|
162
|
-
routes:
|
|
163
|
+
routes: createLogsRoutes(createTestWorkflowsClient()),
|
|
163
164
|
swagger: false,
|
|
164
165
|
});
|
|
165
166
|
await app.ready();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
2
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
1
3
|
import {Context} from '@temporalio/activity';
|
|
2
4
|
import {compactedObjectKey, deleteObject, putCompactedObject} from '#api/object-storage.js';
|
|
3
5
|
import {compactedGzipStream} from '#core/compaction.js';
|
|
@@ -79,7 +81,17 @@ async function compactStream(
|
|
|
79
81
|
stats.lastSeq !== expected.maxSeq ||
|
|
80
82
|
stats.uncompressedBytes !== expected.uncompressedBytes
|
|
81
83
|
) {
|
|
82
|
-
await deleteObject(uploadKey).catch(() =>
|
|
84
|
+
await deleteObject(uploadKey).catch((error) => {
|
|
85
|
+
logger().error(
|
|
86
|
+
{err: error, streamId: stream.id, objectKey: uploadKey},
|
|
87
|
+
'Failed to delete invalid compacted log object',
|
|
88
|
+
);
|
|
89
|
+
reportError(error, {
|
|
90
|
+
boundary: 'logs.cleanup',
|
|
91
|
+
operation: 'delete-invalid-compaction-object',
|
|
92
|
+
extra: {streamId: stream.id, objectKey: uploadKey},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
83
95
|
throw new Error(
|
|
84
96
|
`Compaction integrity check failed for stream ${stream.id}: streamed ${stats.chunkCount} chunks / ${stats.uncompressedBytes} bytes up to seq ${stats.lastSeq}, table holds ${expected.count} / ${expected.uncompressedBytes} bytes up to seq ${expected.maxSeq}`,
|
|
85
97
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
1
2
|
import {logger} from '@shipfox/node-opentelemetry';
|
|
2
3
|
import {temporalClient} from '@shipfox/node-temporal';
|
|
3
4
|
import {config} from '#config.js';
|
|
@@ -41,6 +42,7 @@ export async function compactionReconcileActivity(): Promise<{restarted: number;
|
|
|
41
42
|
{err: error, streamId: stream.id},
|
|
42
43
|
'Failed to re-drive stale stream compaction',
|
|
43
44
|
);
|
|
45
|
+
reportError(error, {boundary: 'logs.maintenance', extra: {streamId: stream.id}});
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
package/test/env.ts
CHANGED
|
@@ -4,9 +4,7 @@ process.env.POSTGRES_USERNAME ??= 'shipfox';
|
|
|
4
4
|
process.env.POSTGRES_PASSWORD ??= 'password';
|
|
5
5
|
process.env.POSTGRES_DATABASE = 'api_test';
|
|
6
6
|
process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
|
|
7
|
-
process.env.
|
|
8
|
-
process.env.AUTH_JOB_LEASE_TOKEN_SECRET = 'test-lease-secret';
|
|
9
|
-
process.env.AUTH_RUNNER_SESSION_TOKEN_SECRET = 'test-runner-session-secret';
|
|
7
|
+
process.env.AUTH_ROOT_KEY = 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
|
|
10
8
|
process.env.SECRETS_ENCRYPTION_KEK = 'ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMTA=';
|
|
11
9
|
|
|
12
10
|
// Small accrual budget so cap/budget tests use tiny payloads: base 100 bytes,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {JOB_LEASE_TOKEN_AUDIENCE} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {jobLeaseTokenKey} from '@shipfox/node-auth-root-key';
|
|
2
3
|
import {signHs256} from '@shipfox/node-jwt';
|
|
3
4
|
|
|
4
5
|
// Matches test/env.ts; the lease-token auth method reads this same value from config.
|
|
5
|
-
const SECRET =
|
|
6
|
+
const SECRET = jobLeaseTokenKey();
|
|
6
7
|
|
|
7
8
|
export interface MintLeaseTokenParams {
|
|
8
9
|
jobId: string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type WorkflowsModuleClient,
|
|
3
|
+
workflowsInterModuleContract,
|
|
4
|
+
} from '@shipfox/api-workflows-dto/inter-module';
|
|
5
|
+
import {defineInterModulePresentation} from '@shipfox/inter-module';
|
|
6
|
+
import {createFakeInterModuleClients} from '@shipfox/node-module/inter-module/testing';
|
|
7
|
+
|
|
8
|
+
export function createTestWorkflowsClient(): WorkflowsModuleClient {
|
|
9
|
+
return createFakeInterModuleClients({
|
|
10
|
+
workflows: defineInterModulePresentation(workflowsInterModuleContract, {
|
|
11
|
+
startRunFromTrigger: vi.fn(),
|
|
12
|
+
deliverEventToJobListener: vi.fn(),
|
|
13
|
+
getStepLogContext: () => ({harness: 'pi' as const}),
|
|
14
|
+
getLeasedAgentToolContext: vi.fn(),
|
|
15
|
+
}),
|
|
16
|
+
}).workflows;
|
|
17
|
+
}
|