@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/temporal/activities/compact-stream.ts"],"sourcesContent":["import {Context} from '@temporalio/activity';\nimport {compactedObjectKey, deleteObject, putCompactedObject} from '#api/object-storage.js';\nimport {compactedGzipStream} from '#core/compaction.js';\nimport {chunkStats} from '#db/chunks.js';\nimport {db, type Transaction} from '#db/db.js';\nimport {getAttemptStreamById, setObjectKeyAndDeleteChunks} from '#db/streams.js';\nimport {type CompactionMetricOutcome, compactionCount} from '#metrics/instance.js';\n\nexport type CompactStreamResult =\n | {outcome: 'gone'}\n | {outcome: 'already-compacted'}\n | {outcome: 'superseded'}\n | {outcome: 'retention-raced'}\n | {outcome: 'compacted'; objectKey: string; chunkCount: number; uncompressedBytes: number};\n\ninterface CompactStreamDependencies {\n compactedGzipStream: typeof compactedGzipStream;\n setObjectKeyAndDeleteChunks: (\n tx: Transaction,\n params: {streamId: string; objectKey: string},\n ) => Promise<{updated: boolean}>;\n}\n\nconst defaultDependencies: CompactStreamDependencies = {\n compactedGzipStream,\n setObjectKeyAndDeleteChunks,\n};\n\n/**\n * Compacts one closed stream into a single gzip NDJSON object, then deletes its chunk rows.\n *\n * load stream ──┬─ gone ─────────────► no-op (retention raced)\n * ├─ object_key set ───► no-op (idempotent re-run)\n * └─ else: upload to a per-attempt key ─► gzip ─► multipart (abort-aware, heartbeat)\n * │\n * verify streamed count/maxSeq/bytes == table (mismatch ─► delete upload, throw, retry)\n * │\n * tx: set object_key (state='closed' AND object_key IS NULL) + delete chunks\n * └─ 0 rows ─► delete this attempt's upload, then re-read the row:\n * gone ─► retention raced · keyed ─► superseded by another attempt\n *\n * Each attempt uploads to its own `compactedObjectKey(stream, uuid)`, so a slow or zombie\n * attempt can never overwrite a published object. The single-winner publish (object_key set\n * + chunk delete, atomic) drops chunks only once a complete object is durable; the integrity\n * check (count, maxSeq, and byte total) guards a read bug from publishing a truncated object\n * before the only copy of the source is gone (S3 part checksums cover byte transfer).\n */\nasync function compactStream(\n params: {streamId: string},\n dependencies: CompactStreamDependencies,\n): Promise<CompactStreamResult> {\n const stream = await getAttemptStreamById(params.streamId);\n if (!stream) return {outcome: 'gone'};\n if (stream.objectKey) return {outcome: 'already-compacted'};\n\n const ctx = Context.current();\n const uploadKey = compactedObjectKey(stream, crypto.randomUUID());\n const expected = await chunkStats(stream.id);\n const {body, stats} = dependencies.compactedGzipStream({\n streamId: stream.id,\n onPage: () => ctx.heartbeat(),\n });\n\n await putCompactedObject({\n key: uploadKey,\n body,\n signal: ctx.cancellationSignal,\n onProgress: () => ctx.heartbeat(),\n metadata: {\n stream_id: stream.id,\n chunk_count: String(expected.count),\n uncompressed_bytes: String(expected.uncompressedBytes),\n last_seq: String(expected.maxSeq),\n },\n });\n\n if (\n stats.chunkCount !== expected.count ||\n stats.lastSeq !== expected.maxSeq ||\n stats.uncompressedBytes !== expected.uncompressedBytes\n ) {\n await deleteObject(uploadKey).catch(() =>
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/compact-stream.ts"],"sourcesContent":["import {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {Context} from '@temporalio/activity';\nimport {compactedObjectKey, deleteObject, putCompactedObject} from '#api/object-storage.js';\nimport {compactedGzipStream} from '#core/compaction.js';\nimport {chunkStats} from '#db/chunks.js';\nimport {db, type Transaction} from '#db/db.js';\nimport {getAttemptStreamById, setObjectKeyAndDeleteChunks} from '#db/streams.js';\nimport {type CompactionMetricOutcome, compactionCount} from '#metrics/instance.js';\n\nexport type CompactStreamResult =\n | {outcome: 'gone'}\n | {outcome: 'already-compacted'}\n | {outcome: 'superseded'}\n | {outcome: 'retention-raced'}\n | {outcome: 'compacted'; objectKey: string; chunkCount: number; uncompressedBytes: number};\n\ninterface CompactStreamDependencies {\n compactedGzipStream: typeof compactedGzipStream;\n setObjectKeyAndDeleteChunks: (\n tx: Transaction,\n params: {streamId: string; objectKey: string},\n ) => Promise<{updated: boolean}>;\n}\n\nconst defaultDependencies: CompactStreamDependencies = {\n compactedGzipStream,\n setObjectKeyAndDeleteChunks,\n};\n\n/**\n * Compacts one closed stream into a single gzip NDJSON object, then deletes its chunk rows.\n *\n * load stream ──┬─ gone ─────────────► no-op (retention raced)\n * ├─ object_key set ───► no-op (idempotent re-run)\n * └─ else: upload to a per-attempt key ─► gzip ─► multipart (abort-aware, heartbeat)\n * │\n * verify streamed count/maxSeq/bytes == table (mismatch ─► delete upload, throw, retry)\n * │\n * tx: set object_key (state='closed' AND object_key IS NULL) + delete chunks\n * └─ 0 rows ─► delete this attempt's upload, then re-read the row:\n * gone ─► retention raced · keyed ─► superseded by another attempt\n *\n * Each attempt uploads to its own `compactedObjectKey(stream, uuid)`, so a slow or zombie\n * attempt can never overwrite a published object. The single-winner publish (object_key set\n * + chunk delete, atomic) drops chunks only once a complete object is durable; the integrity\n * check (count, maxSeq, and byte total) guards a read bug from publishing a truncated object\n * before the only copy of the source is gone (S3 part checksums cover byte transfer).\n */\nasync function compactStream(\n params: {streamId: string},\n dependencies: CompactStreamDependencies,\n): Promise<CompactStreamResult> {\n const stream = await getAttemptStreamById(params.streamId);\n if (!stream) return {outcome: 'gone'};\n if (stream.objectKey) return {outcome: 'already-compacted'};\n\n const ctx = Context.current();\n const uploadKey = compactedObjectKey(stream, crypto.randomUUID());\n const expected = await chunkStats(stream.id);\n const {body, stats} = dependencies.compactedGzipStream({\n streamId: stream.id,\n onPage: () => ctx.heartbeat(),\n });\n\n await putCompactedObject({\n key: uploadKey,\n body,\n signal: ctx.cancellationSignal,\n onProgress: () => ctx.heartbeat(),\n metadata: {\n stream_id: stream.id,\n chunk_count: String(expected.count),\n uncompressed_bytes: String(expected.uncompressedBytes),\n last_seq: String(expected.maxSeq),\n },\n });\n\n if (\n stats.chunkCount !== expected.count ||\n stats.lastSeq !== expected.maxSeq ||\n stats.uncompressedBytes !== expected.uncompressedBytes\n ) {\n await deleteObject(uploadKey).catch((error) => {\n logger().error(\n {err: error, streamId: stream.id, objectKey: uploadKey},\n 'Failed to delete invalid compacted log object',\n );\n reportError(error, {\n boundary: 'logs.cleanup',\n operation: 'delete-invalid-compaction-object',\n extra: {streamId: stream.id, objectKey: uploadKey},\n });\n });\n throw new Error(\n `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}`,\n );\n }\n\n const {updated} = await db().transaction((tx) =>\n dependencies.setObjectKeyAndDeleteChunks(tx, {\n streamId: stream.id,\n objectKey: uploadKey,\n }),\n );\n if (!updated) {\n await deleteObject(uploadKey);\n const current = await getAttemptStreamById(stream.id);\n return {outcome: current ? 'superseded' : 'retention-raced'};\n }\n\n return {\n outcome: 'compacted',\n objectKey: uploadKey,\n chunkCount: stats.chunkCount,\n uncompressedBytes: stats.uncompressedBytes,\n };\n}\n\nexport function createCompactStreamActivity(\n dependencies: CompactStreamDependencies = defaultDependencies,\n): (params: {streamId: string}) => Promise<CompactStreamResult> {\n return async (params) => {\n let outcome: CompactionMetricOutcome = 'failed';\n try {\n const result = await compactStream(params, dependencies);\n outcome = result.outcome;\n return result;\n } finally {\n compactionCount.add(1, {outcome});\n }\n };\n}\n\nexport const compactStreamActivity = createCompactStreamActivity();\n"],"names":["reportError","logger","Context","compactedObjectKey","deleteObject","putCompactedObject","compactedGzipStream","chunkStats","db","getAttemptStreamById","setObjectKeyAndDeleteChunks","compactionCount","defaultDependencies","compactStream","params","dependencies","stream","streamId","outcome","objectKey","ctx","current","uploadKey","crypto","randomUUID","expected","id","body","stats","onPage","heartbeat","key","signal","cancellationSignal","onProgress","metadata","stream_id","chunk_count","String","count","uncompressed_bytes","uncompressedBytes","last_seq","maxSeq","chunkCount","lastSeq","catch","error","err","boundary","operation","extra","Error","updated","transaction","tx","createCompactStreamActivity","result","add","compactStreamActivity"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction-reconcile.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/compaction-reconcile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compaction-reconcile.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/compaction-reconcile.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;GAUG;AACH,wBAAsB,2BAA2B,IAAI,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC,CA4BhG"}
|
|
@@ -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,12 @@ const RECONCILE_BATCH_LIMIT = 100;
|
|
|
41
42
|
err: error,
|
|
42
43
|
streamId: stream.id
|
|
43
44
|
}, 'Failed to re-drive stale stream compaction');
|
|
45
|
+
reportError(error, {
|
|
46
|
+
boundary: 'logs.maintenance',
|
|
47
|
+
extra: {
|
|
48
|
+
streamId: stream.id
|
|
49
|
+
}
|
|
50
|
+
});
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/temporal/activities/compaction-reconcile.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport {temporalClient} from '@shipfox/node-temporal';\nimport {config} from '#config.js';\nimport {listStaleUncompactedStreams} from '#db/streams.js';\nimport {LOGS_COMPACTION_TASK_QUEUE} from '#temporal/constants.js';\n\n// Bounded per tick; remaining stale streams are picked up on the next cron run.\nconst RECONCILE_BATCH_LIMIT = 100;\n\n/**\n * Backstop for the event-triggered path: finds closed streams that never got an object\n * key past the stale window and re-starts compaction for each. Starting by the same\n * `logs-compact:{streamId}` workflow id re-drives a stream whose bounded-retry run already\n * failed and closed, while skipping one whose run is still RUNNING (AlreadyStarted). Starts\n * independent workflows, not children, so a re-driven run is not tied to this cron tick.\n *\n * One stream's unexpected start failure is logged and skipped, never thrown: a single poison\n * stream must not abort the batch and leave the rest of the backlog un-re-driven (the next\n * tick retries it). The activity only fails on the initial DB query.\n */\nexport async function compactionReconcileActivity(): Promise<{restarted: number; failed: number}> {\n const stale = await listStaleUncompactedStreams({\n olderThanSeconds: config.LOG_COMPACTION_RECONCILE_STALE_SECONDS,\n limit: RECONCILE_BATCH_LIMIT,\n });\n\n let restarted = 0;\n let failed = 0;\n for (const stream of stale) {\n try {\n await temporalClient().workflow.start('compactStream', {\n taskQueue: LOGS_COMPACTION_TASK_QUEUE,\n workflowId: `logs-compact:${stream.id}`,\n args: [{streamId: stream.id}],\n });\n restarted += 1;\n } catch (error) {\n if (error instanceof Error && error.name === 'WorkflowExecutionAlreadyStartedError') continue;\n failed += 1;\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to re-drive stale stream compaction',\n );\n }\n }\n\n return {restarted, failed};\n}\n"],"names":["logger","temporalClient","config","listStaleUncompactedStreams","LOGS_COMPACTION_TASK_QUEUE","RECONCILE_BATCH_LIMIT","compactionReconcileActivity","stale","olderThanSeconds","LOG_COMPACTION_RECONCILE_STALE_SECONDS","limit","restarted","failed","stream","workflow","start","taskQueue","workflowId","id","args","streamId","error","Error","name","err"],"mappings":"AAAA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,SAAQC,cAAc,QAAO,yBAAyB;AACtD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,2BAA2B,QAAO,iBAAiB;AAC3D,SAAQC,0BAA0B,QAAO,yBAAyB;AAElE,gFAAgF;AAChF,MAAMC,wBAAwB;AAE9B;;;;;;;;;;CAUC,GACD,OAAO,eAAeC;IACpB,MAAMC,QAAQ,MAAMJ,4BAA4B;QAC9CK,kBAAkBN,OAAOO,sCAAsC;QAC/DC,OAAOL;IACT;IAEA,IAAIM,YAAY;IAChB,IAAIC,SAAS;IACb,KAAK,MAAMC,UAAUN,MAAO;QAC1B,IAAI;YACF,MAAMN,iBAAiBa,QAAQ,CAACC,KAAK,CAAC,iBAAiB;gBACrDC,WAAWZ;gBACXa,YAAY,CAAC,aAAa,EAAEJ,OAAOK,EAAE,EAAE;gBACvCC,MAAM;oBAAC;wBAACC,UAAUP,OAAOK,EAAE;oBAAA;iBAAE;YAC/B;YACAP,aAAa;QACf,EAAE,OAAOU,OAAO;YACd,IAAIA,iBAAiBC,SAASD,MAAME,IAAI,KAAK,wCAAwC;YACrFX,UAAU;YACVZ,SAASqB,KAAK,CACZ;gBAACG,KAAKH;gBAAOD,UAAUP,OAAOK,EAAE;YAAA,GAChC;
|
|
1
|
+
{"version":3,"sources":["../../../src/temporal/activities/compaction-reconcile.ts"],"sourcesContent":["import {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {temporalClient} from '@shipfox/node-temporal';\nimport {config} from '#config.js';\nimport {listStaleUncompactedStreams} from '#db/streams.js';\nimport {LOGS_COMPACTION_TASK_QUEUE} from '#temporal/constants.js';\n\n// Bounded per tick; remaining stale streams are picked up on the next cron run.\nconst RECONCILE_BATCH_LIMIT = 100;\n\n/**\n * Backstop for the event-triggered path: finds closed streams that never got an object\n * key past the stale window and re-starts compaction for each. Starting by the same\n * `logs-compact:{streamId}` workflow id re-drives a stream whose bounded-retry run already\n * failed and closed, while skipping one whose run is still RUNNING (AlreadyStarted). Starts\n * independent workflows, not children, so a re-driven run is not tied to this cron tick.\n *\n * One stream's unexpected start failure is logged and skipped, never thrown: a single poison\n * stream must not abort the batch and leave the rest of the backlog un-re-driven (the next\n * tick retries it). The activity only fails on the initial DB query.\n */\nexport async function compactionReconcileActivity(): Promise<{restarted: number; failed: number}> {\n const stale = await listStaleUncompactedStreams({\n olderThanSeconds: config.LOG_COMPACTION_RECONCILE_STALE_SECONDS,\n limit: RECONCILE_BATCH_LIMIT,\n });\n\n let restarted = 0;\n let failed = 0;\n for (const stream of stale) {\n try {\n await temporalClient().workflow.start('compactStream', {\n taskQueue: LOGS_COMPACTION_TASK_QUEUE,\n workflowId: `logs-compact:${stream.id}`,\n args: [{streamId: stream.id}],\n });\n restarted += 1;\n } catch (error) {\n if (error instanceof Error && error.name === 'WorkflowExecutionAlreadyStartedError') continue;\n failed += 1;\n logger().error(\n {err: error, streamId: stream.id},\n 'Failed to re-drive stale stream compaction',\n );\n reportError(error, {boundary: 'logs.maintenance', extra: {streamId: stream.id}});\n }\n }\n\n return {restarted, failed};\n}\n"],"names":["reportError","logger","temporalClient","config","listStaleUncompactedStreams","LOGS_COMPACTION_TASK_QUEUE","RECONCILE_BATCH_LIMIT","compactionReconcileActivity","stale","olderThanSeconds","LOG_COMPACTION_RECONCILE_STALE_SECONDS","limit","restarted","failed","stream","workflow","start","taskQueue","workflowId","id","args","streamId","error","Error","name","err","boundary","extra"],"mappings":"AAAA,SAAQA,WAAW,QAAO,iCAAiC;AAC3D,SAAQC,MAAM,QAAO,8BAA8B;AACnD,SAAQC,cAAc,QAAO,yBAAyB;AACtD,SAAQC,MAAM,QAAO,aAAa;AAClC,SAAQC,2BAA2B,QAAO,iBAAiB;AAC3D,SAAQC,0BAA0B,QAAO,yBAAyB;AAElE,gFAAgF;AAChF,MAAMC,wBAAwB;AAE9B;;;;;;;;;;CAUC,GACD,OAAO,eAAeC;IACpB,MAAMC,QAAQ,MAAMJ,4BAA4B;QAC9CK,kBAAkBN,OAAOO,sCAAsC;QAC/DC,OAAOL;IACT;IAEA,IAAIM,YAAY;IAChB,IAAIC,SAAS;IACb,KAAK,MAAMC,UAAUN,MAAO;QAC1B,IAAI;YACF,MAAMN,iBAAiBa,QAAQ,CAACC,KAAK,CAAC,iBAAiB;gBACrDC,WAAWZ;gBACXa,YAAY,CAAC,aAAa,EAAEJ,OAAOK,EAAE,EAAE;gBACvCC,MAAM;oBAAC;wBAACC,UAAUP,OAAOK,EAAE;oBAAA;iBAAE;YAC/B;YACAP,aAAa;QACf,EAAE,OAAOU,OAAO;YACd,IAAIA,iBAAiBC,SAASD,MAAME,IAAI,KAAK,wCAAwC;YACrFX,UAAU;YACVZ,SAASqB,KAAK,CACZ;gBAACG,KAAKH;gBAAOD,UAAUP,OAAOK,EAAE;YAAA,GAChC;YAEFnB,YAAYsB,OAAO;gBAACI,UAAU;gBAAoBC,OAAO;oBAACN,UAAUP,OAAOK,EAAE;gBAAA;YAAC;QAChF;IACF;IAEA,OAAO;QAACP;QAAWC;IAAM;AAC3B"}
|