Setting the file. One moment.
Write Log Summary · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page
168 of 182 lib/ write-log-summary.js
JavaScript · 208 lines · 9 KB
13
const
path
=
require
(
'node:path'
);
14 const readline = require ( 'node:readline' );
15
16 const LEDGERS = {
17 attempts: 'state/attempts/write-attempts.ndjson' ,
18 audit: 'logs/import-audit.ndjson' ,
19 };
20 const DEFAULT_LIMIT = 20 ;
21 const MAX_LIMIT = 200 ;
22 const TOP_ERRORS = 5 ;
23 const MESSAGE_CHARS = 160 ;
24 const DEFAULT_FIELDS = [ 'attemptId' , 'sourceStableKey' , 'targetEntityType' , 'status' , 'runId' , 'errorClass' , 'message' ];
25
26 function errorClassOf ( row ) {
27 const failure = row && row.failure;
28 if ( ! failure || typeof failure !== 'object' ) return null ;
29 if (failure.errorCode) return String (failure.errorCode);
30 if (failure.httpStatus) return `http_${ failure . httpStatus }` ;
31 if (failure.outcome) return String (failure.outcome);
32 return 'unclassified' ;
33 }
34
35 function compactAttempt ( row ) {
36 const failure = row.failure && typeof row.failure === 'object' ? row.failure : null ;
37 const message = failure && failure.message ? String (failure.message) : null ;
38 return {
39 attemptId: row.attemptId,
40 sourceStableKey: row.sourceStableKey,
41 targetEntityType: row.targetEntityType || 'unknown' ,
42 status: row.status || 'unknown' ,
43 runId: row.runId || null ,
44 errorClass: errorClassOf (row),
45 message: message && message. length > MESSAGE_CHARS ? `${ message . slice ( 0 , MESSAGE_CHARS ) }…` : message,
46 };
47 }
48
49 async function forEachRow ( filePath , onRow ) {
50 const stream = fs. createReadStream (filePath, { encoding: 'utf8' });
51 const lines = readline. createInterface ({ input: stream, crlfDelay: Infinity });
52 let rows = 0 ;
53 let invalid = 0 ;
54 for await ( const line of lines) {
55 if ( ! line. trim ()) continue ;
56 let row;
57 try { row = JSON . parse (line); } catch { invalid += 1 ; continue ; }
58 rows += 1 ;
59 onRow (row);
60 }
61 return { rows, invalid };
62 }
63
64 function bump ( map , key , sub ) {
65 const inner = map. get (key) || {};
66 inner[sub] = (inner[sub] || 0 ) + 1 ;
67 map. set (key, inner);
68 }
69
70 function sortedObject ( map ) {
71 return Object. fromEntries ([ ... map. entries ()]. sort (([ a ], [ b ]) => String (a). localeCompare ( String (b))));
72 }
73
74 function normalizeOptions ( options = {}) {
75 const limit = Math. min (Math. max (Number. isFinite ( Number (options.limit)) ? Number (options.limit) : DEFAULT_LIMIT , 0 ), MAX_LIMIT );
76 const fields = Array. isArray (options.fields) && options.fields. length ? options.fields : DEFAULT_FIELDS ;
77 return { run: options.run || null , sinceRun: options.sinceRun || null , entity: options.entity || null , status: options.status || null , limit, fields };
78 }
79
80 async function summarizeAttempts ( filePath , options = {}) {
81 const opts = normalizeOptions (options);
82 const attempts = new Map ();
83 const runOrder = [];
84 const seenRuns = new Set ();
85 const { rows , invalid } = await forEachRow (filePath, ( row ) => {
86 if ( ! row || ! row.attemptId) return ;
87 if (row.runId && ! seenRuns. has (row.runId)) { seenRuns. add (row.runId); runOrder. push (row.runId); }
88 const previous = attempts. get (row.attemptId);
89 const merged = compactAttempt (previous ? { ... previous.raw, ... row } : row);
90 // Keep only the fields compactAttempt needs, so memory tracks attempts, not row size.
91 attempts. set (row.attemptId, { ... merged, raw: { attemptId: row.attemptId, sourceStableKey: merged.sourceStableKey, targetEntityType: merged.targetEntityType, status: merged.status, runId: merged.runId, failure: row.failure || (previous && previous.raw.failure) || undefined } });
92 });
93
94 const latestRun = runOrder. length ? runOrder[runOrder. length - 1 ] : null ;
95 const runFilter = opts.run === 'latest' ? latestRun : opts.run;
96 const sinceIndex = opts.sinceRun ? runOrder. indexOf (opts.sinceRun) : - 1 ;
97 const runsAfter = sinceIndex >= 0 ? new Set (runOrder. slice (sinceIndex + 1 )) : null ;
98
99 const byEntity = new Map ();
100 const errors = new Map ();
101 const sample = [];
102 let matching = 0 ;
103 for ( const attempt of attempts. values ()) {
104 if (runFilter && attempt.runId !== runFilter) continue ;
105 if (runsAfter && ! runsAfter. has (attempt.runId)) continue ;
106 bump (byEntity, attempt.targetEntityType, attempt.status);
107 if (attempt.errorClass) bump (errors, attempt.targetEntityType, attempt.errorClass);
108 if (opts.entity && attempt.targetEntityType !== opts.entity) continue ;
109 if (opts.status && attempt.status !== opts.status) continue ;
110 matching += 1 ;
111 if (sample. length < opts.limit && (opts.entity || opts.status)) {
112 sample. push (Object. fromEntries (opts.fields. filter (( field ) => field in attempt). map (( field ) => [field, attempt[field]])));
113 }
114 }
115
116 const topErrors = {};
117 for ( const [ entity , classes ] of errors. entries ()) {
118 topErrors[entity] = Object. entries (classes). sort (( a , b ) => b[ 1 ] - a[ 1 ]). slice ( 0 , TOP_ERRORS ). map (([ errorClass , count ]) => ({ errorClass, count }));
119 }
120 const totals = {};
121 for ( const statuses of byEntity. values ()) {
122 for ( const [ status , count ] of Object. entries (statuses)) totals[status] = (totals[status] || 0 ) + count;
123 }
124
125 return {
126 ledger: 'attempts' ,
127 file: filePath,
128 bytes: fs. statSync (filePath).size,
129 rows,
130 invalidRows: invalid,
131 attempts: attempts.size,
132 runs: { count: runOrder. length , latest: latestRun, scope: runFilter || (opts.sinceRun ? `after:${ opts . sinceRun }` : 'all' ) },
133 totals,
134 byEntity: sortedObject (byEntity),
135 topErrors,
136 filter: (opts.entity || opts.status) ? { entity: opts.entity, status: opts.status, matching, shown: sample. length , limit: opts.limit } : null ,
137 sample,
138 };
139 }
140
141 async function summarizeAudit ( filePath , options = {}) {
142 const opts = normalizeOptions (options);
143 const byEndpoint = new Map ();
144 const runs = new Set ();
145 const { rows , invalid } = await forEachRow (filePath, ( row ) => {
146 if (row.runId) runs. add (row.runId);
147 if (opts.run && opts.run !== 'latest' && row.runId !== opts.run) return ;
148 const endpoint = `${ row . method || ''} ${ row . endpoint || row . operation || 'unknown'}` . trim ();
149 const outcome = row.httpStatus ? String (row.httpStatus) : (row.result || row.status || 'unknown' );
150 bump (byEndpoint, endpoint, String (outcome));
151 });
152 const entries = [ ... byEndpoint. entries ()]
153 . map (([ endpoint , outcomes ]) => [endpoint, outcomes, Object. values (outcomes). reduce (( a , b ) => a + b, 0 )])
154 . sort (( a , b ) => b[ 2 ] - a[ 2 ]);
155 return {
156 ledger: 'audit' ,
157 file: filePath,
158 bytes: fs. statSync (filePath).size,
159 rows,
160 invalidRows: invalid,
161 runs: { count: runs.size },
162 byEndpoint: Object. fromEntries (entries. slice ( 0 , Math. max (opts.limit, 1 )). map (([ endpoint , outcomes ]) => [endpoint, outcomes])),
163 endpointsShown: Math. min (entries. length , Math. max (opts.limit, 1 )),
164 endpointsTotal: entries. length ,
165 };
166 }
167
168 async function summarizeWriteLog ( projectDir , options = {}) {
169 const ledger = options.log || 'attempts' ;
170 if ( ! LEDGERS [ledger]) throw new Error ( `unknown --log ${ ledger } (expected ${ Object . keys ( LEDGERS ). join ( ' or ' ) })` );
171 const filePath = path. join (projectDir, LEDGERS [ledger]);
172 if ( ! fs. existsSync (filePath)) {
173 return { ledger, file: filePath, missing: true , rows: 0 };
174 }
175 return ledger === 'audit' ? summarizeAudit (filePath, options) : summarizeAttempts (filePath, options);
176 }
177
178 const TEXT_CAP = 4096 ;
179
180 function formatText ( summary ) {
181 const lines = [];
182 if (summary.missing) return `${ summary . ledger } ledger not found: ${ summary . file } \n ` ;
183 lines. push ( `${ summary . ledger } ledger: ${ summary . rows } rows, ${ ( summary . bytes / 1e6 ). toFixed ( 1 ) } MB${ summary . invalidRows ? `, ${ summary . invalidRows } unreadable` : ''}` );
184 if (summary.ledger === 'audit' ) {
185 for ( const [ endpoint , outcomes ] of Object. entries (summary.byEndpoint)) {
186 lines. push ( ` ${ endpoint }: ${ Object . entries ( outcomes ). map (([ k , v ]) => `${ k }=${ v }` ). join ( ' ' ) }` );
187 }
188 if (summary.endpointsTotal > summary.endpointsShown) lines. push ( ` … ${ summary . endpointsTotal - summary . endpointsShown } more endpoints (raise --limit)` );
189 } else {
190 lines. push ( `attempts: ${ summary . attempts }; runs: ${ summary . runs . count } (latest ${ summary . runs . latest }); scope: ${ summary . runs . scope }` );
191 lines. push ( `totals: ${ Object . entries ( summary . totals ). map (([ k , v ]) => `${ k }=${ v }` ). join ( ' ' ) }` );
192 for ( const [ entity , statuses ] of Object. entries (summary.byEntity)) {
193 const errs = (summary.topErrors[entity] || []). map (( e ) => `${ e . errorClass }×${ e . count }` ). join ( ', ' );
194 lines. push ( ` ${ entity }: ${ Object . entries ( statuses ). map (([ k , v ]) => `${ k }=${ v }` ). join ( ' ' ) }${ errs ? ` | errors: ${ errs }` : ''}` );
195 }
196 if (summary.filter) {
197 lines. push ( `filter entity=${ summary . filter . entity || '*'} status=${ summary . filter . status || '*'}: ${ summary . filter . matching } match, showing ${ summary . filter . shown }` );
198 for ( const row of summary.sample) lines. push ( ` ${ JSON . stringify ( row ) }` );
199 }
200 }
201 let text = `${ lines . join ( ' \n ' ) } \n ` ;
202 if (Buffer. byteLength (text) > TEXT_CAP ) {
203 text = `${ Buffer . from ( text ). subarray ( 0 , TEXT_CAP - 80 ). toString ( 'utf8' ) } \n … output capped at ${ TEXT_CAP } bytes; narrow with --entity/--status/--limit \n ` ;
204 }
205 return text;
206 }
207
208 module . exports = { summarizeWriteLog, formatText, errorClassOf, LEDGERS, TEXT_CAP, DEFAULT_LIMIT };