@saptools/cf-live-trace 0.1.6 → 0.1.8
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 +26 -7
- package/dist/cli.js +1216 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +139 -7
- package/dist/index.js +1070 -67
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -519,29 +519,48 @@ function extractEvaluateValue(result) {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
// src/preview.ts
|
|
522
|
-
|
|
523
|
-
|
|
522
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
523
|
+
function truncatePreview(preview, maxBytes) {
|
|
524
|
+
if (maxBytes <= 0) {
|
|
524
525
|
return { preview, truncated: false };
|
|
525
526
|
}
|
|
526
|
-
|
|
527
|
+
const encoded = Buffer2.from(preview);
|
|
528
|
+
if (encoded.length <= maxBytes) {
|
|
527
529
|
return { preview, truncated: false };
|
|
528
530
|
}
|
|
529
|
-
return {
|
|
531
|
+
return {
|
|
532
|
+
preview: encoded.subarray(0, utf8Boundary(encoded, maxBytes)).toString("utf8"),
|
|
533
|
+
truncated: true
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function utf8Boundary(encoded, maxBytes) {
|
|
537
|
+
let boundary = Math.min(maxBytes, encoded.length);
|
|
538
|
+
while (boundary > 0 && isContinuationByte(encoded[boundary])) {
|
|
539
|
+
boundary -= 1;
|
|
540
|
+
}
|
|
541
|
+
return boundary;
|
|
542
|
+
}
|
|
543
|
+
function isContinuationByte(value) {
|
|
544
|
+
return value !== void 0 && (value & 192) === 128;
|
|
530
545
|
}
|
|
531
546
|
|
|
532
547
|
// src/payload.ts
|
|
533
548
|
var fallbackEventId = 0;
|
|
534
549
|
function parseDrainResult(payload, options) {
|
|
535
550
|
if (!isRecord2(payload)) {
|
|
536
|
-
return { events: [], droppedCount: 0, queueSize: 0 };
|
|
551
|
+
return { drainId: null, events: [], droppedCount: 0, queueSize: 0 };
|
|
537
552
|
}
|
|
538
553
|
const rawEvents = Array.isArray(payload["events"]) ? payload["events"] : [];
|
|
539
554
|
return {
|
|
555
|
+
drainId: readDrainId(payload["drainId"]),
|
|
540
556
|
events: rawEvents.map((event) => parseRuntimeEvent(event, options)).filter((event) => event !== null),
|
|
541
557
|
droppedCount: readNonNegativeNumber(payload["droppedCount"]),
|
|
542
558
|
queueSize: readNonNegativeNumber(payload["queueSize"])
|
|
543
559
|
};
|
|
544
560
|
}
|
|
561
|
+
function readDrainId(value) {
|
|
562
|
+
return typeof value === "string" && /^d\d+$/.test(value) ? value : null;
|
|
563
|
+
}
|
|
545
564
|
function parseRuntimeEvent(payload, options) {
|
|
546
565
|
if (!isRecord2(payload)) {
|
|
547
566
|
return null;
|
|
@@ -635,16 +654,20 @@ function isRecord2(value) {
|
|
|
635
654
|
|
|
636
655
|
// src/runtime-source.ts
|
|
637
656
|
var CF_LIVE_TRACE_GLOBAL_NAME = "__SAPTOOLS_CF_LIVE_TRACE__";
|
|
638
|
-
var CF_LIVE_TRACE_RUNTIME_VERSION =
|
|
657
|
+
var CF_LIVE_TRACE_RUNTIME_VERSION = 3;
|
|
639
658
|
var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
640
659
|
(() => {
|
|
641
660
|
const name = '${CF_LIVE_TRACE_GLOBAL_NAME}';
|
|
642
661
|
const runtimeVersion = ${String(CF_LIVE_TRACE_RUNTIME_VERSION)};
|
|
643
662
|
const existing = globalThis[name];
|
|
644
663
|
if (existing && typeof existing.version === 'number' && existing.version >= runtimeVersion) return existing;
|
|
664
|
+
let staleCleanup = null;
|
|
645
665
|
if (existing && typeof existing.uninstall === 'function') {
|
|
646
666
|
try {
|
|
647
|
-
existing.uninstall();
|
|
667
|
+
const cleanup = existing.uninstall();
|
|
668
|
+
if (cleanup && typeof cleanup.then === 'function') {
|
|
669
|
+
staleCleanup = Promise.resolve(cleanup).catch(() => undefined);
|
|
670
|
+
}
|
|
648
671
|
} catch {}
|
|
649
672
|
}
|
|
650
673
|
let BufferCtor = globalThis.Buffer;
|
|
@@ -657,7 +680,9 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
657
680
|
droppedCount: 0,
|
|
658
681
|
originals: {},
|
|
659
682
|
seen: new WeakSet(),
|
|
660
|
-
nextId: 1
|
|
683
|
+
nextId: 1,
|
|
684
|
+
nextDrainId: 1,
|
|
685
|
+
pendingDrain: null
|
|
661
686
|
};
|
|
662
687
|
const loadRequire = () => {
|
|
663
688
|
if (typeof require === 'function') return require;
|
|
@@ -690,13 +715,62 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
690
715
|
if (chunk instanceof Uint8Array && BufferCtor) return BufferCtor.from(chunk).toString('utf8');
|
|
691
716
|
return '';
|
|
692
717
|
};
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
if (
|
|
718
|
+
const chunkBuffer = (chunk) => {
|
|
719
|
+
if (!BufferCtor || chunk === undefined || chunk === null) return null;
|
|
720
|
+
if (BufferCtor.isBuffer(chunk)) return chunk;
|
|
721
|
+
if (typeof chunk === 'string' || chunk instanceof Uint8Array) return BufferCtor.from(chunk);
|
|
722
|
+
return null;
|
|
723
|
+
};
|
|
724
|
+
const textByteLength = (text) => BufferCtor ? BufferCtor.byteLength(text) : text.length;
|
|
725
|
+
const chunkByteLength = (chunk) => {
|
|
726
|
+
if (chunk === undefined || chunk === null) return 0;
|
|
727
|
+
if (BufferCtor && BufferCtor.isBuffer(chunk)) return chunk.length;
|
|
728
|
+
if (chunk instanceof Uint8Array) return chunk.byteLength;
|
|
729
|
+
return textByteLength(typeof chunk === 'string' ? chunk : '');
|
|
730
|
+
};
|
|
731
|
+
const truncateTextToBytes = (text, maxBytes) => {
|
|
732
|
+
if (!BufferCtor) return text.slice(0, maxBytes);
|
|
733
|
+
const encoded = BufferCtor.from(text);
|
|
734
|
+
if (encoded.length <= maxBytes) return text;
|
|
735
|
+
let boundary = Math.min(maxBytes, encoded.length);
|
|
736
|
+
while (boundary > 0 && encoded[boundary] !== undefined && (encoded[boundary] & 0xc0) === 0x80) {
|
|
737
|
+
boundary -= 1;
|
|
738
|
+
}
|
|
739
|
+
return encoded.subarray(0, boundary).toString('utf8');
|
|
740
|
+
};
|
|
741
|
+
const appendPreview = (chunks, currentBytes, chunk, enabled) => {
|
|
742
|
+
if (!enabled) return currentBytes;
|
|
743
|
+
const maxBytes = Math.floor(Number(state.options.maxBodyBytes) || 0);
|
|
744
|
+
if (maxBytes <= 0) return currentBytes;
|
|
745
|
+
const remaining = maxBytes - currentBytes;
|
|
746
|
+
if (remaining <= 0) return currentBytes;
|
|
747
|
+
const buffer = chunkBuffer(chunk);
|
|
748
|
+
if (buffer) {
|
|
749
|
+
const addition = buffer.subarray(0, remaining);
|
|
750
|
+
chunks.push(addition);
|
|
751
|
+
return currentBytes + addition.length;
|
|
752
|
+
}
|
|
696
753
|
const text = chunkText(chunk);
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
return
|
|
754
|
+
const addition = truncateTextToBytes(text, remaining);
|
|
755
|
+
chunks.push(addition);
|
|
756
|
+
return currentBytes + textByteLength(addition);
|
|
757
|
+
};
|
|
758
|
+
const completeUtf8Length = (buffer) => {
|
|
759
|
+
if (!buffer || buffer.length === 0) return 0;
|
|
760
|
+
let start = buffer.length - 1;
|
|
761
|
+
while (start > 0 && (buffer[start] & 0xc0) === 0x80) start -= 1;
|
|
762
|
+
const lead = buffer[start];
|
|
763
|
+
const expected = lead <= 0x7f ? 1 : lead <= 0xdf ? 2 : lead <= 0xef ? 3 : lead <= 0xf7 ? 4 : 1;
|
|
764
|
+
return buffer.length - start < expected ? start : buffer.length;
|
|
765
|
+
};
|
|
766
|
+
const previewText = (chunks, retainedBytes) => {
|
|
767
|
+
if (!BufferCtor) {
|
|
768
|
+
const text = chunks.join('');
|
|
769
|
+
return { text, bytes: textByteLength(text) };
|
|
770
|
+
}
|
|
771
|
+
const buffer = BufferCtor.concat(chunks, retainedBytes);
|
|
772
|
+
const completeBytes = completeUtf8Length(buffer);
|
|
773
|
+
return { text: buffer.subarray(0, completeBytes).toString('utf8'), bytes: completeBytes };
|
|
700
774
|
};
|
|
701
775
|
const enqueue = (event) => {
|
|
702
776
|
if (state.queue.length >= state.options.maxEvents) {
|
|
@@ -713,31 +787,30 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
713
787
|
const traceId = String(state.nextId++);
|
|
714
788
|
let requestBytes = 0;
|
|
715
789
|
let responseBytes = 0;
|
|
716
|
-
|
|
717
|
-
|
|
790
|
+
const requestPreviewChunks = [];
|
|
791
|
+
const responsePreviewChunks = [];
|
|
792
|
+
let requestPreviewBytes = 0;
|
|
793
|
+
let responsePreviewBytes = 0;
|
|
718
794
|
let finished = false;
|
|
719
795
|
const originalReqEmit = req.emit;
|
|
720
796
|
const originalWrite = res.write;
|
|
721
797
|
const originalEnd = res.end;
|
|
722
798
|
req.emit = function patchedReqEmit(eventName, ...args) {
|
|
723
799
|
if (eventName === 'data' && args[0] !== undefined) {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
requestPreview = appendPreview(requestPreview, args[0], state.options.captureRequestBody);
|
|
800
|
+
requestBytes += chunkByteLength(args[0]);
|
|
801
|
+
requestPreviewBytes = appendPreview(requestPreviewChunks, requestPreviewBytes, args[0], state.options.captureRequestBody);
|
|
727
802
|
}
|
|
728
803
|
return originalReqEmit.apply(this, [eventName, ...args]);
|
|
729
804
|
};
|
|
730
805
|
res.write = function patchedWrite(chunk, ...args) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
responsePreview = appendPreview(responsePreview, chunk, state.options.captureResponseBody);
|
|
806
|
+
responseBytes += chunkByteLength(chunk);
|
|
807
|
+
responsePreviewBytes = appendPreview(responsePreviewChunks, responsePreviewBytes, chunk, state.options.captureResponseBody);
|
|
734
808
|
return originalWrite.apply(this, [chunk, ...args]);
|
|
735
809
|
};
|
|
736
810
|
res.end = function patchedEnd(chunk, ...args) {
|
|
737
811
|
if (chunk !== undefined) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
responsePreview = appendPreview(responsePreview, chunk, state.options.captureResponseBody);
|
|
812
|
+
responseBytes += chunkByteLength(chunk);
|
|
813
|
+
responsePreviewBytes = appendPreview(responsePreviewChunks, responsePreviewBytes, chunk, state.options.captureResponseBody);
|
|
741
814
|
}
|
|
742
815
|
return originalEnd.apply(this, [chunk, ...args]);
|
|
743
816
|
};
|
|
@@ -748,6 +821,8 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
748
821
|
res.write = originalWrite;
|
|
749
822
|
res.end = originalEnd;
|
|
750
823
|
const rawUrl = initialUrl || String(req.url || '');
|
|
824
|
+
const requestPreview = previewText(requestPreviewChunks, requestPreviewBytes);
|
|
825
|
+
const responsePreview = previewText(responsePreviewChunks, responsePreviewBytes);
|
|
751
826
|
enqueue({
|
|
752
827
|
id: traceId,
|
|
753
828
|
timestamp: new Date().toISOString(),
|
|
@@ -762,10 +837,10 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
762
837
|
responseBytes,
|
|
763
838
|
requestHeaders: toHeaderRecord(req.headers),
|
|
764
839
|
responseHeaders: toHeaderRecord(typeof res.getHeaders === 'function' ? res.getHeaders() : {}),
|
|
765
|
-
requestBodyPreview: requestPreview,
|
|
766
|
-
responseBodyPreview: responsePreview,
|
|
767
|
-
requestBodyTruncated: state.options.maxBodyBytes > 0 &&
|
|
768
|
-
responseBodyTruncated: state.options.maxBodyBytes > 0 &&
|
|
840
|
+
requestBodyPreview: requestPreview.text,
|
|
841
|
+
responseBodyPreview: responsePreview.text,
|
|
842
|
+
requestBodyTruncated: state.options.captureRequestBody && state.options.maxBodyBytes > 0 && requestBytes > requestPreview.bytes,
|
|
843
|
+
responseBodyTruncated: state.options.captureResponseBody && state.options.maxBodyBytes > 0 && responseBytes > responsePreview.bytes,
|
|
769
844
|
droppedBeforeEvent: state.droppedCount,
|
|
770
845
|
traceId,
|
|
771
846
|
correlationId: req.headers && typeof req.headers['x-saptools-trace-id'] === 'string' ? req.headers['x-saptools-trace-id'] : null
|
|
@@ -789,10 +864,10 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
789
864
|
const numeric = Number(value);
|
|
790
865
|
return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
|
|
791
866
|
};
|
|
792
|
-
const limitPreview = (event, previewKey, truncatedKey,
|
|
867
|
+
const limitPreview = (event, previewKey, truncatedKey, maxBytes) => {
|
|
793
868
|
const preview = event[previewKey];
|
|
794
|
-
if (
|
|
795
|
-
return { ...event, [previewKey]: preview
|
|
869
|
+
if (maxBytes <= 0 || typeof preview !== 'string' || textByteLength(preview) <= maxBytes) return event;
|
|
870
|
+
return { ...event, [previewKey]: truncateTextToBytes(preview, maxBytes), [truncatedKey]: true };
|
|
796
871
|
};
|
|
797
872
|
const eventForDrain = (event, maxChars) => {
|
|
798
873
|
if (!event || typeof event !== 'object') return event;
|
|
@@ -801,10 +876,44 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
801
876
|
output = limitPreview(output, 'responseBodyPreview', 'responseBodyTruncated', maxChars);
|
|
802
877
|
return output;
|
|
803
878
|
};
|
|
879
|
+
const acknowledgeDrain = (drainId) => {
|
|
880
|
+
const pending = state.pendingDrain;
|
|
881
|
+
if (!pending || drainId !== pending.id) return;
|
|
882
|
+
const drainedEvents = new Set(pending.sourceEvents);
|
|
883
|
+
state.queue = state.queue.filter((event) => !drainedEvents.has(event));
|
|
884
|
+
state.pendingDrain = null;
|
|
885
|
+
};
|
|
886
|
+
const drainResult = () => {
|
|
887
|
+
const pending = state.pendingDrain;
|
|
888
|
+
if (!pending) {
|
|
889
|
+
return { drainId: null, events: [], droppedCount: state.droppedCount, queueSize: state.queue.length };
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
drainId: pending.id,
|
|
893
|
+
events: pending.events,
|
|
894
|
+
droppedCount: state.droppedCount,
|
|
895
|
+
queueSize: state.queue.length
|
|
896
|
+
};
|
|
897
|
+
};
|
|
804
898
|
const api = {
|
|
805
899
|
version: runtimeVersion,
|
|
806
900
|
async install(options) {
|
|
807
|
-
|
|
901
|
+
if (staleCleanup) {
|
|
902
|
+
await staleCleanup;
|
|
903
|
+
staleCleanup = null;
|
|
904
|
+
}
|
|
905
|
+
state.options = {
|
|
906
|
+
...state.options,
|
|
907
|
+
...options,
|
|
908
|
+
maxBodyBytes: toTransportLimit(options && options.maxBodyBytes),
|
|
909
|
+
maxEvents: Math.max(1, Math.floor(Number(options && options.maxEvents) || 1))
|
|
910
|
+
};
|
|
911
|
+
state.queue = [];
|
|
912
|
+
state.pendingDrain = null;
|
|
913
|
+
state.droppedCount = 0;
|
|
914
|
+
state.seen = new WeakSet();
|
|
915
|
+
state.nextId = 1;
|
|
916
|
+
state.nextDrainId = 1;
|
|
808
917
|
if (!state.installed) {
|
|
809
918
|
if (!BufferCtor) {
|
|
810
919
|
const bufferModule = await loadModule('buffer');
|
|
@@ -823,11 +932,19 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
823
932
|
state.enabled = false;
|
|
824
933
|
return api.status();
|
|
825
934
|
},
|
|
826
|
-
drainEvents(maxCount, maxTransportBodyBytes) {
|
|
935
|
+
drainEvents(maxCount, maxTransportBodyBytes, acknowledgedDrainId) {
|
|
936
|
+
acknowledgeDrain(acknowledgedDrainId);
|
|
937
|
+
if (state.pendingDrain) return drainResult();
|
|
827
938
|
const count = Math.max(0, Math.min(Number(maxCount) || 0, state.queue.length));
|
|
828
939
|
const transportLimit = toTransportLimit(maxTransportBodyBytes);
|
|
829
|
-
|
|
830
|
-
|
|
940
|
+
if (count === 0) return drainResult();
|
|
941
|
+
const sourceEvents = state.queue.slice(0, count);
|
|
942
|
+
state.pendingDrain = {
|
|
943
|
+
id: 'd' + String(state.nextDrainId++),
|
|
944
|
+
sourceEvents,
|
|
945
|
+
events: sourceEvents.map((event) => eventForDrain(event, transportLimit))
|
|
946
|
+
};
|
|
947
|
+
return drainResult();
|
|
831
948
|
},
|
|
832
949
|
status() {
|
|
833
950
|
return { installed: state.installed, enabled: state.enabled, queueSize: state.queue.length, droppedCount: state.droppedCount, maxEvents: state.options.maxEvents };
|
|
@@ -839,6 +956,8 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
839
956
|
if (state.originals.httpServerEmit && http && http.Server) http.Server.prototype.emit = state.originals.httpServerEmit;
|
|
840
957
|
if (state.originals.httpsServerEmit && https && https.Server) https.Server.prototype.emit = state.originals.httpsServerEmit;
|
|
841
958
|
state.installed = false;
|
|
959
|
+
state.queue = [];
|
|
960
|
+
state.pendingDrain = null;
|
|
842
961
|
return api.status();
|
|
843
962
|
}
|
|
844
963
|
};
|
|
@@ -849,8 +968,9 @@ var CF_LIVE_TRACE_RUNTIME_SOURCE = `
|
|
|
849
968
|
function buildInstallExpression(options) {
|
|
850
969
|
return `${CF_LIVE_TRACE_RUNTIME_SOURCE}.install(${JSON.stringify(options)})`;
|
|
851
970
|
}
|
|
852
|
-
function buildDrainExpression(maxCount, maxTransportBodyBytes) {
|
|
853
|
-
|
|
971
|
+
function buildDrainExpression(maxCount, maxTransportBodyBytes, acknowledgedDrainId = null) {
|
|
972
|
+
const acknowledgement = JSON.stringify(acknowledgedDrainId);
|
|
973
|
+
return `globalThis.${CF_LIVE_TRACE_GLOBAL_NAME}?.drainEvents(${String(maxCount)}, ${String(maxTransportBodyBytes)}, ${acknowledgement}) ?? { drainId: null, events: [], droppedCount: 0, queueSize: 0 }`;
|
|
854
974
|
}
|
|
855
975
|
function buildStopExpression(options) {
|
|
856
976
|
return options.uninstallRuntimeHook ? `globalThis.${CF_LIVE_TRACE_GLOBAL_NAME}?.uninstall() ?? { installed: false, enabled: false }` : `globalThis.${CF_LIVE_TRACE_GLOBAL_NAME}?.disable() ?? { installed: false, enabled: false }`;
|
|
@@ -942,7 +1062,8 @@ var RUNTIME_QUEUE_SIZE = 1e3;
|
|
|
942
1062
|
var CONTROL_EVALUATE_TIMEOUT_MS = 5e3;
|
|
943
1063
|
var DRAIN_EVALUATE_TIMEOUT_MS = 1e4;
|
|
944
1064
|
var DRAIN_TIMEOUT_RETRY_LIMIT = 3;
|
|
945
|
-
var
|
|
1065
|
+
var MAX_DRAIN_EVALUATE_TIMEOUT_MS = 6e4;
|
|
1066
|
+
var DRAIN_BODY_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
946
1067
|
var defaultDependencies = {
|
|
947
1068
|
prepareCfSession,
|
|
948
1069
|
ensureSshEnabled,
|
|
@@ -959,11 +1080,12 @@ var LiveTraceSession = class {
|
|
|
959
1080
|
}
|
|
960
1081
|
options;
|
|
961
1082
|
dependencies;
|
|
962
|
-
|
|
1083
|
+
summaryEvents = [];
|
|
963
1084
|
consecutiveDrainTimeouts = 0;
|
|
964
|
-
|
|
1085
|
+
drainTask;
|
|
965
1086
|
inspectorClient;
|
|
966
1087
|
pollTimer;
|
|
1088
|
+
pendingDrainAcknowledgement = null;
|
|
967
1089
|
state = "idle";
|
|
968
1090
|
stopRequested = false;
|
|
969
1091
|
tunnelHandle;
|
|
@@ -972,6 +1094,8 @@ var LiveTraceSession = class {
|
|
|
972
1094
|
return;
|
|
973
1095
|
}
|
|
974
1096
|
this.stopRequested = false;
|
|
1097
|
+
this.pendingDrainAcknowledgement = null;
|
|
1098
|
+
this.summaryEvents.splice(0);
|
|
975
1099
|
await this.startRuntimeTrace(resolveStartOptions(options));
|
|
976
1100
|
}
|
|
977
1101
|
async stop(options) {
|
|
@@ -1057,36 +1181,65 @@ var LiveTraceSession = class {
|
|
|
1057
1181
|
this.stopPolling();
|
|
1058
1182
|
this.consecutiveDrainTimeouts = 0;
|
|
1059
1183
|
this.pollTimer = this.dependencies.setInterval(() => {
|
|
1060
|
-
|
|
1184
|
+
this.startDrain(maxBodyBytes);
|
|
1061
1185
|
}, DRAIN_INTERVAL_MS);
|
|
1062
1186
|
}
|
|
1063
|
-
|
|
1064
|
-
if (this.
|
|
1187
|
+
startDrain(maxBodyBytes) {
|
|
1188
|
+
if (this.drainTask !== void 0 || this.inspectorClient === void 0 || this.state !== "streaming") {
|
|
1065
1189
|
return;
|
|
1066
1190
|
}
|
|
1067
|
-
|
|
1191
|
+
const task = this.drainTraceEvents(maxBodyBytes);
|
|
1192
|
+
this.drainTask = task;
|
|
1193
|
+
void task.then(
|
|
1194
|
+
() => {
|
|
1195
|
+
this.clearDrainTask(task);
|
|
1196
|
+
},
|
|
1197
|
+
() => {
|
|
1198
|
+
this.clearDrainTask(task);
|
|
1199
|
+
}
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
clearDrainTask(task) {
|
|
1203
|
+
if (this.drainTask === task) {
|
|
1204
|
+
this.drainTask = void 0;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async drainTraceEvents(maxBodyBytes) {
|
|
1208
|
+
const batchSize = resolveDrainBatchSize(maxBodyBytes);
|
|
1068
1209
|
try {
|
|
1069
|
-
const payload = await this.
|
|
1070
|
-
buildDrainExpression(
|
|
1071
|
-
|
|
1210
|
+
const payload = await this.requireInspector().evaluate(
|
|
1211
|
+
buildDrainExpression(
|
|
1212
|
+
batchSize,
|
|
1213
|
+
resolveDrainTransportBodyLimit(maxBodyBytes),
|
|
1214
|
+
this.pendingDrainAcknowledgement
|
|
1215
|
+
),
|
|
1216
|
+
resolveDrainEvaluateTimeout(maxBodyBytes, batchSize)
|
|
1072
1217
|
);
|
|
1073
1218
|
this.consecutiveDrainTimeouts = 0;
|
|
1074
|
-
|
|
1219
|
+
try {
|
|
1220
|
+
await this.publishDrainedEvents(payload, maxBodyBytes);
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
await this.handleEventHandlerFailure(error);
|
|
1223
|
+
}
|
|
1075
1224
|
} catch (error) {
|
|
1076
1225
|
await this.handleDrainFailure(error);
|
|
1077
|
-
} finally {
|
|
1078
|
-
this.drainInFlight = false;
|
|
1079
1226
|
}
|
|
1080
1227
|
}
|
|
1081
|
-
publishDrainedEvents(payload, maxBodyBytes) {
|
|
1228
|
+
async publishDrainedEvents(payload, maxBodyBytes) {
|
|
1082
1229
|
const drained = parseDrainResult(payload, { appId: this.options.target.app, maxBodyBytes });
|
|
1083
|
-
if (drained.events.length
|
|
1230
|
+
if (drained.events.length > 0) {
|
|
1231
|
+
await this.options.onEvents?.(drained.events);
|
|
1232
|
+
this.publishSummary(drained.events);
|
|
1233
|
+
}
|
|
1234
|
+
this.pendingDrainAcknowledgement = drained.drainId;
|
|
1235
|
+
}
|
|
1236
|
+
publishSummary(events) {
|
|
1237
|
+
if (this.options.onSummary === void 0) {
|
|
1084
1238
|
return;
|
|
1085
1239
|
}
|
|
1086
|
-
this.
|
|
1087
|
-
this.
|
|
1088
|
-
this.options.
|
|
1089
|
-
this.options.onSummary?.(buildUrlSummaries(this.events));
|
|
1240
|
+
this.summaryEvents.push(...events.map(toSummaryEvent));
|
|
1241
|
+
this.summaryEvents.splice(0, Math.max(0, this.summaryEvents.length - RUNTIME_QUEUE_SIZE));
|
|
1242
|
+
this.options.onSummary(buildUrlSummaries(this.summaryEvents));
|
|
1090
1243
|
}
|
|
1091
1244
|
async handleDrainFailure(error) {
|
|
1092
1245
|
if (isEvaluateTimeout(error)) {
|
|
@@ -1097,11 +1250,19 @@ var LiveTraceSession = class {
|
|
|
1097
1250
|
}
|
|
1098
1251
|
}
|
|
1099
1252
|
this.log(`Live Trace stream failed for ${this.options.target.app}: ${formatError(error)}`);
|
|
1100
|
-
await this.stopRuntimeTrace(false);
|
|
1253
|
+
await this.stopRuntimeTrace(false, false);
|
|
1101
1254
|
this.postState("error", "Runtime HTTP trace connection was lost.", false, true);
|
|
1102
1255
|
}
|
|
1103
|
-
async
|
|
1256
|
+
async handleEventHandlerFailure(error) {
|
|
1257
|
+
this.log(`Live Trace event handler failed for ${this.options.target.app}: ${formatError(error)}`);
|
|
1258
|
+
await this.stopRuntimeTrace(false, false);
|
|
1259
|
+
this.postState("error", "Trace event handler failed.", false, true);
|
|
1260
|
+
}
|
|
1261
|
+
async stopRuntimeTrace(uninstallRuntimeHook, waitForDrain = true) {
|
|
1104
1262
|
this.stopPolling();
|
|
1263
|
+
if (waitForDrain) {
|
|
1264
|
+
await this.drainTask;
|
|
1265
|
+
}
|
|
1105
1266
|
this.consecutiveDrainTimeouts = 0;
|
|
1106
1267
|
const uninstalled = await this.stopInspectorHook(uninstallRuntimeHook);
|
|
1107
1268
|
await this.closeInspectorClient();
|
|
@@ -1185,12 +1346,20 @@ var LiveTraceStartupError = class extends Error {
|
|
|
1185
1346
|
stateMessage;
|
|
1186
1347
|
};
|
|
1187
1348
|
function resolveStartOptions(options) {
|
|
1349
|
+
const maxBodyBytes = options.maxBodyBytes ?? 4096;
|
|
1350
|
+
if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes <= 0) {
|
|
1351
|
+
throw new Error("maxBodyBytes must be a positive safe integer.");
|
|
1352
|
+
}
|
|
1353
|
+
const runtimeQueueSize = options.runtimeQueueSize ?? RUNTIME_QUEUE_SIZE;
|
|
1354
|
+
if (!Number.isSafeInteger(runtimeQueueSize) || runtimeQueueSize <= 0) {
|
|
1355
|
+
throw new Error("runtimeQueueSize must be a positive safe integer.");
|
|
1356
|
+
}
|
|
1188
1357
|
return {
|
|
1189
1358
|
captureHeaders: options.captureHeaders ?? true,
|
|
1190
1359
|
captureRequestBody: options.captureRequestBody ?? true,
|
|
1191
1360
|
captureResponseBody: options.captureResponseBody ?? true,
|
|
1192
|
-
maxBodyBytes
|
|
1193
|
-
runtimeQueueSize
|
|
1361
|
+
maxBodyBytes,
|
|
1362
|
+
runtimeQueueSize
|
|
1194
1363
|
};
|
|
1195
1364
|
}
|
|
1196
1365
|
function stopLateTunnel(tunnel) {
|
|
@@ -1199,7 +1368,16 @@ function stopLateTunnel(tunnel) {
|
|
|
1199
1368
|
}
|
|
1200
1369
|
}
|
|
1201
1370
|
function resolveDrainTransportBodyLimit(maxBodyBytes) {
|
|
1202
|
-
return maxBodyBytes > 0 ?
|
|
1371
|
+
return maxBodyBytes > 0 ? maxBodyBytes : 1;
|
|
1372
|
+
}
|
|
1373
|
+
function resolveDrainBatchSize(maxBodyBytes) {
|
|
1374
|
+
const perEventBytes = Math.max(1, maxBodyBytes * 2);
|
|
1375
|
+
return Math.max(1, Math.min(DRAIN_BATCH_SIZE, Math.floor(DRAIN_BODY_BUDGET_BYTES / perEventBytes)));
|
|
1376
|
+
}
|
|
1377
|
+
function resolveDrainEvaluateTimeout(maxBodyBytes, batchSize) {
|
|
1378
|
+
const estimatedBytes = Math.min(Number.MAX_SAFE_INTEGER, maxBodyBytes * 2 * batchSize);
|
|
1379
|
+
const extraMegabytes = Math.ceil(Math.max(0, estimatedBytes - 1e6) / 1e6);
|
|
1380
|
+
return Math.min(MAX_DRAIN_EVALUATE_TIMEOUT_MS, DRAIN_EVALUATE_TIMEOUT_MS + extraMegabytes * 5e3);
|
|
1203
1381
|
}
|
|
1204
1382
|
function isEvaluateTimeout(error) {
|
|
1205
1383
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1222,8 +1400,569 @@ function formatError(error) {
|
|
|
1222
1400
|
const message = error instanceof Error ? error.message : String(error);
|
|
1223
1401
|
return message.trim().length > 0 ? message.trim() : "Unknown error";
|
|
1224
1402
|
}
|
|
1403
|
+
function toSummaryEvent(event) {
|
|
1404
|
+
return {
|
|
1405
|
+
...event,
|
|
1406
|
+
requestHeaders: {},
|
|
1407
|
+
responseHeaders: {},
|
|
1408
|
+
requestBodyPreview: "",
|
|
1409
|
+
responseBodyPreview: ""
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/trace-compact.ts
|
|
1414
|
+
var COMPACT_BODY_PREVIEW_CHARS = 128;
|
|
1415
|
+
var SENSITIVE_QUERY_KEYS = new Set([
|
|
1416
|
+
"access_token",
|
|
1417
|
+
"api_key",
|
|
1418
|
+
"auth",
|
|
1419
|
+
"authorization",
|
|
1420
|
+
"client_secret",
|
|
1421
|
+
"oauth_code",
|
|
1422
|
+
"passwd",
|
|
1423
|
+
"password",
|
|
1424
|
+
"refresh_token",
|
|
1425
|
+
"sig",
|
|
1426
|
+
"signature",
|
|
1427
|
+
"token"
|
|
1428
|
+
].map(normalizeQueryKey));
|
|
1429
|
+
function detectBodyFormat(body, headers) {
|
|
1430
|
+
if (body.length === 0) {
|
|
1431
|
+
return "empty";
|
|
1432
|
+
}
|
|
1433
|
+
const contentType = headerValue(headers, "content-type").toLowerCase();
|
|
1434
|
+
if (contentType.includes("json")) {
|
|
1435
|
+
return "json";
|
|
1436
|
+
}
|
|
1437
|
+
if (contentType.includes("html")) {
|
|
1438
|
+
return "html";
|
|
1439
|
+
}
|
|
1440
|
+
if (contentType.includes("xml")) {
|
|
1441
|
+
return "xml";
|
|
1442
|
+
}
|
|
1443
|
+
if (contentType.includes("x-www-form-urlencoded")) {
|
|
1444
|
+
return "form";
|
|
1445
|
+
}
|
|
1446
|
+
if (contentType.includes("octet-stream")) {
|
|
1447
|
+
return "binary";
|
|
1448
|
+
}
|
|
1449
|
+
return detectBodyFormatFromText(body, contentType);
|
|
1450
|
+
}
|
|
1451
|
+
function compactTraceEvent(record) {
|
|
1452
|
+
const event = record.event;
|
|
1453
|
+
const requestBody = compactBody(event.requestBodyPreview);
|
|
1454
|
+
const responseBody = compactBody(event.responseBodyPreview);
|
|
1455
|
+
return {
|
|
1456
|
+
id: event.id,
|
|
1457
|
+
sessionId: record.sessionId,
|
|
1458
|
+
requestId: record.requestId,
|
|
1459
|
+
timestamp: event.timestamp,
|
|
1460
|
+
instance: event.instance,
|
|
1461
|
+
method: event.method,
|
|
1462
|
+
path: event.path,
|
|
1463
|
+
url: redactSensitiveQueryValues(event.url),
|
|
1464
|
+
normalizedUrl: redactSensitiveQueryValues(event.normalizedUrl),
|
|
1465
|
+
status: event.status,
|
|
1466
|
+
durationMs: event.durationMs,
|
|
1467
|
+
requestBytes: event.requestBytes,
|
|
1468
|
+
responseBytes: event.responseBytes,
|
|
1469
|
+
requestBodyFormat: record.requestBodyFormat,
|
|
1470
|
+
responseBodyFormat: record.responseBodyFormat,
|
|
1471
|
+
requestBodyPreview: requestBody.preview,
|
|
1472
|
+
requestBodyPreviewRemainingChars: requestBody.remainingChars,
|
|
1473
|
+
responseBodyPreview: responseBody.preview,
|
|
1474
|
+
responseBodyPreviewRemainingChars: responseBody.remainingChars,
|
|
1475
|
+
requestBodyTruncated: event.requestBodyTruncated,
|
|
1476
|
+
responseBodyTruncated: event.responseBodyTruncated,
|
|
1477
|
+
droppedBeforeEvent: event.droppedBeforeEvent,
|
|
1478
|
+
source: event.source,
|
|
1479
|
+
traceId: event.traceId,
|
|
1480
|
+
correlationId: event.correlationId
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function headerValue(headers, name) {
|
|
1484
|
+
const lowerName = name.toLowerCase();
|
|
1485
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1486
|
+
if (key.toLowerCase() === lowerName) {
|
|
1487
|
+
return value;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return "";
|
|
1491
|
+
}
|
|
1492
|
+
function detectBodyFormatFromText(body, contentType) {
|
|
1493
|
+
const trimmed = body.trim();
|
|
1494
|
+
if (isJsonText(trimmed)) {
|
|
1495
|
+
return "json";
|
|
1496
|
+
}
|
|
1497
|
+
if (/^<!doctype\s+html/i.test(trimmed) || /^<html(?:\s|>)/i.test(trimmed)) {
|
|
1498
|
+
return "html";
|
|
1499
|
+
}
|
|
1500
|
+
if (/^<\?xml(?:\s|>)/i.test(trimmed) || /^<[A-Za-z_][\w:.-]*(?:\s|>|\/>)/.test(trimmed)) {
|
|
1501
|
+
return "xml";
|
|
1502
|
+
}
|
|
1503
|
+
if (contentType.startsWith("text/")) {
|
|
1504
|
+
return "text";
|
|
1505
|
+
}
|
|
1506
|
+
if (hasBinaryControlChars(body)) {
|
|
1507
|
+
return "binary";
|
|
1508
|
+
}
|
|
1509
|
+
return "text";
|
|
1510
|
+
}
|
|
1511
|
+
function isJsonText(text) {
|
|
1512
|
+
if (!text.startsWith("{") && !text.startsWith("[")) {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
try {
|
|
1516
|
+
JSON.parse(text);
|
|
1517
|
+
return true;
|
|
1518
|
+
} catch {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function hasBinaryControlChars(text) {
|
|
1523
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
1524
|
+
const code = text.charCodeAt(index);
|
|
1525
|
+
if (code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31) {
|
|
1526
|
+
return true;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
function compactBody(body) {
|
|
1532
|
+
let preview = "";
|
|
1533
|
+
let length = 0;
|
|
1534
|
+
for (const character of body) {
|
|
1535
|
+
if (length < COMPACT_BODY_PREVIEW_CHARS) {
|
|
1536
|
+
preview += character;
|
|
1537
|
+
}
|
|
1538
|
+
length += 1;
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
preview,
|
|
1542
|
+
remainingChars: Math.max(0, length - COMPACT_BODY_PREVIEW_CHARS)
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function redactSensitiveQueryValues(rawUrl) {
|
|
1546
|
+
const queryIndex = rawUrl.indexOf("?");
|
|
1547
|
+
if (queryIndex < 0) {
|
|
1548
|
+
return rawUrl;
|
|
1549
|
+
}
|
|
1550
|
+
const fragmentIndex = rawUrl.indexOf("#", queryIndex);
|
|
1551
|
+
const queryEnd = fragmentIndex < 0 ? rawUrl.length : fragmentIndex;
|
|
1552
|
+
const query = rawUrl.slice(queryIndex + 1, queryEnd);
|
|
1553
|
+
const redacted = query.split("&").map(redactQueryPart).join("&");
|
|
1554
|
+
return `${rawUrl.slice(0, queryIndex + 1)}${redacted}${rawUrl.slice(queryEnd)}`;
|
|
1555
|
+
}
|
|
1556
|
+
function redactQueryPart(part) {
|
|
1557
|
+
const separatorIndex = part.indexOf("=");
|
|
1558
|
+
const rawKey = separatorIndex < 0 ? part : part.slice(0, separatorIndex);
|
|
1559
|
+
if (!SENSITIVE_QUERY_KEYS.has(normalizeQueryKey(rawKey))) {
|
|
1560
|
+
return part;
|
|
1561
|
+
}
|
|
1562
|
+
return `${rawKey}=redacted`;
|
|
1563
|
+
}
|
|
1564
|
+
function normalizeQueryKey(rawKey) {
|
|
1565
|
+
try {
|
|
1566
|
+
return decodeURIComponent(rawKey).toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
|
1567
|
+
} catch {
|
|
1568
|
+
return rawKey.toLowerCase().replaceAll(/[^a-z0-9]/g, "");
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/trace-store.ts
|
|
1573
|
+
import { createHash, randomBytes } from "crypto";
|
|
1574
|
+
import { access, mkdir, readFile, readdir, rename, rm as rm2, writeFile } from "fs/promises";
|
|
1575
|
+
import { homedir } from "os";
|
|
1576
|
+
import { join as join2 } from "path";
|
|
1577
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
1578
|
+
var CF_LIVE_TRACE_DIR_NAME = "cf-live-trace";
|
|
1579
|
+
var SESSIONS_DIR_NAME = "sessions";
|
|
1580
|
+
var EVENTS_DIR_NAME = "events";
|
|
1581
|
+
var MANIFEST_FILE_NAME = "manifest.json";
|
|
1582
|
+
var TRACE_TTL_MS = 2 * 60 * 60 * 1e3;
|
|
1583
|
+
var MAX_TARGET_SLUG_BYTES = 160;
|
|
1584
|
+
var SESSION_ID_PATTERN = /^s(?:[0-9a-f]{8}|[0-9a-f]{16})$/;
|
|
1585
|
+
var REQUEST_ID_PATTERN = /^r(?:[0-9a-f]{8}|[0-9a-f]{16})$/;
|
|
1586
|
+
function traceSessionsRoot(saptoolsRoot) {
|
|
1587
|
+
return join2(saptoolsRoot ?? join2(homedir(), SAPTOOLS_DIR_NAME), CF_LIVE_TRACE_DIR_NAME, SESSIONS_DIR_NAME);
|
|
1588
|
+
}
|
|
1589
|
+
async function createTraceSession(input, options = {}) {
|
|
1590
|
+
await pruneTraceSessions(options);
|
|
1591
|
+
const sessionId = resolveSessionId(options.sessionId);
|
|
1592
|
+
const createdAt = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1593
|
+
const target = toTargetIdentity(input.target);
|
|
1594
|
+
const directory = sessionDirectory(sessionId, options.saptoolsRoot);
|
|
1595
|
+
const eventsDirectory2 = join2(directory, EVENTS_DIR_NAME);
|
|
1596
|
+
const manifestPath2 = join2(directory, MANIFEST_FILE_NAME);
|
|
1597
|
+
await mkdir(eventsDirectory2, { recursive: true, mode: 448 });
|
|
1598
|
+
await writeJsonFile(manifestPath2, createManifest(sessionId, createdAt, target));
|
|
1599
|
+
return { sessionId, createdAt, target, directory, eventsDirectory: eventsDirectory2, manifestPath: manifestPath2 };
|
|
1600
|
+
}
|
|
1601
|
+
async function writeTraceEvent(session, event, options = {}) {
|
|
1602
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
1603
|
+
const requestId = resolveRequestId(options.requestId?.());
|
|
1604
|
+
await ensureSessionManifest(session);
|
|
1605
|
+
const record = {
|
|
1606
|
+
version: 1,
|
|
1607
|
+
sessionId: session.sessionId,
|
|
1608
|
+
requestId,
|
|
1609
|
+
createdAt: now.toISOString(),
|
|
1610
|
+
expiresAt: new Date(now.getTime() + TRACE_TTL_MS).toISOString(),
|
|
1611
|
+
target: session.target,
|
|
1612
|
+
requestBodyFormat: detectBodyFormat(event.requestBodyPreview, event.requestHeaders),
|
|
1613
|
+
responseBodyFormat: detectBodyFormat(event.responseBodyPreview, event.responseHeaders),
|
|
1614
|
+
event
|
|
1615
|
+
};
|
|
1616
|
+
const backupPath = join2(session.eventsDirectory, eventFileName(record, now));
|
|
1617
|
+
await mkdir(session.eventsDirectory, { recursive: true, mode: 448 });
|
|
1618
|
+
await writeJsonFile(backupPath, record);
|
|
1619
|
+
return { ...record, backupPath };
|
|
1620
|
+
}
|
|
1621
|
+
async function readTraceEvent(sessionId, requestId, options = {}) {
|
|
1622
|
+
const resolvedSessionId = resolveSessionId(sessionId);
|
|
1623
|
+
const resolvedRequestId = resolveRequestId(requestId);
|
|
1624
|
+
const entries = await listEventFilePaths(resolvedSessionId, options.saptoolsRoot);
|
|
1625
|
+
const candidates = entries.filter((path) => path.includes(`-${resolvedRequestId}-`));
|
|
1626
|
+
for (const path of candidates) {
|
|
1627
|
+
const record = await readStoredTraceEvent(path, resolvedSessionId);
|
|
1628
|
+
if (record === void 0 || isExpired(record, options)) {
|
|
1629
|
+
await rm2(path, { force: true });
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
if (record.requestId === resolvedRequestId) {
|
|
1633
|
+
return record;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
throw new Error("Saved trace request not found or expired");
|
|
1637
|
+
}
|
|
1638
|
+
async function visitTraceEvents(sessionId, visitor, options = {}) {
|
|
1639
|
+
const resolvedSessionId = resolveSessionId(sessionId);
|
|
1640
|
+
const entries = await listEventFilePaths(resolvedSessionId, options.saptoolsRoot);
|
|
1641
|
+
for (const path of entries) {
|
|
1642
|
+
const record = await readStoredTraceEvent(path, resolvedSessionId);
|
|
1643
|
+
if (record === void 0 || isExpired(record, options)) {
|
|
1644
|
+
await rm2(path, { force: true });
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (!await visitor(record)) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
async function listTraceSessions(options = {}) {
|
|
1653
|
+
await pruneTraceSessions(options);
|
|
1654
|
+
const sessionIds = await listSessionIds(options.saptoolsRoot);
|
|
1655
|
+
const summaries = await Promise.all(sessionIds.map(async (sessionId) => await readSessionSummary(sessionId, options)));
|
|
1656
|
+
return summaries.filter((summary) => summary !== void 0).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
1657
|
+
}
|
|
1658
|
+
async function pruneTraceSessions(options = {}) {
|
|
1659
|
+
const sessionIds = await listSessionIds(options.saptoolsRoot);
|
|
1660
|
+
const now = (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
1661
|
+
let removed = 0;
|
|
1662
|
+
for (const sessionId of sessionIds) {
|
|
1663
|
+
removed += await pruneSession(sessionId, now, options.saptoolsRoot);
|
|
1664
|
+
}
|
|
1665
|
+
return removed;
|
|
1666
|
+
}
|
|
1667
|
+
function toTargetIdentity(target) {
|
|
1668
|
+
return {
|
|
1669
|
+
...target.region === void 0 ? {} : { region: target.region },
|
|
1670
|
+
...target.apiEndpoint === void 0 ? {} : { apiEndpoint: target.apiEndpoint },
|
|
1671
|
+
org: target.org,
|
|
1672
|
+
space: target.space,
|
|
1673
|
+
app: target.app,
|
|
1674
|
+
instance: String(target.instanceIndex ?? 0)
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function createManifest(sessionId, createdAt, target) {
|
|
1678
|
+
return { version: 1, sessionId, createdAt, target };
|
|
1679
|
+
}
|
|
1680
|
+
async function ensureSessionManifest(session) {
|
|
1681
|
+
try {
|
|
1682
|
+
await access(session.manifestPath);
|
|
1683
|
+
return;
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
if (!isNodeError(error, "ENOENT")) {
|
|
1686
|
+
throw error;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
await mkdir(session.eventsDirectory, { recursive: true, mode: 448 });
|
|
1690
|
+
await writeJsonFile(
|
|
1691
|
+
session.manifestPath,
|
|
1692
|
+
createManifest(session.sessionId, session.createdAt, session.target)
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
function isExpired(record, options) {
|
|
1696
|
+
const now = (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
|
|
1697
|
+
return Date.parse(record.expiresAt) <= now;
|
|
1698
|
+
}
|
|
1699
|
+
async function readSessionSummary(sessionId, options) {
|
|
1700
|
+
const manifest = await readStoredManifest(manifestPath(sessionId, options.saptoolsRoot));
|
|
1701
|
+
if (manifest?.sessionId !== sessionId) {
|
|
1702
|
+
return void 0;
|
|
1703
|
+
}
|
|
1704
|
+
const eventCount = (await listEventFilePaths(sessionId, options.saptoolsRoot)).length;
|
|
1705
|
+
return {
|
|
1706
|
+
sessionId,
|
|
1707
|
+
createdAt: manifest.createdAt,
|
|
1708
|
+
target: manifest.target,
|
|
1709
|
+
eventCount,
|
|
1710
|
+
directory: sessionDirectory(sessionId, options.saptoolsRoot)
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
async function pruneSession(sessionId, now, saptoolsRoot) {
|
|
1714
|
+
const manifest = await readStoredManifest(manifestPath(sessionId, saptoolsRoot));
|
|
1715
|
+
const eventPaths = await listEventFilePaths(sessionId, saptoolsRoot);
|
|
1716
|
+
let removed = 0;
|
|
1717
|
+
let remaining = 0;
|
|
1718
|
+
for (const path of eventPaths) {
|
|
1719
|
+
const expiresAt = eventPathExpiresAt(path) ?? await storedEventExpiresAt(path);
|
|
1720
|
+
if (expiresAt === void 0 || expiresAt <= now) {
|
|
1721
|
+
await rm2(path, { force: true });
|
|
1722
|
+
removed += 1;
|
|
1723
|
+
} else {
|
|
1724
|
+
remaining += 1;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (remaining === 0 && isManifestExpiredOrMissing(manifest, now)) {
|
|
1728
|
+
await rm2(sessionDirectory(sessionId, saptoolsRoot), { recursive: true, force: true });
|
|
1729
|
+
}
|
|
1730
|
+
return removed;
|
|
1731
|
+
}
|
|
1732
|
+
async function storedEventExpiresAt(path) {
|
|
1733
|
+
const record = await readStoredTraceEvent(path);
|
|
1734
|
+
return record === void 0 ? void 0 : Date.parse(record.expiresAt);
|
|
1735
|
+
}
|
|
1736
|
+
function isManifestExpiredOrMissing(manifest, now) {
|
|
1737
|
+
if (manifest === void 0) {
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
return Date.parse(manifest.createdAt) + TRACE_TTL_MS <= now;
|
|
1741
|
+
}
|
|
1742
|
+
async function listSessionIds(saptoolsRoot) {
|
|
1743
|
+
try {
|
|
1744
|
+
const entries = await readdir(traceSessionsRoot(saptoolsRoot), { withFileTypes: true });
|
|
1745
|
+
return entries.filter((entry) => entry.isDirectory() && SESSION_ID_PATTERN.test(entry.name)).map((entry) => entry.name);
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1748
|
+
return [];
|
|
1749
|
+
}
|
|
1750
|
+
throw error;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function listEventFilePaths(sessionId, saptoolsRoot) {
|
|
1754
|
+
try {
|
|
1755
|
+
const directory = eventsDirectory(sessionId, saptoolsRoot);
|
|
1756
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1757
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => join2(directory, entry.name)).sort(compareEventPaths);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1760
|
+
return [];
|
|
1761
|
+
}
|
|
1762
|
+
throw error;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function compareEventPaths(left, right) {
|
|
1766
|
+
const leftTimestamp = eventPathTimestamp(left);
|
|
1767
|
+
const rightTimestamp = eventPathTimestamp(right);
|
|
1768
|
+
return leftTimestamp.localeCompare(rightTimestamp) || left.localeCompare(right);
|
|
1769
|
+
}
|
|
1770
|
+
function eventPathTimestamp(path) {
|
|
1771
|
+
return /-(\d{8}T\d{9}Z)\.json$/.exec(path)?.[1] ?? "";
|
|
1772
|
+
}
|
|
1773
|
+
function eventPathExpiresAt(path) {
|
|
1774
|
+
const compact = eventPathTimestamp(path);
|
|
1775
|
+
if (compact.length === 0) {
|
|
1776
|
+
return void 0;
|
|
1777
|
+
}
|
|
1778
|
+
const iso = compact.replace(
|
|
1779
|
+
/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z$/,
|
|
1780
|
+
"$1-$2-$3T$4:$5:$6.$7Z"
|
|
1781
|
+
);
|
|
1782
|
+
const createdAt = Date.parse(iso);
|
|
1783
|
+
return Number.isNaN(createdAt) ? void 0 : createdAt + TRACE_TTL_MS;
|
|
1784
|
+
}
|
|
1785
|
+
async function readStoredManifest(path) {
|
|
1786
|
+
const parsed = await readJsonFile(path);
|
|
1787
|
+
return parsed !== void 0 && isStoredManifest(parsed) ? parsed : void 0;
|
|
1788
|
+
}
|
|
1789
|
+
async function readStoredTraceEvent(path, expectedSessionId) {
|
|
1790
|
+
const parsed = await readJsonFile(path);
|
|
1791
|
+
if (parsed === void 0 || !isStoredTraceEvent(parsed)) {
|
|
1792
|
+
return void 0;
|
|
1793
|
+
}
|
|
1794
|
+
if (expectedSessionId !== void 0 && parsed.sessionId !== expectedSessionId) {
|
|
1795
|
+
return void 0;
|
|
1796
|
+
}
|
|
1797
|
+
return { ...parsed, backupPath: path };
|
|
1798
|
+
}
|
|
1799
|
+
async function readJsonFile(path) {
|
|
1800
|
+
let raw;
|
|
1801
|
+
try {
|
|
1802
|
+
raw = await readFile(path, "utf8");
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
if (isNodeError(error, "ENOENT")) {
|
|
1805
|
+
return void 0;
|
|
1806
|
+
}
|
|
1807
|
+
throw error;
|
|
1808
|
+
}
|
|
1809
|
+
try {
|
|
1810
|
+
return JSON.parse(raw);
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
if (!(error instanceof SyntaxError)) {
|
|
1813
|
+
throw error;
|
|
1814
|
+
}
|
|
1815
|
+
return void 0;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
async function writeJsonFile(path, value) {
|
|
1819
|
+
const temporaryPath = `${path}.tmp-${process.pid.toString()}-${randomHex(4)}`;
|
|
1820
|
+
let renamed = false;
|
|
1821
|
+
try {
|
|
1822
|
+
await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
|
|
1823
|
+
`, {
|
|
1824
|
+
encoding: "utf8",
|
|
1825
|
+
mode: 384
|
|
1826
|
+
});
|
|
1827
|
+
await rename(temporaryPath, path);
|
|
1828
|
+
renamed = true;
|
|
1829
|
+
} finally {
|
|
1830
|
+
if (!renamed) {
|
|
1831
|
+
await rm2(temporaryPath, { force: true });
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function eventFileName(record, now) {
|
|
1836
|
+
const parts = [
|
|
1837
|
+
targetSlug(record.target),
|
|
1838
|
+
record.sessionId,
|
|
1839
|
+
record.requestId,
|
|
1840
|
+
fileTimestamp(now)
|
|
1841
|
+
].join("-");
|
|
1842
|
+
return `${parts}.json`;
|
|
1843
|
+
}
|
|
1844
|
+
function targetSlug(target) {
|
|
1845
|
+
const regionOrApi = target.region ?? target.apiEndpoint ?? "api";
|
|
1846
|
+
const fullSlug = [regionOrApi, target.org, target.space, target.app].map(sanitizePathPart).join("-");
|
|
1847
|
+
if (Buffer.byteLength(fullSlug) <= MAX_TARGET_SLUG_BYTES) {
|
|
1848
|
+
return fullSlug;
|
|
1849
|
+
}
|
|
1850
|
+
const hash = createHash("sha256").update(fullSlug).digest("hex").slice(0, 12);
|
|
1851
|
+
const prefix = fullSlug.slice(0, MAX_TARGET_SLUG_BYTES - hash.length - 1).replace(/-+$/, "");
|
|
1852
|
+
return `${prefix}-${hash}`;
|
|
1853
|
+
}
|
|
1854
|
+
function sanitizePathPart(value) {
|
|
1855
|
+
const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1856
|
+
return sanitized.length === 0 ? "unknown" : sanitized;
|
|
1857
|
+
}
|
|
1858
|
+
function fileTimestamp(now) {
|
|
1859
|
+
return now.toISOString().replaceAll("-", "").replaceAll(":", "").replace(".", "");
|
|
1860
|
+
}
|
|
1861
|
+
function sessionDirectory(sessionId, saptoolsRoot) {
|
|
1862
|
+
return join2(traceSessionsRoot(saptoolsRoot), sessionId);
|
|
1863
|
+
}
|
|
1864
|
+
function eventsDirectory(sessionId, saptoolsRoot) {
|
|
1865
|
+
return join2(sessionDirectory(sessionId, saptoolsRoot), EVENTS_DIR_NAME);
|
|
1866
|
+
}
|
|
1867
|
+
function manifestPath(sessionId, saptoolsRoot) {
|
|
1868
|
+
return join2(sessionDirectory(sessionId, saptoolsRoot), MANIFEST_FILE_NAME);
|
|
1869
|
+
}
|
|
1870
|
+
function resolveSessionId(value) {
|
|
1871
|
+
const sessionId = value ?? `s${randomHex(8)}`;
|
|
1872
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
1873
|
+
throw new Error("Invalid trace session id");
|
|
1874
|
+
}
|
|
1875
|
+
return sessionId;
|
|
1876
|
+
}
|
|
1877
|
+
function resolveRequestId(value) {
|
|
1878
|
+
const requestId = value ?? `r${randomHex(8)}`;
|
|
1879
|
+
if (!REQUEST_ID_PATTERN.test(requestId)) {
|
|
1880
|
+
throw new Error("Invalid trace request id");
|
|
1881
|
+
}
|
|
1882
|
+
return requestId;
|
|
1883
|
+
}
|
|
1884
|
+
function randomHex(bytes) {
|
|
1885
|
+
return randomBytes(bytes).toString("hex");
|
|
1886
|
+
}
|
|
1887
|
+
function isStoredManifest(value) {
|
|
1888
|
+
return isRecord3(value) && value["version"] === 1 && isSessionId(value["sessionId"]) && isIsoTimestamp(value["createdAt"]) && isTraceTargetIdentity(value["target"]);
|
|
1889
|
+
}
|
|
1890
|
+
function isStoredTraceEvent(value) {
|
|
1891
|
+
return isRecord3(value) && value["version"] === 1 && isStoredEventMetadata(value) && isTraceTargetIdentity(value["target"]) && isTraceBodyFormat(value["requestBodyFormat"]) && isTraceBodyFormat(value["responseBodyFormat"]) && isLiveTraceEvent(value["event"]);
|
|
1892
|
+
}
|
|
1893
|
+
function isStoredEventMetadata(value) {
|
|
1894
|
+
return isSessionId(value["sessionId"]) && isRequestId(value["requestId"]) && isIsoTimestamp(value["createdAt"]) && isIsoTimestamp(value["expiresAt"]);
|
|
1895
|
+
}
|
|
1896
|
+
function isTraceTargetIdentity(value) {
|
|
1897
|
+
return isRecord3(value) && optionalString(value["region"]) && optionalString(value["apiEndpoint"]) && typeof value["org"] === "string" && typeof value["space"] === "string" && typeof value["app"] === "string" && typeof value["instance"] === "string";
|
|
1898
|
+
}
|
|
1899
|
+
function isLiveTraceEvent(value) {
|
|
1900
|
+
return isRecord3(value) && hasLiveTraceStrings(value) && hasLiveTraceMetrics(value) && isStringRecord(value["requestHeaders"]) && isStringRecord(value["responseHeaders"]) && typeof value["requestBodyTruncated"] === "boolean" && typeof value["responseBodyTruncated"] === "boolean" && value["source"] === "runtime-http" && optionalNullableString(value["correlationId"]);
|
|
1901
|
+
}
|
|
1902
|
+
function hasLiveTraceStrings(value) {
|
|
1903
|
+
const fields = [
|
|
1904
|
+
"id",
|
|
1905
|
+
"appId",
|
|
1906
|
+
"instance",
|
|
1907
|
+
"method",
|
|
1908
|
+
"path",
|
|
1909
|
+
"url",
|
|
1910
|
+
"normalizedUrl",
|
|
1911
|
+
"requestBodyPreview",
|
|
1912
|
+
"responseBodyPreview",
|
|
1913
|
+
"traceId"
|
|
1914
|
+
];
|
|
1915
|
+
return isIsoTimestamp(value["timestamp"]) && fields.every((field) => typeof value[field] === "string");
|
|
1916
|
+
}
|
|
1917
|
+
function hasLiveTraceMetrics(value) {
|
|
1918
|
+
return isNullableFiniteNumber(value["status"]) && isNullableNonNegativeNumber(value["durationMs"]) && isNonNegativeFiniteNumber(value["requestBytes"]) && isNonNegativeFiniteNumber(value["responseBytes"]) && isNonNegativeFiniteNumber(value["droppedBeforeEvent"]);
|
|
1919
|
+
}
|
|
1920
|
+
function isTraceBodyFormat(value) {
|
|
1921
|
+
return value === "empty" || value === "json" || value === "xml" || value === "html" || value === "form" || value === "text" || value === "binary" || value === "unknown";
|
|
1922
|
+
}
|
|
1923
|
+
function optionalString(value) {
|
|
1924
|
+
return value === void 0 || typeof value === "string";
|
|
1925
|
+
}
|
|
1926
|
+
function optionalNullableString(value) {
|
|
1927
|
+
return value === null || typeof value === "string";
|
|
1928
|
+
}
|
|
1929
|
+
function isStringRecord(value) {
|
|
1930
|
+
return isRecord3(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1931
|
+
}
|
|
1932
|
+
function isSessionId(value) {
|
|
1933
|
+
return typeof value === "string" && SESSION_ID_PATTERN.test(value);
|
|
1934
|
+
}
|
|
1935
|
+
function isRequestId(value) {
|
|
1936
|
+
return typeof value === "string" && REQUEST_ID_PATTERN.test(value);
|
|
1937
|
+
}
|
|
1938
|
+
function isIsoTimestamp(value) {
|
|
1939
|
+
if (typeof value !== "string") {
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
const parsed = new Date(value);
|
|
1943
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === value;
|
|
1944
|
+
}
|
|
1945
|
+
function isNullableFiniteNumber(value) {
|
|
1946
|
+
return value === null || isFiniteNumber(value);
|
|
1947
|
+
}
|
|
1948
|
+
function isNullableNonNegativeNumber(value) {
|
|
1949
|
+
return value === null || isNonNegativeFiniteNumber(value);
|
|
1950
|
+
}
|
|
1951
|
+
function isNonNegativeFiniteNumber(value) {
|
|
1952
|
+
return isFiniteNumber(value) && value >= 0;
|
|
1953
|
+
}
|
|
1954
|
+
function isFiniteNumber(value) {
|
|
1955
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
1956
|
+
}
|
|
1957
|
+
function isRecord3(value) {
|
|
1958
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1959
|
+
}
|
|
1960
|
+
function isNodeError(error, code) {
|
|
1961
|
+
return isRecord3(error) && error["code"] === code;
|
|
1962
|
+
}
|
|
1225
1963
|
|
|
1226
1964
|
// src/cli/options.ts
|
|
1965
|
+
var MAX_TIMER_SECONDS = 2147483;
|
|
1227
1966
|
function buildRunOptions2(flags, env) {
|
|
1228
1967
|
requireRegionOrApi(flags);
|
|
1229
1968
|
const target = buildTarget(flags, env);
|
|
@@ -1250,7 +1989,7 @@ function parsePositiveInteger(raw, label) {
|
|
|
1250
1989
|
return void 0;
|
|
1251
1990
|
}
|
|
1252
1991
|
const value = Number.parseInt(raw, 10);
|
|
1253
|
-
if (!Number.
|
|
1992
|
+
if (!Number.isSafeInteger(value) || value <= 0 || String(value) !== raw.trim()) {
|
|
1254
1993
|
throw new Error(`Invalid ${label}: "${raw}" \u2014 expected a positive integer.`);
|
|
1255
1994
|
}
|
|
1256
1995
|
return value;
|
|
@@ -1318,6 +2057,9 @@ function buildTarget(flags, env) {
|
|
|
1318
2057
|
function buildLimits(flags) {
|
|
1319
2058
|
const duration = parsePositiveInteger(flags.duration, "--duration");
|
|
1320
2059
|
const maxEvents = parsePositiveInteger(flags.maxEvents, "--max-events");
|
|
2060
|
+
if (duration !== void 0 && duration > MAX_TIMER_SECONDS) {
|
|
2061
|
+
throw new Error(`--duration is too large; maximum is ${String(MAX_TIMER_SECONDS)} seconds.`);
|
|
2062
|
+
}
|
|
1321
2063
|
return {
|
|
1322
2064
|
...duration === void 0 ? {} : { durationMs: duration * 1e3 },
|
|
1323
2065
|
...maxEvents === void 0 ? {} : { maxEvents }
|
|
@@ -1352,14 +2094,11 @@ function parseInstanceIndex(value) {
|
|
|
1352
2094
|
return parsed ?? 0;
|
|
1353
2095
|
}
|
|
1354
2096
|
function parseBodyLimit(value) {
|
|
1355
|
-
|
|
1356
|
-
return 4096;
|
|
1357
|
-
}
|
|
1358
|
-
return parseNonNegativeInteger(value, "--max-body-bytes");
|
|
2097
|
+
return parsePositiveInteger(value, "--max-body-bytes") ?? 4096;
|
|
1359
2098
|
}
|
|
1360
2099
|
function parseNonNegativeInteger(raw, label) {
|
|
1361
2100
|
const value = Number.parseInt(raw, 10);
|
|
1362
|
-
if (!Number.
|
|
2101
|
+
if (!Number.isSafeInteger(value) || value < 0 || String(value) !== raw.trim()) {
|
|
1363
2102
|
throw new Error(`Invalid ${label}: "${raw}" \u2014 expected a non-negative integer.`);
|
|
1364
2103
|
}
|
|
1365
2104
|
return value;
|
|
@@ -1395,22 +2134,384 @@ function writeLog(message) {
|
|
|
1395
2134
|
function writeSummaryLine(event) {
|
|
1396
2135
|
const status = event.status === null ? "-" : String(event.status);
|
|
1397
2136
|
const duration = event.durationMs === null ? "-" : `${event.durationMs.toString()}ms`;
|
|
1398
|
-
|
|
2137
|
+
const request = event.sessionId === void 0 || event.requestId === void 0 ? "" : ` ${event.sessionId}/${event.requestId}`;
|
|
2138
|
+
process2.stdout.write(`${event.timestamp} ${event.method} ${event.normalizedUrl} ${status} ${duration}${request}
|
|
1399
2139
|
`);
|
|
1400
2140
|
}
|
|
1401
2141
|
|
|
2142
|
+
// src/trace-inspect.ts
|
|
2143
|
+
var DEFAULT_BODY_LIMIT = 4e3;
|
|
2144
|
+
function inspectTraceBodyResult(record, options) {
|
|
2145
|
+
const limit = positive("limit", options.limit ?? DEFAULT_BODY_LIMIT);
|
|
2146
|
+
const maxRows = positive("max rows", options.maxRows ?? Number.MAX_SAFE_INTEGER);
|
|
2147
|
+
const pointer = options.path ?? "";
|
|
2148
|
+
const parsed = parseJsonBody(bodyText(record, options.body));
|
|
2149
|
+
const selected = resolvePointer(parsed, pointer);
|
|
2150
|
+
const rows = inspectionRows(selected, pointer, limit, maxRows);
|
|
2151
|
+
return {
|
|
2152
|
+
rows: rows.values,
|
|
2153
|
+
totalRows: rows.total,
|
|
2154
|
+
rowsTruncated: rows.total > rows.values.length
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
function inspectionRows(selected, pointer, valueLimit, maxRows) {
|
|
2158
|
+
if (Array.isArray(selected)) {
|
|
2159
|
+
const values = selected.slice(0, maxRows).map(
|
|
2160
|
+
(item, index) => inspectionRow(`${pointer}/${String(index)}`, item, valueLimit)
|
|
2161
|
+
);
|
|
2162
|
+
return { values, total: selected.length };
|
|
2163
|
+
}
|
|
2164
|
+
if (!isRecord4(selected)) {
|
|
2165
|
+
return { values: [inspectionRow(pointer, selected, valueLimit)], total: 1 };
|
|
2166
|
+
}
|
|
2167
|
+
return objectInspectionRows(selected, pointer, valueLimit, maxRows);
|
|
2168
|
+
}
|
|
2169
|
+
function objectInspectionRows(selected, pointer, valueLimit, maxRows) {
|
|
2170
|
+
const values = [];
|
|
2171
|
+
let total = 0;
|
|
2172
|
+
for (const key in selected) {
|
|
2173
|
+
if (!Object.hasOwn(selected, key)) {
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
if (values.length < maxRows) {
|
|
2177
|
+
values.push(inspectionRow(`${pointer}/${escapePointerToken(key)}`, selected[key], valueLimit));
|
|
2178
|
+
}
|
|
2179
|
+
total += 1;
|
|
2180
|
+
}
|
|
2181
|
+
return { values, total };
|
|
2182
|
+
}
|
|
2183
|
+
function searchTraceRecords(records, searchTerm, options) {
|
|
2184
|
+
const term = searchTerm.trim().toLowerCase();
|
|
2185
|
+
if (term.length === 0) {
|
|
2186
|
+
throw new Error("search text must not be empty");
|
|
2187
|
+
}
|
|
2188
|
+
const limit = positive("limit", options.limit);
|
|
2189
|
+
const previewLength = positive("preview length", options.previewLength ?? 128);
|
|
2190
|
+
const matches = [];
|
|
2191
|
+
for (const record of records) {
|
|
2192
|
+
for (const side of selectedSides(options.body)) {
|
|
2193
|
+
const remaining = limit - matches.length;
|
|
2194
|
+
matches.push(...searchBody(record, side, term, remaining, previewLength));
|
|
2195
|
+
if (matches.length >= limit) {
|
|
2196
|
+
return matches;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return matches;
|
|
2201
|
+
}
|
|
2202
|
+
function searchBody(record, side, term, limit, previewLength) {
|
|
2203
|
+
if (limit <= 0) {
|
|
2204
|
+
return [];
|
|
2205
|
+
}
|
|
2206
|
+
const body = bodyText(record, side);
|
|
2207
|
+
const parsed = tryParseJson(body);
|
|
2208
|
+
return parsed === void 0 ? plainTextMatches(record, side, body, term, limit, previewLength) : jsonSearchMatches(record, side, parsed.value, term, limit, previewLength);
|
|
2209
|
+
}
|
|
2210
|
+
function plainTextMatches(record, side, text, term, limit, previewLength) {
|
|
2211
|
+
const matches = [];
|
|
2212
|
+
const lowerText = text.toLowerCase();
|
|
2213
|
+
let offset = lowerText.indexOf(term);
|
|
2214
|
+
while (offset >= 0 && matches.length < limit) {
|
|
2215
|
+
const start = Math.max(0, offset - 32);
|
|
2216
|
+
matches.push(toSearchMatch(record, side, "", text.slice(start, start + previewLength).replaceAll(/[\r\n\t]/g, " "), offset));
|
|
2217
|
+
offset = lowerText.indexOf(term, offset + Math.max(1, term.length));
|
|
2218
|
+
}
|
|
2219
|
+
return matches;
|
|
2220
|
+
}
|
|
2221
|
+
function jsonSearchMatches(record, side, root, term, limit, previewLength) {
|
|
2222
|
+
const matches = [];
|
|
2223
|
+
const stack = [{ path: "", value: root }];
|
|
2224
|
+
while (stack.length > 0 && matches.length < limit) {
|
|
2225
|
+
const entry = stack.pop();
|
|
2226
|
+
if (entry === void 0) {
|
|
2227
|
+
break;
|
|
2228
|
+
}
|
|
2229
|
+
inspectJsonSearchEntry(record, side, entry, term, limit, previewLength, matches, stack);
|
|
2230
|
+
}
|
|
2231
|
+
return matches.slice(0, limit);
|
|
2232
|
+
}
|
|
2233
|
+
function inspectJsonSearchEntry(record, side, entry, term, limit, previewLength, matches, stack) {
|
|
2234
|
+
if (typeof entry.value === "object" && entry.value !== null) {
|
|
2235
|
+
pushJsonChildren(record, side, entry, term, limit, previewLength, matches, stack);
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
const text = jsonScalarSearchText(entry.value);
|
|
2239
|
+
if (text.toLowerCase().includes(term)) {
|
|
2240
|
+
matches.push(toSearchMatch(record, side, entry.path, text.slice(0, previewLength)));
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function pushJsonChildren(record, side, entry, term, limit, previewLength, matches, stack) {
|
|
2244
|
+
if (!Array.isArray(entry.value) && !isRecord4(entry.value)) {
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const entries = Array.isArray(entry.value) ? entry.value.map((item, index) => [String(index), item]) : Object.entries(entry.value);
|
|
2248
|
+
for (const [key, value] of entries.reverse()) {
|
|
2249
|
+
if (matches.length >= limit) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
const path = `${entry.path}/${escapePointerToken(key)}`;
|
|
2253
|
+
if (key.toLowerCase().includes(term)) {
|
|
2254
|
+
matches.push(toSearchMatch(record, side, path, jsonValueText(value, previewLength)));
|
|
2255
|
+
}
|
|
2256
|
+
stack.push({ path, value });
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
function toSearchMatch(record, side, path, preview, offset) {
|
|
2260
|
+
return {
|
|
2261
|
+
sessionId: record.sessionId,
|
|
2262
|
+
requestId: record.requestId,
|
|
2263
|
+
timestamp: record.event.timestamp,
|
|
2264
|
+
method: record.event.method,
|
|
2265
|
+
normalizedUrl: record.event.normalizedUrl,
|
|
2266
|
+
status: record.event.status,
|
|
2267
|
+
body: side,
|
|
2268
|
+
path,
|
|
2269
|
+
...offset === void 0 ? {} : { offset },
|
|
2270
|
+
preview
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
function selectedSides(body) {
|
|
2274
|
+
return body === "both" ? ["request", "response"] : [body];
|
|
2275
|
+
}
|
|
2276
|
+
function bodyText(record, body) {
|
|
2277
|
+
return body === "request" ? record.event.requestBodyPreview : record.event.responseBodyPreview;
|
|
2278
|
+
}
|
|
2279
|
+
function parseJsonBody(body) {
|
|
2280
|
+
try {
|
|
2281
|
+
return JSON.parse(body);
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
throw new Error("Saved trace body does not contain valid JSON", { cause: error });
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
function tryParseJson(body) {
|
|
2287
|
+
try {
|
|
2288
|
+
return { value: JSON.parse(body) };
|
|
2289
|
+
} catch {
|
|
2290
|
+
return void 0;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
function decodePointerToken(token) {
|
|
2294
|
+
if (/~(?:[^01]|$)/.test(token)) {
|
|
2295
|
+
throw new Error("Invalid JSON Pointer escape");
|
|
2296
|
+
}
|
|
2297
|
+
return token.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
2298
|
+
}
|
|
2299
|
+
function pointerTokens(pointer) {
|
|
2300
|
+
if (pointer === "") {
|
|
2301
|
+
return [];
|
|
2302
|
+
}
|
|
2303
|
+
if (!pointer.startsWith("/")) {
|
|
2304
|
+
throw new Error("JSON Pointer must be empty or start with /");
|
|
2305
|
+
}
|
|
2306
|
+
return pointer.slice(1).split("/").map(decodePointerToken);
|
|
2307
|
+
}
|
|
2308
|
+
function resolvePointer(root, pointer) {
|
|
2309
|
+
let current = root;
|
|
2310
|
+
for (const token of pointerTokens(pointer)) {
|
|
2311
|
+
current = resolvePointerToken(current, token, pointer);
|
|
2312
|
+
}
|
|
2313
|
+
return current;
|
|
2314
|
+
}
|
|
2315
|
+
function resolvePointerToken(current, token, pointer) {
|
|
2316
|
+
if (Array.isArray(current)) {
|
|
2317
|
+
const index = Number(token);
|
|
2318
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= current.length) {
|
|
2319
|
+
throw new Error(`JSON Pointer path "${pointer}" not found`);
|
|
2320
|
+
}
|
|
2321
|
+
return current[index];
|
|
2322
|
+
}
|
|
2323
|
+
if (isRecord4(current) && Object.hasOwn(current, token)) {
|
|
2324
|
+
return current[token];
|
|
2325
|
+
}
|
|
2326
|
+
throw new Error(`JSON Pointer path "${pointer}" not found`);
|
|
2327
|
+
}
|
|
2328
|
+
function escapePointerToken(token) {
|
|
2329
|
+
return token.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
2330
|
+
}
|
|
2331
|
+
function inspectionRow(path, value, limit) {
|
|
2332
|
+
return { path, type: jsonType(value), value: jsonValueText(value, limit) };
|
|
2333
|
+
}
|
|
2334
|
+
function jsonType(value) {
|
|
2335
|
+
if (value === null) {
|
|
2336
|
+
return "null";
|
|
2337
|
+
}
|
|
2338
|
+
if (Array.isArray(value)) {
|
|
2339
|
+
return "array";
|
|
2340
|
+
}
|
|
2341
|
+
return typeof value;
|
|
2342
|
+
}
|
|
2343
|
+
function jsonValueText(value, limit) {
|
|
2344
|
+
if (Array.isArray(value)) {
|
|
2345
|
+
return `items=${String(value.length)}`;
|
|
2346
|
+
}
|
|
2347
|
+
if (typeof value === "object" && value !== null) {
|
|
2348
|
+
return `keys=${String(Object.keys(value).length)}`;
|
|
2349
|
+
}
|
|
2350
|
+
if (typeof value === "string") {
|
|
2351
|
+
return value.slice(0, limit);
|
|
2352
|
+
}
|
|
2353
|
+
if (value === null) {
|
|
2354
|
+
return "null";
|
|
2355
|
+
}
|
|
2356
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2357
|
+
return String(value);
|
|
2358
|
+
}
|
|
2359
|
+
return "";
|
|
2360
|
+
}
|
|
2361
|
+
function jsonScalarSearchText(value) {
|
|
2362
|
+
if (typeof value === "string") {
|
|
2363
|
+
return value;
|
|
2364
|
+
}
|
|
2365
|
+
if (value === null) {
|
|
2366
|
+
return "null";
|
|
2367
|
+
}
|
|
2368
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2369
|
+
return String(value);
|
|
2370
|
+
}
|
|
2371
|
+
return "";
|
|
2372
|
+
}
|
|
2373
|
+
function positive(name, value) {
|
|
2374
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
2375
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
2376
|
+
}
|
|
2377
|
+
return value;
|
|
2378
|
+
}
|
|
2379
|
+
function isRecord4(value) {
|
|
2380
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// src/cli/session-commands.ts
|
|
2384
|
+
function registerSessionCommands(program) {
|
|
2385
|
+
const session = program.command("session").description("inspect saved live trace sessions");
|
|
2386
|
+
session.command("list").description("list active trace sessions").action(async () => {
|
|
2387
|
+
writeJson({ sessions: await listTraceSessions() });
|
|
2388
|
+
});
|
|
2389
|
+
session.command("events <sessionId>").description("list compact events saved for a trace session").option("--method <method>", "filter by HTTP method").option("--status <code>", "filter by HTTP status", parseIntOption).option("--path <text>", "filter by URL/path substring").option("--limit <count>", "maximum events to print", parseIntOption).action(async (sessionId, _options, command) => {
|
|
2390
|
+
await runEvents(sessionId, command.opts());
|
|
2391
|
+
});
|
|
2392
|
+
session.command("search <sessionId> <text>").description("search saved request and response bodies").option("--body <side>", "request, response, or both", parseSearchBodySide, "both").option("--limit <count>", "maximum matches to print", parseIntOption).option("--length <chars>", "maximum preview characters", parseIntOption).action(async (sessionId, text, _options, command) => {
|
|
2393
|
+
await runSearch(sessionId, text, command.opts());
|
|
2394
|
+
});
|
|
2395
|
+
session.command("body <sessionId> <requestId>").description("inspect a saved JSON request or response body").option("--body <side>", "request or response", parseBodySide, "response").option("--path <pointer>", "JSON Pointer inside the saved body").option("--limit <chars>", "maximum characters per value", parseIntOption).option("--rows <count>", "maximum structure rows to print", parseIntOption).action(async (sessionId, requestId, _options, command) => {
|
|
2396
|
+
await runBody(sessionId, requestId, command.opts());
|
|
2397
|
+
});
|
|
2398
|
+
session.command("prune").description("remove expired trace event files").action(async () => {
|
|
2399
|
+
writeJson({ removed: await pruneTraceSessions() });
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
async function runEvents(sessionId, options) {
|
|
2403
|
+
const limit = positive2("--limit", options.limit, 50);
|
|
2404
|
+
const status = optionalHttpStatus(options.status);
|
|
2405
|
+
const events = [];
|
|
2406
|
+
await visitTraceEvents(sessionId, (record) => {
|
|
2407
|
+
if (matchesEvent(record, options, status)) {
|
|
2408
|
+
events.push(compactTraceEvent(record));
|
|
2409
|
+
}
|
|
2410
|
+
return events.length < limit;
|
|
2411
|
+
});
|
|
2412
|
+
writeJson({ sessionId, events });
|
|
2413
|
+
}
|
|
2414
|
+
async function runSearch(sessionId, text, options) {
|
|
2415
|
+
const limit = positive2("--limit", options.limit, 20);
|
|
2416
|
+
const body = options.body ?? "both";
|
|
2417
|
+
const previewLength = positive2("--length", options.length, 128);
|
|
2418
|
+
const matches = [];
|
|
2419
|
+
await visitTraceEvents(sessionId, (record) => {
|
|
2420
|
+
const found = searchTraceRecords([record], text, {
|
|
2421
|
+
body,
|
|
2422
|
+
limit: limit - matches.length,
|
|
2423
|
+
previewLength
|
|
2424
|
+
});
|
|
2425
|
+
matches.push(...found);
|
|
2426
|
+
return matches.length < limit;
|
|
2427
|
+
});
|
|
2428
|
+
writeJson({ sessionId, matches });
|
|
2429
|
+
}
|
|
2430
|
+
async function runBody(sessionId, requestId, options) {
|
|
2431
|
+
const record = await readTraceEvent(sessionId, requestId);
|
|
2432
|
+
const body = options.body ?? "response";
|
|
2433
|
+
const inspection = inspectTraceBodyResult(record, {
|
|
2434
|
+
body,
|
|
2435
|
+
...options.path === void 0 ? {} : { path: options.path },
|
|
2436
|
+
limit: positive2("--limit", options.limit, 4e3),
|
|
2437
|
+
maxRows: positive2("--rows", options.rows, 100)
|
|
2438
|
+
});
|
|
2439
|
+
writeJson({
|
|
2440
|
+
sessionId,
|
|
2441
|
+
requestId,
|
|
2442
|
+
body,
|
|
2443
|
+
format: body === "request" ? record.requestBodyFormat : record.responseBodyFormat,
|
|
2444
|
+
...inspection
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
function matchesEvent(record, options, status) {
|
|
2448
|
+
const method = options.method?.trim().toUpperCase();
|
|
2449
|
+
const path = options.path?.trim().toLowerCase();
|
|
2450
|
+
if (method !== void 0 && record.event.method !== method) {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
if (status !== void 0 && record.event.status !== status) {
|
|
2454
|
+
return false;
|
|
2455
|
+
}
|
|
2456
|
+
return path === void 0 || record.event.normalizedUrl.toLowerCase().includes(path);
|
|
2457
|
+
}
|
|
2458
|
+
function parseIntOption(value) {
|
|
2459
|
+
const trimmed = value.trim();
|
|
2460
|
+
if (!/^-?\d+$/.test(trimmed)) {
|
|
2461
|
+
throw new Error(`Expected an integer but received "${value}"`);
|
|
2462
|
+
}
|
|
2463
|
+
const parsed = Number(trimmed);
|
|
2464
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
2465
|
+
throw new Error(`Expected a safe integer but received "${value}"`);
|
|
2466
|
+
}
|
|
2467
|
+
return parsed;
|
|
2468
|
+
}
|
|
2469
|
+
function positive2(name, value, fallback) {
|
|
2470
|
+
const resolved = value ?? fallback;
|
|
2471
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
2472
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
2473
|
+
}
|
|
2474
|
+
return resolved;
|
|
2475
|
+
}
|
|
2476
|
+
function optionalHttpStatus(value) {
|
|
2477
|
+
if (value === void 0) {
|
|
2478
|
+
return void 0;
|
|
2479
|
+
}
|
|
2480
|
+
if (value < 100 || value > 599) {
|
|
2481
|
+
throw new Error("--status must be an integer from 100 through 599");
|
|
2482
|
+
}
|
|
2483
|
+
return value;
|
|
2484
|
+
}
|
|
2485
|
+
function parseSearchBodySide(value) {
|
|
2486
|
+
if (value === "request" || value === "response" || value === "both") {
|
|
2487
|
+
return value;
|
|
2488
|
+
}
|
|
2489
|
+
throw new Error(`Invalid --body: "${value}" \u2014 expected request, response, or both.`);
|
|
2490
|
+
}
|
|
2491
|
+
function parseBodySide(value) {
|
|
2492
|
+
if (value === "request" || value === "response") {
|
|
2493
|
+
return value;
|
|
2494
|
+
}
|
|
2495
|
+
throw new Error(`Invalid --body: "${value}" \u2014 expected request or response.`);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
1402
2498
|
// src/cli/program.ts
|
|
1403
2499
|
async function main(argv) {
|
|
1404
2500
|
const program = new Command();
|
|
1405
|
-
program.name("cf-live-trace").description("Inject a runtime HTTP trace hook into a Cloud Foundry Node.js app and stream request/response events").option("-r, --region <key>", "CF region key (default: current cf target)").option("--api-endpoint <url>", "Explicit CF API endpoint").option("-o, --org <name>", "CF org name (default: current cf target)").option("-s, --space <name>", "CF space name (default: current cf target)").
|
|
2501
|
+
program.name("cf-live-trace").description("Inject a runtime HTTP trace hook into a Cloud Foundry Node.js app and stream request/response events").option("-r, --region <key>", "CF region key (default: current cf target)").option("--api-endpoint <url>", "Explicit CF API endpoint").option("-o, --org <name>", "CF org name (default: current cf target)").option("-s, --space <name>", "CF space name (default: current cf target)").option("-a, --app <name>", "CF app name").option("--email <value>", "SAP email (default: SAP_EMAIL)").option("--password <value>", "SAP password (default: SAP_PASSWORD)").option("-i, --instance <index>", "CF app instance index (default: 0)").option("--cf-home <dir>", "Use an existing CF_HOME instead of a temporary one").option("--cf-command <path>", "CF CLI executable or test shim").option("--duration <seconds>", "Stop after N seconds").option("--max-events <count>", "Stop after N trace events").option("--max-body-bytes <bytes>", "Maximum request/response capture bytes; must be greater than 0", "4096").option("--no-capture-headers", "Do not capture request/response headers").option("--no-capture-request-body", "Do not capture request body previews").option("--no-capture-response-body", "Do not capture response body previews").option("--no-uninstall-on-exit", "Disable the runtime hook instead of uninstalling it on exit").option("--format <format>", "Output format: ndjson, summary, json", "ndjson").option("--quiet", "Suppress progress messages on stderr").action(async (flags) => {
|
|
1406
2502
|
await runTraceCommand(await buildRunOptionsWithCurrentTarget(flags, process3.env));
|
|
1407
2503
|
});
|
|
2504
|
+
registerSessionCommands(program);
|
|
1408
2505
|
await program.parseAsync([...argv]);
|
|
1409
2506
|
}
|
|
1410
2507
|
async function runTraceCommand(options) {
|
|
1411
2508
|
const cfHome = await resolveCfHome(options);
|
|
2509
|
+
let retention;
|
|
1412
2510
|
try {
|
|
1413
|
-
const
|
|
2511
|
+
const traceSession = await createTraceSession({ target: options.target });
|
|
2512
|
+
retention = startRetentionPruner(options);
|
|
2513
|
+
writeSessionHints(traceSession, options);
|
|
2514
|
+
const output = { count: 0, events: [] };
|
|
1414
2515
|
const eventLimit = createEventLimit(options);
|
|
1415
2516
|
const runtimeError = createRuntimeErrorStopWaiter();
|
|
1416
2517
|
const session = new LiveTraceSession({
|
|
@@ -1426,30 +2527,61 @@ async function runTraceCommand(options) {
|
|
|
1426
2527
|
writeLog(message);
|
|
1427
2528
|
}
|
|
1428
2529
|
},
|
|
1429
|
-
onEvents: (batch) => {
|
|
1430
|
-
handleEvents(batch, options,
|
|
1431
|
-
eventLimit.check(
|
|
2530
|
+
onEvents: async (batch) => {
|
|
2531
|
+
await handleEvents(batch, options, output, traceSession);
|
|
2532
|
+
eventLimit.check(output.count);
|
|
1432
2533
|
}
|
|
1433
2534
|
});
|
|
1434
2535
|
await runUntilStopped(session, options, eventLimit, runtimeError);
|
|
1435
2536
|
if (options.format === "json") {
|
|
1436
|
-
writeJson({ events });
|
|
2537
|
+
writeJson({ sessionId: traceSession.sessionId, events: output.events });
|
|
1437
2538
|
}
|
|
1438
2539
|
} finally {
|
|
2540
|
+
retention?.cleanup();
|
|
1439
2541
|
await cfHome.dispose();
|
|
1440
2542
|
}
|
|
1441
2543
|
}
|
|
1442
|
-
function
|
|
1443
|
-
|
|
1444
|
-
|
|
2544
|
+
function startRetentionPruner(options) {
|
|
2545
|
+
const timer = setInterval(() => {
|
|
2546
|
+
void pruneTraceSessions().catch((error) => {
|
|
2547
|
+
if (!options.quiet) {
|
|
2548
|
+
writeLog(`session: retention cleanup failed: ${formatError2(error)}`);
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
}, 6e4);
|
|
2552
|
+
timer.unref();
|
|
2553
|
+
return {
|
|
2554
|
+
cleanup() {
|
|
2555
|
+
clearInterval(timer);
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
async function handleEvents(batch, options, output, traceSession) {
|
|
2560
|
+
const remaining = options.limits.maxEvents === void 0 ? batch.length : Math.max(0, options.limits.maxEvents - output.count);
|
|
2561
|
+
for (const event of batch.slice(0, remaining)) {
|
|
2562
|
+
const record = await writeTraceEvent(traceSession, event);
|
|
2563
|
+
const compact = compactTraceEvent(record);
|
|
2564
|
+
output.count += 1;
|
|
2565
|
+
if (options.format === "json") {
|
|
2566
|
+
output.events.push(compact);
|
|
2567
|
+
}
|
|
1445
2568
|
if (options.format === "ndjson") {
|
|
1446
|
-
writeJsonLine(
|
|
2569
|
+
writeJsonLine(compact);
|
|
1447
2570
|
}
|
|
1448
2571
|
if (options.format === "summary") {
|
|
1449
|
-
writeSummaryLine(
|
|
2572
|
+
writeSummaryLine(compact);
|
|
1450
2573
|
}
|
|
1451
2574
|
}
|
|
1452
2575
|
}
|
|
2576
|
+
function writeSessionHints(traceSession, options) {
|
|
2577
|
+
if (options.quiet) {
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
writeLog(`session: id=${traceSession.sessionId} backups=${traceSession.directory} ttl=2h`);
|
|
2581
|
+
writeLog(`session: list events with \`cf-live-trace session events ${traceSession.sessionId}\``);
|
|
2582
|
+
writeLog(`session: search bodies with \`cf-live-trace session search ${traceSession.sessionId} <text>\``);
|
|
2583
|
+
writeLog(`session: inspect JSON with \`cf-live-trace session body ${traceSession.sessionId} <requestId> --body response --path / --limit 4000\``);
|
|
2584
|
+
}
|
|
1453
2585
|
async function runUntilStopped(session, options, eventLimit, runtimeError) {
|
|
1454
2586
|
const abort = createAbortPromise();
|
|
1455
2587
|
const duration = createDurationStopWaiter(options.limits.durationMs);
|
|
@@ -1593,6 +2725,10 @@ async function resolveCfHome(options) {
|
|
|
1593
2725
|
}
|
|
1594
2726
|
};
|
|
1595
2727
|
}
|
|
2728
|
+
function formatError2(error) {
|
|
2729
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2730
|
+
return message.trim().length > 0 ? message.trim() : "Unknown error";
|
|
2731
|
+
}
|
|
1596
2732
|
|
|
1597
2733
|
// src/cli.ts
|
|
1598
2734
|
try {
|