Skip to content

Commit a5c2fd8

Browse files
authored
Merge pull request #496 from begd/fix/complete-errror-message
Observability: centralize error formatting + refine severity classification
2 parents 2787802 + 044a9fb commit a5c2fd8

File tree

3 files changed

+136
-28
lines changed

3 files changed

+136
-28
lines changed

indexer/src/kadena-server/server.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,7 @@ export async function startGraphqlServer() {
319319
for (const err of ctx.errors) {
320320
const path = Array.isArray(err.path) ? err.path.join('.') : '';
321321
// Log full message first so monitoring puts it in data.error; details go in extra.args
322-
console.error(`[ERROR][GRAPHQL] ${err.message}`, {
323-
operation: op,
324-
path,
325-
});
322+
console.error(`[ERROR][GRAPHQL] ${err.message}`, { operation: op, path });
326323
}
327324
} catch {
328325
// no-op

indexer/src/services/monitoring.ts

Lines changed: 133 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from 'axios';
2+
import os from 'os';
23

34
export type ReportErrorPayload = {
45
endpoint: string;
@@ -50,8 +51,10 @@ class HttpErrorReporter implements ErrorReporter {
5051
headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey },
5152
timeout: 30000,
5253
});
53-
} catch {
54-
console.error('[ERROR][MONITORING][REPORT_ERROR] Failed to report error:', error);
54+
} catch (error) {
55+
const err = error instanceof Error ? error : new Error(String(error));
56+
err.message = `[ERROR][MONITORING][REPORT_ERROR] Failed to report error: ${err.message}`;
57+
console.error(err);
5558
}
5659
}
5760
}
@@ -78,7 +81,10 @@ export function initializeErrorMonitoring(): void {
7881
const reporter = new HttpErrorReporter(monitoringUrl, apiKey);
7982
const originalConsoleError = console.error.bind(console);
8083

81-
const classifySeverity = (message: string, extra: unknown): 'major' | 'degraded' | 'minimal' => {
84+
const classifySeverity = (
85+
message: string,
86+
extra: unknown,
87+
): 'major' | 'degraded' | 'minimal' | 'none' => {
8288
const msg = (message || '').toLowerCase();
8389
const extraText = (() => {
8490
try {
@@ -127,33 +133,138 @@ export function initializeErrorMonitoring(): void {
127133
];
128134
if (degradedHints.some(h => hay.includes(h))) return 'degraded';
129135

130-
return 'minimal';
136+
// Minimal (Severity 3) — explicit low-impact signals per table
137+
const minimalHints = [
138+
// Minor parsing, non-standard cases
139+
'minor parsing',
140+
'non standard',
141+
'non-standard',
142+
// Occasional transient/slow behavior, small impact
143+
'transient',
144+
'slow query',
145+
'slow queries',
146+
'occasionally retry',
147+
'occasional retry',
148+
// Small gaps / less critical segments
149+
'small gaps',
150+
'less critical',
151+
'less-critical',
152+
// Logging/audit minor issues
153+
'minor formatting',
154+
'sporadic log gaps',
155+
'log gaps',
156+
'formatting issue',
157+
];
158+
if (minimalHints.some(h => hay.includes(h))) return 'minimal';
159+
160+
// Default fallback if no category matched
161+
return 'none';
131162
};
132163

133164
console.error = (...args: unknown[]) => {
134165
originalConsoleError(...args);
135166

167+
// Normalize message and extra to centralize formatting
168+
const firstError = args.find(a => a instanceof Error) as Error | undefined;
169+
const firstString = args.find(a => typeof a === 'string') as string | undefined;
170+
const allObjects = args.filter(
171+
a => a !== null && typeof a === 'object' && !(a instanceof Error),
172+
) as Record<string, unknown>[];
173+
136174
let message = 'Unknown error';
137-
let extra: unknown;
138-
139-
if (args.length > 0) {
140-
const first = args[0] as unknown;
141-
if (first instanceof Error) {
142-
message = first.message || 'Error';
143-
extra = { stack: first.stack, ...(args[1] as object) };
144-
} else if (typeof first === 'string') {
145-
message = first as string;
146-
if (args.length > 1) extra = { args: args.slice(1) };
147-
} else {
148-
try {
149-
message = JSON.stringify(first);
150-
} catch {
151-
message = String(first);
152-
}
153-
if (args.length > 1) extra = { args: args.slice(1) };
175+
let extra: Record<string, unknown> = {};
176+
// Merge all non-Error context objects (shallow)
177+
if (allObjects.length > 0) {
178+
try {
179+
extra = Object.assign({}, ...allObjects);
180+
} catch {
181+
// noop
154182
}
155183
}
156184

185+
if (firstError && firstString) {
186+
message = `${firstString}: ${firstError.message || 'Error'}`;
187+
if (firstError.stack) extra.stack = firstError.stack;
188+
} else if (firstError) {
189+
message = firstError.message || 'Error';
190+
if (firstError.stack) extra.stack = firstError.stack;
191+
} else if (firstString) {
192+
message = firstString;
193+
} else if (args.length > 0) {
194+
try {
195+
message = JSON.stringify(args[0]);
196+
} catch {
197+
message = String(args[0]);
198+
}
199+
if (args.length > 1) extra.args = args.slice(1) as unknown[];
200+
}
201+
202+
// Compute raw error message (without tags/prefixes)
203+
const rawErrorMessage = (() => {
204+
if (firstError?.message) return firstError.message;
205+
if (typeof firstString === 'string') {
206+
// Strip bracket tags like [ERROR][CACHE] from the beginning
207+
return firstString.replace(/^(?:\[[^\]]+\])+\s*/g, '').trim();
208+
}
209+
return typeof message === 'string' ? message : 'Error';
210+
})();
211+
212+
// Attach runtime/env info
213+
try {
214+
(extra as Record<string, unknown>)['pid'] = process.pid;
215+
(extra as Record<string, unknown>)['node'] = process.version;
216+
(extra as Record<string, unknown>)['host'] = os.hostname();
217+
(extra as Record<string, unknown>)['uptime'] = Math.round(process.uptime());
218+
} catch {}
219+
220+
// Derive callsite (function, file, line, column) from stack (error or synthetic)
221+
try {
222+
const stackSource = firstError?.stack || new Error().stack || '';
223+
const frames = stackSource.split('\n').map(s => s.trim());
224+
const frame = frames.find(
225+
f => f && !f.includes('services/monitoring') && (f.includes('at ') || f.includes('@')),
226+
);
227+
if (frame) {
228+
// Patterns: "at fn (file:line:col)" or "at file:line:col"
229+
const m = /at\s+(?:(?<fn>[^\s(]+)\s+\()?(?<loc>[^)]+)\)?/.exec(frame);
230+
const loc = m?.groups?.loc ?? '';
231+
const [filePath, line, column] = (() => {
232+
const parts = loc.split(':');
233+
if (parts.length >= 3) return [parts.slice(0, -2).join(':'), parts.at(-2), parts.at(-1)];
234+
return [loc, undefined, undefined];
235+
})();
236+
(extra as any).function = m?.groups?.fn ?? undefined;
237+
(extra as any).file = filePath;
238+
if (line) (extra as any).line = Number(line);
239+
if (column) (extra as any).column = Number(column);
240+
241+
// Phase derivation from file path
242+
const phase = (() => {
243+
const p = String(filePath || '').toLowerCase();
244+
if (p.includes('/cache/')) return 'cache';
245+
if (p.includes('/services/streaming')) return 'streaming';
246+
if (p.includes('/services/payload') || p.includes('/models/') || p.includes('sequelize'))
247+
return 'db';
248+
if (p.includes('/kadena-server/')) return 'graphql';
249+
if (p.includes('/services/price')) return 'price';
250+
if (p.includes('/services/missing')) return 'missing';
251+
if (p.includes('/services/define-canonical')) return 'canonical';
252+
if (p.includes('/services/guards')) return 'guards';
253+
return 'app';
254+
})();
255+
(extra as any).phase = phase;
256+
}
257+
} catch {}
258+
259+
// Derive tags from bracket prefixes in the string (if any)
260+
try {
261+
const tagSource = firstString || (typeof message === 'string' ? message : '');
262+
const tags = Array.from(tagSource.matchAll(/\[([^\]]+)\]/g))
263+
.map(m => m[1])
264+
.filter(Boolean);
265+
if (tags.length) (extra as any).tags = tags;
266+
} catch {}
267+
157268
// Ignore GraphQL-internal logs if requested (by tag)
158269
if (typeof message === 'string' && message.includes('[GRAPHQL]')) {
159270
return;
@@ -184,7 +295,7 @@ export function initializeErrorMonitoring(): void {
184295
instance,
185296
operation,
186297
error: message,
187-
extra: extraWithSeverity,
298+
extra: { ...(extraWithSeverity as Record<string, unknown>), message: rawErrorMessage },
188299
});
189300
};
190301
}

indexer/src/services/streaming.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export async function startStreaming() {
7373
// Handle connection errors
7474
eventSource.onerror = (error: any) => {
7575
// TODO: [RETRY-OPTIMIZATION] Consider adding retry/backoff or a reconnect strategy; at minimum emit a metric.
76-
console.error('[ERROR][NET][CONN_LOST] EventSource connection error', { error });
76+
console.error('[ERROR][NET][CONN_LOST] EventSource connection error', error);
7777
};
7878

7979
const processBlock = async (block: any) => {
@@ -270,7 +270,7 @@ export async function saveBlock(
270270
// Process the block's transactions and events
271271
return processPayloadKey(createdBlock, payloadData, tx);
272272
} catch (error) {
273-
console.error(`[ERROR][DB][DATA_CORRUPT] Failed to save block to database:`, error);
273+
console.error('[ERROR][DB][DATA_CORRUPT] Failed to save block to database:', error);
274274
return null;
275275
}
276276
}

0 commit comments

Comments
 (0)