trickle-observe 0.2.127 → 0.2.128
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/dist/hono.d.ts +41 -0
- package/dist/hono.js +396 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +12 -2
- package/dist/observe-register.js +44 -0
- package/package.json +2 -2
- package/src/hono.ts +403 -0
- package/src/index.ts +10 -1
- package/src/observe-register.ts +42 -0
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrument a Hono app by monkey-patching route registration methods.
|
|
3
|
+
*
|
|
4
|
+
* Must be called BEFORE routes are defined:
|
|
5
|
+
*
|
|
6
|
+
* import { Hono } from 'hono';
|
|
7
|
+
* import { instrumentHono } from 'trickle';
|
|
8
|
+
*
|
|
9
|
+
* const app = new Hono();
|
|
10
|
+
* instrumentHono(app);
|
|
11
|
+
*
|
|
12
|
+
* app.get('/api/users', (c) => c.json({ users: [] }));
|
|
13
|
+
*
|
|
14
|
+
* Captures:
|
|
15
|
+
* - Input: body (JSON), params, query from the Hono context
|
|
16
|
+
* - Output: the data passed to c.json() / c.text() or returned directly
|
|
17
|
+
* - Errors: exceptions thrown in handlers
|
|
18
|
+
* - Timing: request duration in milliseconds
|
|
19
|
+
*/
|
|
20
|
+
export declare function instrumentHono(app: any, userOpts?: {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
environment?: string;
|
|
23
|
+
sampleRate?: number;
|
|
24
|
+
maxDepth?: number;
|
|
25
|
+
}): void;
|
|
26
|
+
/**
|
|
27
|
+
* Hono middleware for observability. Use this as an alternative to
|
|
28
|
+
* monkey-patching route methods:
|
|
29
|
+
*
|
|
30
|
+
* import { Hono } from 'hono';
|
|
31
|
+
* import { trickleHonoMiddleware } from 'trickle';
|
|
32
|
+
*
|
|
33
|
+
* const app = new Hono();
|
|
34
|
+
* app.use('*', trickleHonoMiddleware());
|
|
35
|
+
*/
|
|
36
|
+
export declare function trickleHonoMiddleware(userOpts?: {
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
environment?: string;
|
|
39
|
+
sampleRate?: number;
|
|
40
|
+
maxDepth?: number;
|
|
41
|
+
}): (c: any, next: () => Promise<void>) => Promise<void | Response>;
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.instrumentHono = instrumentHono;
|
|
37
|
+
exports.trickleHonoMiddleware = trickleHonoMiddleware;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const pathMod = __importStar(require("path"));
|
|
40
|
+
const type_inference_1 = require("./type-inference");
|
|
41
|
+
const type_hash_1 = require("./type-hash");
|
|
42
|
+
const cache_1 = require("./cache");
|
|
43
|
+
const transport_1 = require("./transport");
|
|
44
|
+
const env_detect_1 = require("./env-detect");
|
|
45
|
+
const call_trace_1 = require("./call-trace");
|
|
46
|
+
const honoCache = new cache_1.TypeCache();
|
|
47
|
+
// ── Input extraction ──
|
|
48
|
+
async function extractHonoInput(c) {
|
|
49
|
+
const input = {};
|
|
50
|
+
try {
|
|
51
|
+
// Hono body: c.req.json() / c.req.text() — need to clone to avoid consuming
|
|
52
|
+
const contentType = c.req.header('content-type') || '';
|
|
53
|
+
if (contentType.includes('application/json')) {
|
|
54
|
+
try {
|
|
55
|
+
const body = await c.req.json();
|
|
56
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
57
|
+
input.body = body;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { /* body may not be parseable */ }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
try {
|
|
65
|
+
// c.req.param() returns all params as an object
|
|
66
|
+
const params = c.req.param();
|
|
67
|
+
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
|
|
68
|
+
input.params = params;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
try {
|
|
73
|
+
// c.req.query() returns all query params
|
|
74
|
+
const query = c.req.query();
|
|
75
|
+
if (query && typeof query === 'object' && Object.keys(query).length > 0) {
|
|
76
|
+
input.query = query;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
return input;
|
|
81
|
+
}
|
|
82
|
+
// ── Sample sanitization (local copy) ──
|
|
83
|
+
function sanitizeSample(value, depth = 3) {
|
|
84
|
+
if (depth <= 0)
|
|
85
|
+
return '[truncated]';
|
|
86
|
+
if (value === null || value === undefined)
|
|
87
|
+
return value;
|
|
88
|
+
const t = typeof value;
|
|
89
|
+
if (t === 'string') {
|
|
90
|
+
const s = value;
|
|
91
|
+
return s.length > 200 ? s.substring(0, 200) + '...' : s;
|
|
92
|
+
}
|
|
93
|
+
if (t === 'number' || t === 'boolean')
|
|
94
|
+
return value;
|
|
95
|
+
if (t === 'bigint')
|
|
96
|
+
return String(value);
|
|
97
|
+
if (t === 'function')
|
|
98
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
|
|
101
|
+
}
|
|
102
|
+
if (t === 'object') {
|
|
103
|
+
if (value instanceof Date)
|
|
104
|
+
return value.toISOString();
|
|
105
|
+
if (value instanceof Error)
|
|
106
|
+
return { error: value.message };
|
|
107
|
+
if (value instanceof Map)
|
|
108
|
+
return `[Map: ${value.size} entries]`;
|
|
109
|
+
if (value instanceof Set)
|
|
110
|
+
return `[Set: ${value.size} items]`;
|
|
111
|
+
const obj = value;
|
|
112
|
+
const result = {};
|
|
113
|
+
const keys = Object.keys(obj).slice(0, 20);
|
|
114
|
+
for (const key of keys) {
|
|
115
|
+
try {
|
|
116
|
+
result[key] = sanitizeSample(obj[key], depth - 1);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
result[key] = '[unreadable]';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
return String(value);
|
|
125
|
+
}
|
|
126
|
+
// ── Error file writing ──
|
|
127
|
+
function writeErrorToFile(error, input, routeName) {
|
|
128
|
+
try {
|
|
129
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
130
|
+
const defaultDir = pathMod.join(process.cwd(), '.trickle');
|
|
131
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
136
|
+
const stackLines = (err.stack || '').split('\n');
|
|
137
|
+
let errorFile;
|
|
138
|
+
let errorLine;
|
|
139
|
+
for (const sl of stackLines.slice(1)) {
|
|
140
|
+
const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
|
|
141
|
+
if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
|
|
142
|
+
errorFile = m[1];
|
|
143
|
+
errorLine = parseInt(m[2]);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const record = {
|
|
148
|
+
kind: 'error',
|
|
149
|
+
error: err.message,
|
|
150
|
+
type: err.constructor?.name || 'Error',
|
|
151
|
+
message: err.message,
|
|
152
|
+
file: errorFile,
|
|
153
|
+
line: errorLine,
|
|
154
|
+
stack: stackLines.slice(0, 6).join('\n'),
|
|
155
|
+
route: routeName,
|
|
156
|
+
request: input,
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
};
|
|
159
|
+
fs.appendFileSync(pathMod.join(dir, 'errors.jsonl'), JSON.stringify(record) + '\n');
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Never crash the user's app
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── Payload emission ──
|
|
166
|
+
function emitHonoPayload(functionName, environment, maxDepth, input, output, error, durationMs) {
|
|
167
|
+
try {
|
|
168
|
+
const functionKey = `hono::${functionName}`;
|
|
169
|
+
const argsType = (0, type_inference_1.inferType)(input, maxDepth);
|
|
170
|
+
const returnType = error ? { kind: 'unknown' } : (0, type_inference_1.inferType)(output, maxDepth);
|
|
171
|
+
const hash = (0, type_hash_1.hashType)(argsType, returnType);
|
|
172
|
+
if (!error && !honoCache.shouldSend(functionKey, hash))
|
|
173
|
+
return;
|
|
174
|
+
if (!error)
|
|
175
|
+
honoCache.markSent(functionKey, hash);
|
|
176
|
+
const payload = {
|
|
177
|
+
functionName,
|
|
178
|
+
module: 'hono',
|
|
179
|
+
language: 'js',
|
|
180
|
+
environment,
|
|
181
|
+
typeHash: hash,
|
|
182
|
+
argsType,
|
|
183
|
+
returnType,
|
|
184
|
+
sampleInput: sanitizeSample(input),
|
|
185
|
+
sampleOutput: error ? undefined : sanitizeSample(output),
|
|
186
|
+
};
|
|
187
|
+
if (durationMs !== undefined) {
|
|
188
|
+
payload.durationMs = Math.round(durationMs * 100) / 100;
|
|
189
|
+
}
|
|
190
|
+
if (error) {
|
|
191
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
192
|
+
payload.error = {
|
|
193
|
+
type: err.constructor?.name || 'Error',
|
|
194
|
+
message: err.message,
|
|
195
|
+
stackTrace: err.stack,
|
|
196
|
+
argsSnapshot: sanitizeSample(input),
|
|
197
|
+
};
|
|
198
|
+
writeErrorToFile(error, input, functionName);
|
|
199
|
+
}
|
|
200
|
+
(0, transport_1.enqueue)(payload);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Never crash the user's app
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ── Public API ──
|
|
207
|
+
/**
|
|
208
|
+
* Instrument a Hono app by monkey-patching route registration methods.
|
|
209
|
+
*
|
|
210
|
+
* Must be called BEFORE routes are defined:
|
|
211
|
+
*
|
|
212
|
+
* import { Hono } from 'hono';
|
|
213
|
+
* import { instrumentHono } from 'trickle';
|
|
214
|
+
*
|
|
215
|
+
* const app = new Hono();
|
|
216
|
+
* instrumentHono(app);
|
|
217
|
+
*
|
|
218
|
+
* app.get('/api/users', (c) => c.json({ users: [] }));
|
|
219
|
+
*
|
|
220
|
+
* Captures:
|
|
221
|
+
* - Input: body (JSON), params, query from the Hono context
|
|
222
|
+
* - Output: the data passed to c.json() / c.text() or returned directly
|
|
223
|
+
* - Errors: exceptions thrown in handlers
|
|
224
|
+
* - Timing: request duration in milliseconds
|
|
225
|
+
*/
|
|
226
|
+
function instrumentHono(app, userOpts) {
|
|
227
|
+
const opts = {
|
|
228
|
+
enabled: userOpts?.enabled !== false,
|
|
229
|
+
environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
|
|
230
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
231
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
232
|
+
};
|
|
233
|
+
if (!opts.enabled)
|
|
234
|
+
return;
|
|
235
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch', 'all', 'options', 'head'];
|
|
236
|
+
for (const method of methods) {
|
|
237
|
+
const original = app[method];
|
|
238
|
+
if (typeof original !== 'function')
|
|
239
|
+
continue;
|
|
240
|
+
app[method] = function (path, ...handlers) {
|
|
241
|
+
const pathStr = typeof path === 'string' ? path : String(path);
|
|
242
|
+
const routeName = `${method.toUpperCase()} ${pathStr}`;
|
|
243
|
+
const wrapped = handlers.map((handler) => {
|
|
244
|
+
if (typeof handler !== 'function')
|
|
245
|
+
return handler;
|
|
246
|
+
return wrapHonoHandler(handler, routeName, opts);
|
|
247
|
+
});
|
|
248
|
+
return original.call(this, path, ...wrapped);
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function wrapHonoHandler(handler, routeName, opts) {
|
|
253
|
+
const wrapped = async function (c, next) {
|
|
254
|
+
// Sample rate check
|
|
255
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
|
|
256
|
+
return handler.call(this, c, next);
|
|
257
|
+
}
|
|
258
|
+
const startTime = performance.now();
|
|
259
|
+
const callId = (0, call_trace_1.traceCall)(routeName, 'hono');
|
|
260
|
+
// Extract input (async because body parsing is async in Hono)
|
|
261
|
+
let input = {};
|
|
262
|
+
try {
|
|
263
|
+
input = await extractHonoInput(c);
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
// Intercept c.json() to capture output
|
|
267
|
+
let captured = false;
|
|
268
|
+
const originalJson = c.json;
|
|
269
|
+
if (typeof originalJson === 'function') {
|
|
270
|
+
c.json = function (data, ...args) {
|
|
271
|
+
if (!captured) {
|
|
272
|
+
captured = true;
|
|
273
|
+
const durationMs = performance.now() - startTime;
|
|
274
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
|
|
275
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, data, undefined, durationMs);
|
|
276
|
+
}
|
|
277
|
+
return originalJson.call(c, data, ...args);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Intercept c.text() for text responses
|
|
281
|
+
const originalText = c.text;
|
|
282
|
+
if (typeof originalText === 'function') {
|
|
283
|
+
c.text = function (data, ...args) {
|
|
284
|
+
if (!captured) {
|
|
285
|
+
captured = true;
|
|
286
|
+
const durationMs = performance.now() - startTime;
|
|
287
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
|
|
288
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __text: true }, undefined, durationMs);
|
|
289
|
+
}
|
|
290
|
+
return originalText.call(c, data, ...args);
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const result = await handler.call(this, c, next);
|
|
295
|
+
// Hono handlers can return a Response object directly
|
|
296
|
+
if (result && !captured) {
|
|
297
|
+
captured = true;
|
|
298
|
+
const durationMs = performance.now() - startTime;
|
|
299
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
|
|
300
|
+
// Try to extract JSON from the Response
|
|
301
|
+
if (result instanceof Response) {
|
|
302
|
+
try {
|
|
303
|
+
const cloned = result.clone();
|
|
304
|
+
const ct = cloned.headers.get('content-type') || '';
|
|
305
|
+
if (ct.includes('application/json')) {
|
|
306
|
+
const body = await cloned.json();
|
|
307
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, result, undefined, durationMs);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
if (!captured) {
|
|
325
|
+
captured = true;
|
|
326
|
+
const durationMs = performance.now() - startTime;
|
|
327
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
|
|
328
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
|
|
329
|
+
}
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
|
|
334
|
+
Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
|
|
335
|
+
return wrapped;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Hono middleware for observability. Use this as an alternative to
|
|
339
|
+
* monkey-patching route methods:
|
|
340
|
+
*
|
|
341
|
+
* import { Hono } from 'hono';
|
|
342
|
+
* import { trickleHonoMiddleware } from 'trickle';
|
|
343
|
+
*
|
|
344
|
+
* const app = new Hono();
|
|
345
|
+
* app.use('*', trickleHonoMiddleware());
|
|
346
|
+
*/
|
|
347
|
+
function trickleHonoMiddleware(userOpts) {
|
|
348
|
+
const opts = {
|
|
349
|
+
enabled: userOpts?.enabled !== false,
|
|
350
|
+
environment: userOpts?.environment || (0, env_detect_1.detectEnvironment)(),
|
|
351
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
352
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
353
|
+
};
|
|
354
|
+
return async function trickleHonoMw(c, next) {
|
|
355
|
+
if (!opts.enabled)
|
|
356
|
+
return next();
|
|
357
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate)
|
|
358
|
+
return next();
|
|
359
|
+
const startTime = performance.now();
|
|
360
|
+
const routeName = `${c.req.method} ${c.req.path}`;
|
|
361
|
+
const callId = (0, call_trace_1.traceCall)(routeName, 'hono');
|
|
362
|
+
let input = {};
|
|
363
|
+
try {
|
|
364
|
+
input = await extractHonoInput(c);
|
|
365
|
+
}
|
|
366
|
+
catch { }
|
|
367
|
+
try {
|
|
368
|
+
await next();
|
|
369
|
+
const durationMs = performance.now() - startTime;
|
|
370
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs);
|
|
371
|
+
// After next(), capture from c.res if available
|
|
372
|
+
if (c.res) {
|
|
373
|
+
try {
|
|
374
|
+
const ct = c.res.headers?.get('content-type') || '';
|
|
375
|
+
if (ct.includes('application/json')) {
|
|
376
|
+
const cloned = c.res.clone();
|
|
377
|
+
const body = await cloned.json();
|
|
378
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
const durationMs = performance.now() - startTime;
|
|
391
|
+
(0, call_trace_1.traceReturn)(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
|
|
392
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
|
|
393
|
+
throw err;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function trickleExpress(app: any, opts?: {
|
|
|
42
42
|
maxDepth?: number;
|
|
43
43
|
}): void;
|
|
44
44
|
/**
|
|
45
|
-
* Auto-instrument a framework app. Supports Express, Fastify, and
|
|
45
|
+
* Auto-instrument a framework app. Supports Express, Fastify, Koa, and Hono.
|
|
46
46
|
*
|
|
47
47
|
* Usage:
|
|
48
48
|
* import { instrument } from 'trickle';
|
|
@@ -64,6 +64,7 @@ export { flush } from './transport';
|
|
|
64
64
|
export { instrumentExpress, trickleMiddleware } from './express';
|
|
65
65
|
export { instrumentFastify, tricklePlugin } from './fastify';
|
|
66
66
|
export { instrumentKoa, instrumentKoaRouter } from './koa';
|
|
67
|
+
export { instrumentHono, trickleHonoMiddleware } from './hono';
|
|
67
68
|
export { observe, observeFn } from './observe';
|
|
68
69
|
export type { ObserveOpts } from './observe';
|
|
69
70
|
export { wrapFunction } from './wrap';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.wrapFunction = exports.observeFn = exports.observe = exports.instrumentKoaRouter = exports.instrumentKoa = exports.tricklePlugin = exports.instrumentFastify = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
|
|
3
|
+
exports.wrapFunction = exports.observeFn = exports.observe = exports.trickleHonoMiddleware = exports.instrumentHono = exports.instrumentKoaRouter = exports.instrumentKoa = exports.tricklePlugin = exports.instrumentFastify = exports.trickleMiddleware = exports.instrumentExpress = exports.flush = void 0;
|
|
4
4
|
exports.configure = configure;
|
|
5
5
|
exports.trickle = trickle;
|
|
6
6
|
exports.trickleHandler = trickleHandler;
|
|
@@ -12,6 +12,7 @@ const env_detect_1 = require("./env-detect");
|
|
|
12
12
|
const express_1 = require("./express");
|
|
13
13
|
const fastify_1 = require("./fastify");
|
|
14
14
|
const koa_1 = require("./koa");
|
|
15
|
+
const hono_1 = require("./hono");
|
|
15
16
|
let globalOpts = {
|
|
16
17
|
backendUrl: 'http://localhost:4888',
|
|
17
18
|
batchIntervalMs: 2000,
|
|
@@ -110,7 +111,7 @@ function trickleExpress(app, opts) {
|
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
/**
|
|
113
|
-
* Auto-instrument a framework app. Supports Express, Fastify, and
|
|
114
|
+
* Auto-instrument a framework app. Supports Express, Fastify, Koa, and Hono.
|
|
114
115
|
*
|
|
115
116
|
* Usage:
|
|
116
117
|
* import { instrument } from 'trickle';
|
|
@@ -134,6 +135,12 @@ function instrument(app, opts) {
|
|
|
134
135
|
sampleRate: opts?.sampleRate ?? 1,
|
|
135
136
|
maxDepth: opts?.maxDepth ?? 5,
|
|
136
137
|
};
|
|
138
|
+
// Detect Hono: has .fetch (bound method), .route(), .get(), but NOT .listen() on the app itself
|
|
139
|
+
// Hono apps use serve() from @hono/node-server rather than app.listen()
|
|
140
|
+
if (typeof app.fetch === 'function' && typeof app.get === 'function' && typeof app.route === 'function' && typeof app.fire === 'function') {
|
|
141
|
+
(0, hono_1.instrumentHono)(app, mergedOpts);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
137
144
|
// Detect Fastify: has .route(), .register(), .addHook()
|
|
138
145
|
if (typeof app.route === 'function' && typeof app.register === 'function' && typeof app.addHook === 'function') {
|
|
139
146
|
(0, fastify_1.instrumentFastify)(app, mergedOpts);
|
|
@@ -199,6 +206,9 @@ Object.defineProperty(exports, "tricklePlugin", { enumerable: true, get: functio
|
|
|
199
206
|
var koa_2 = require("./koa");
|
|
200
207
|
Object.defineProperty(exports, "instrumentKoa", { enumerable: true, get: function () { return koa_2.instrumentKoa; } });
|
|
201
208
|
Object.defineProperty(exports, "instrumentKoaRouter", { enumerable: true, get: function () { return koa_2.instrumentKoaRouter; } });
|
|
209
|
+
var hono_2 = require("./hono");
|
|
210
|
+
Object.defineProperty(exports, "instrumentHono", { enumerable: true, get: function () { return hono_2.instrumentHono; } });
|
|
211
|
+
Object.defineProperty(exports, "trickleHonoMiddleware", { enumerable: true, get: function () { return hono_2.trickleHonoMiddleware; } });
|
|
202
212
|
var observe_1 = require("./observe");
|
|
203
213
|
Object.defineProperty(exports, "observe", { enumerable: true, get: function () { return observe_1.observe; } });
|
|
204
214
|
Object.defineProperty(exports, "observeFn", { enumerable: true, get: function () { return observe_1.observeFn; } });
|
package/dist/observe-register.js
CHANGED
|
@@ -41,6 +41,7 @@ const fetch_observer_1 = require("./fetch-observer");
|
|
|
41
41
|
const express_1 = require("./express");
|
|
42
42
|
const fastify_1 = require("./fastify");
|
|
43
43
|
const koa_1 = require("./koa");
|
|
44
|
+
const hono_1 = require("./hono");
|
|
44
45
|
const trace_var_1 = require("./trace-var");
|
|
45
46
|
const call_trace_1 = require("./call-trace");
|
|
46
47
|
const llm_observer_1 = require("./llm-observer");
|
|
@@ -1502,6 +1503,49 @@ if (enabled) {
|
|
|
1502
1503
|
}
|
|
1503
1504
|
catch { /* fall through */ }
|
|
1504
1505
|
}
|
|
1506
|
+
// ── Hono auto-detection ──
|
|
1507
|
+
if (request === 'hono' && !expressPatched.has('hono')) {
|
|
1508
|
+
expressPatched.add('hono');
|
|
1509
|
+
try {
|
|
1510
|
+
const honoMod = exports;
|
|
1511
|
+
const HonoClass = honoMod.Hono || (honoMod.default && honoMod.default.Hono);
|
|
1512
|
+
if (HonoClass && typeof HonoClass === 'function') {
|
|
1513
|
+
const OrigHono = HonoClass;
|
|
1514
|
+
const WrappedHono = function (...args) {
|
|
1515
|
+
const app = new OrigHono(...args);
|
|
1516
|
+
try {
|
|
1517
|
+
(0, hono_1.instrumentHono)(app, { environment });
|
|
1518
|
+
if (debug) {
|
|
1519
|
+
console.log('[trickle/observe] Auto-instrumented Hono app');
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
catch (e) {
|
|
1523
|
+
if (debug) {
|
|
1524
|
+
console.log('[trickle/observe] Hono instrumentation error:', e.message);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return app;
|
|
1528
|
+
};
|
|
1529
|
+
WrappedHono.prototype = OrigHono.prototype;
|
|
1530
|
+
for (const key of Object.keys(OrigHono)) {
|
|
1531
|
+
WrappedHono[key] = OrigHono[key];
|
|
1532
|
+
}
|
|
1533
|
+
if (honoMod.Hono) {
|
|
1534
|
+
honoMod.Hono = WrappedHono;
|
|
1535
|
+
}
|
|
1536
|
+
try {
|
|
1537
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
1538
|
+
if (require.cache[resolvedPath]) {
|
|
1539
|
+
const cached = require.cache[resolvedPath].exports;
|
|
1540
|
+
if (cached.Hono)
|
|
1541
|
+
cached.Hono = WrappedHono;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
catch { /* non-critical */ }
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
catch { /* fall through */ }
|
|
1548
|
+
}
|
|
1505
1549
|
// ── Database auto-detection: patch database drivers to capture SQL queries ──
|
|
1506
1550
|
if (request === 'pg' && !expressPatched.has('pg')) {
|
|
1507
1551
|
expressPatched.add('pg');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trickle-observe",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Zero-code runtime observability for JavaScript/TypeScript. Auto-instruments Express, Fastify, Koa, OpenAI, Anthropic, Gemini, MCP. Captures functions, variables, LLM calls, agent workflows.",
|
|
3
|
+
"version": "0.2.128",
|
|
4
|
+
"description": "Zero-code runtime observability for JavaScript/TypeScript. Auto-instruments Express, Fastify, Koa, Hono, OpenAI, Anthropic, Gemini, MCP. Captures functions, variables, LLM calls, agent workflows.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
package/src/hono.ts
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as pathMod from 'path';
|
|
3
|
+
import { TypeNode, IngestPayload } from './types';
|
|
4
|
+
import { inferType } from './type-inference';
|
|
5
|
+
import { hashType } from './type-hash';
|
|
6
|
+
import { TypeCache } from './cache';
|
|
7
|
+
import { enqueue } from './transport';
|
|
8
|
+
import { detectEnvironment } from './env-detect';
|
|
9
|
+
import { withRequestContext } from './request-context';
|
|
10
|
+
import { traceCall, traceReturn } from './call-trace';
|
|
11
|
+
|
|
12
|
+
const honoCache = new TypeCache();
|
|
13
|
+
|
|
14
|
+
interface HonoInstrumentOpts {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
environment: string;
|
|
17
|
+
sampleRate: number;
|
|
18
|
+
maxDepth: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── Input extraction ──
|
|
22
|
+
|
|
23
|
+
async function extractHonoInput(c: any): Promise<Record<string, unknown>> {
|
|
24
|
+
const input: Record<string, unknown> = {};
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Hono body: c.req.json() / c.req.text() — need to clone to avoid consuming
|
|
28
|
+
const contentType = c.req.header('content-type') || '';
|
|
29
|
+
if (contentType.includes('application/json')) {
|
|
30
|
+
try {
|
|
31
|
+
const body = await c.req.json();
|
|
32
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
33
|
+
input.body = body;
|
|
34
|
+
}
|
|
35
|
+
} catch { /* body may not be parseable */ }
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// c.req.param() returns all params as an object
|
|
41
|
+
const params = c.req.param();
|
|
42
|
+
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
|
|
43
|
+
input.params = params;
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// c.req.query() returns all query params
|
|
49
|
+
const query = c.req.query();
|
|
50
|
+
if (query && typeof query === 'object' && Object.keys(query).length > 0) {
|
|
51
|
+
input.query = query;
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
|
|
55
|
+
return input;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Sample sanitization (local copy) ──
|
|
59
|
+
|
|
60
|
+
function sanitizeSample(value: unknown, depth: number = 3): unknown {
|
|
61
|
+
if (depth <= 0) return '[truncated]';
|
|
62
|
+
if (value === null || value === undefined) return value;
|
|
63
|
+
|
|
64
|
+
const t = typeof value;
|
|
65
|
+
if (t === 'string') {
|
|
66
|
+
const s = value as string;
|
|
67
|
+
return s.length > 200 ? s.substring(0, 200) + '...' : s;
|
|
68
|
+
}
|
|
69
|
+
if (t === 'number' || t === 'boolean') return value;
|
|
70
|
+
if (t === 'bigint') return String(value);
|
|
71
|
+
if (t === 'function') return `[Function: ${(value as Function).name || 'anonymous'}]`;
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (t === 'object') {
|
|
78
|
+
if (value instanceof Date) return value.toISOString();
|
|
79
|
+
if (value instanceof Error) return { error: value.message };
|
|
80
|
+
if (value instanceof Map) return `[Map: ${value.size} entries]`;
|
|
81
|
+
if (value instanceof Set) return `[Set: ${value.size} items]`;
|
|
82
|
+
|
|
83
|
+
const obj = value as Record<string, unknown>;
|
|
84
|
+
const result: Record<string, unknown> = {};
|
|
85
|
+
const keys = Object.keys(obj).slice(0, 20);
|
|
86
|
+
for (const key of keys) {
|
|
87
|
+
try {
|
|
88
|
+
result[key] = sanitizeSample(obj[key], depth - 1);
|
|
89
|
+
} catch {
|
|
90
|
+
result[key] = '[unreadable]';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Error file writing ──
|
|
100
|
+
|
|
101
|
+
function writeErrorToFile(error: unknown, input: Record<string, unknown>, routeName: string): void {
|
|
102
|
+
try {
|
|
103
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
104
|
+
const defaultDir = pathMod.join(process.cwd(), '.trickle');
|
|
105
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || defaultDir;
|
|
106
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
107
|
+
|
|
108
|
+
const stackLines = (err.stack || '').split('\n');
|
|
109
|
+
let errorFile: string | undefined;
|
|
110
|
+
let errorLine: number | undefined;
|
|
111
|
+
for (const sl of stackLines.slice(1)) {
|
|
112
|
+
const m = sl.match(/\((.+):(\d+):\d+\)/) || sl.match(/at (.+):(\d+):\d+/);
|
|
113
|
+
if (m && !m[1].includes('node_modules') && !m[1].includes('node:') && !m[1].includes('trickle-observe')) {
|
|
114
|
+
errorFile = m[1];
|
|
115
|
+
errorLine = parseInt(m[2]);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const record = {
|
|
121
|
+
kind: 'error',
|
|
122
|
+
error: err.message,
|
|
123
|
+
type: err.constructor?.name || 'Error',
|
|
124
|
+
message: err.message,
|
|
125
|
+
file: errorFile,
|
|
126
|
+
line: errorLine,
|
|
127
|
+
stack: stackLines.slice(0, 6).join('\n'),
|
|
128
|
+
route: routeName,
|
|
129
|
+
request: input,
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
fs.appendFileSync(pathMod.join(dir, 'errors.jsonl'), JSON.stringify(record) + '\n');
|
|
134
|
+
} catch {
|
|
135
|
+
// Never crash the user's app
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Payload emission ──
|
|
140
|
+
|
|
141
|
+
function emitHonoPayload(
|
|
142
|
+
functionName: string,
|
|
143
|
+
environment: string,
|
|
144
|
+
maxDepth: number,
|
|
145
|
+
input: Record<string, unknown>,
|
|
146
|
+
output: unknown,
|
|
147
|
+
error?: unknown,
|
|
148
|
+
durationMs?: number,
|
|
149
|
+
): void {
|
|
150
|
+
try {
|
|
151
|
+
const functionKey = `hono::${functionName}`;
|
|
152
|
+
const argsType = inferType(input, maxDepth);
|
|
153
|
+
const returnType = error ? ({ kind: 'unknown' } as TypeNode) : inferType(output, maxDepth);
|
|
154
|
+
const hash = hashType(argsType, returnType);
|
|
155
|
+
|
|
156
|
+
if (!error && !honoCache.shouldSend(functionKey, hash)) return;
|
|
157
|
+
if (!error) honoCache.markSent(functionKey, hash);
|
|
158
|
+
|
|
159
|
+
const payload: IngestPayload = {
|
|
160
|
+
functionName,
|
|
161
|
+
module: 'hono',
|
|
162
|
+
language: 'js',
|
|
163
|
+
environment,
|
|
164
|
+
typeHash: hash,
|
|
165
|
+
argsType,
|
|
166
|
+
returnType,
|
|
167
|
+
sampleInput: sanitizeSample(input),
|
|
168
|
+
sampleOutput: error ? undefined : sanitizeSample(output),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (durationMs !== undefined) {
|
|
172
|
+
payload.durationMs = Math.round(durationMs * 100) / 100;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (error) {
|
|
176
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
177
|
+
payload.error = {
|
|
178
|
+
type: err.constructor?.name || 'Error',
|
|
179
|
+
message: err.message,
|
|
180
|
+
stackTrace: err.stack,
|
|
181
|
+
argsSnapshot: sanitizeSample(input),
|
|
182
|
+
};
|
|
183
|
+
writeErrorToFile(error, input, functionName);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
enqueue(payload);
|
|
187
|
+
} catch {
|
|
188
|
+
// Never crash the user's app
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Public API ──
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Instrument a Hono app by monkey-patching route registration methods.
|
|
196
|
+
*
|
|
197
|
+
* Must be called BEFORE routes are defined:
|
|
198
|
+
*
|
|
199
|
+
* import { Hono } from 'hono';
|
|
200
|
+
* import { instrumentHono } from 'trickle';
|
|
201
|
+
*
|
|
202
|
+
* const app = new Hono();
|
|
203
|
+
* instrumentHono(app);
|
|
204
|
+
*
|
|
205
|
+
* app.get('/api/users', (c) => c.json({ users: [] }));
|
|
206
|
+
*
|
|
207
|
+
* Captures:
|
|
208
|
+
* - Input: body (JSON), params, query from the Hono context
|
|
209
|
+
* - Output: the data passed to c.json() / c.text() or returned directly
|
|
210
|
+
* - Errors: exceptions thrown in handlers
|
|
211
|
+
* - Timing: request duration in milliseconds
|
|
212
|
+
*/
|
|
213
|
+
export function instrumentHono(
|
|
214
|
+
app: any,
|
|
215
|
+
userOpts?: { enabled?: boolean; environment?: string; sampleRate?: number; maxDepth?: number },
|
|
216
|
+
): void {
|
|
217
|
+
const opts: HonoInstrumentOpts = {
|
|
218
|
+
enabled: userOpts?.enabled !== false,
|
|
219
|
+
environment: userOpts?.environment || detectEnvironment(),
|
|
220
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
221
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (!opts.enabled) return;
|
|
225
|
+
|
|
226
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch', 'all', 'options', 'head'] as const;
|
|
227
|
+
|
|
228
|
+
for (const method of methods) {
|
|
229
|
+
const original = app[method];
|
|
230
|
+
if (typeof original !== 'function') continue;
|
|
231
|
+
|
|
232
|
+
app[method] = function (this: any, path: string, ...handlers: any[]) {
|
|
233
|
+
const pathStr = typeof path === 'string' ? path : String(path);
|
|
234
|
+
const routeName = `${method.toUpperCase()} ${pathStr}`;
|
|
235
|
+
|
|
236
|
+
const wrapped = handlers.map((handler: any) => {
|
|
237
|
+
if (typeof handler !== 'function') return handler;
|
|
238
|
+
|
|
239
|
+
return wrapHonoHandler(handler, routeName, opts);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return original.call(this, path, ...wrapped);
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function wrapHonoHandler(
|
|
248
|
+
handler: Function,
|
|
249
|
+
routeName: string,
|
|
250
|
+
opts: HonoInstrumentOpts,
|
|
251
|
+
): Function {
|
|
252
|
+
const wrapped = async function (this: any, c: any, next?: any) {
|
|
253
|
+
// Sample rate check
|
|
254
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) {
|
|
255
|
+
return handler.call(this, c, next);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const startTime = performance.now();
|
|
259
|
+
const callId = traceCall(routeName, 'hono');
|
|
260
|
+
|
|
261
|
+
// Extract input (async because body parsing is async in Hono)
|
|
262
|
+
let input: Record<string, unknown> = {};
|
|
263
|
+
try {
|
|
264
|
+
input = await extractHonoInput(c);
|
|
265
|
+
} catch {}
|
|
266
|
+
|
|
267
|
+
// Intercept c.json() to capture output
|
|
268
|
+
let captured = false;
|
|
269
|
+
const originalJson = c.json;
|
|
270
|
+
if (typeof originalJson === 'function') {
|
|
271
|
+
c.json = function (data: any, ...args: any[]) {
|
|
272
|
+
if (!captured) {
|
|
273
|
+
captured = true;
|
|
274
|
+
const durationMs = performance.now() - startTime;
|
|
275
|
+
traceReturn(callId, routeName, 'hono', durationMs);
|
|
276
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, data, undefined, durationMs);
|
|
277
|
+
}
|
|
278
|
+
return originalJson.call(c, data, ...args);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Intercept c.text() for text responses
|
|
283
|
+
const originalText = c.text;
|
|
284
|
+
if (typeof originalText === 'function') {
|
|
285
|
+
c.text = function (data: any, ...args: any[]) {
|
|
286
|
+
if (!captured) {
|
|
287
|
+
captured = true;
|
|
288
|
+
const durationMs = performance.now() - startTime;
|
|
289
|
+
traceReturn(callId, routeName, 'hono', durationMs);
|
|
290
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __text: true }, undefined, durationMs);
|
|
291
|
+
}
|
|
292
|
+
return originalText.call(c, data, ...args);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const result = await handler.call(this, c, next);
|
|
298
|
+
|
|
299
|
+
// Hono handlers can return a Response object directly
|
|
300
|
+
if (result && !captured) {
|
|
301
|
+
captured = true;
|
|
302
|
+
const durationMs = performance.now() - startTime;
|
|
303
|
+
traceReturn(callId, routeName, 'hono', durationMs);
|
|
304
|
+
|
|
305
|
+
// Try to extract JSON from the Response
|
|
306
|
+
if (result instanceof Response) {
|
|
307
|
+
try {
|
|
308
|
+
const cloned = result.clone();
|
|
309
|
+
const ct = cloned.headers.get('content-type') || '';
|
|
310
|
+
if (ct.includes('application/json')) {
|
|
311
|
+
const body = await cloned.json();
|
|
312
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
|
|
313
|
+
} else {
|
|
314
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, result, undefined, durationMs);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return result;
|
|
325
|
+
} catch (err) {
|
|
326
|
+
if (!captured) {
|
|
327
|
+
captured = true;
|
|
328
|
+
const durationMs = performance.now() - startTime;
|
|
329
|
+
traceReturn(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
|
|
330
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
|
|
331
|
+
}
|
|
332
|
+
throw err;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
Object.defineProperty(wrapped, 'name', { value: handler.name || routeName, configurable: true });
|
|
337
|
+
Object.defineProperty(wrapped, 'length', { value: handler.length, configurable: true });
|
|
338
|
+
|
|
339
|
+
return wrapped;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Hono middleware for observability. Use this as an alternative to
|
|
344
|
+
* monkey-patching route methods:
|
|
345
|
+
*
|
|
346
|
+
* import { Hono } from 'hono';
|
|
347
|
+
* import { trickleHonoMiddleware } from 'trickle';
|
|
348
|
+
*
|
|
349
|
+
* const app = new Hono();
|
|
350
|
+
* app.use('*', trickleHonoMiddleware());
|
|
351
|
+
*/
|
|
352
|
+
export function trickleHonoMiddleware(
|
|
353
|
+
userOpts?: { enabled?: boolean; environment?: string; sampleRate?: number; maxDepth?: number },
|
|
354
|
+
): (c: any, next: () => Promise<void>) => Promise<void | Response> {
|
|
355
|
+
const opts: HonoInstrumentOpts = {
|
|
356
|
+
enabled: userOpts?.enabled !== false,
|
|
357
|
+
environment: userOpts?.environment || detectEnvironment(),
|
|
358
|
+
sampleRate: userOpts?.sampleRate ?? 1,
|
|
359
|
+
maxDepth: userOpts?.maxDepth ?? 5,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return async function trickleHonoMw(c: any, next: () => Promise<void>): Promise<void | Response> {
|
|
363
|
+
if (!opts.enabled) return next();
|
|
364
|
+
if (opts.sampleRate < 1 && Math.random() > opts.sampleRate) return next();
|
|
365
|
+
|
|
366
|
+
const startTime = performance.now();
|
|
367
|
+
const routeName = `${c.req.method} ${c.req.path}`;
|
|
368
|
+
const callId = traceCall(routeName, 'hono');
|
|
369
|
+
|
|
370
|
+
let input: Record<string, unknown> = {};
|
|
371
|
+
try {
|
|
372
|
+
input = await extractHonoInput(c);
|
|
373
|
+
} catch {}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
await next();
|
|
377
|
+
|
|
378
|
+
const durationMs = performance.now() - startTime;
|
|
379
|
+
traceReturn(callId, routeName, 'hono', durationMs);
|
|
380
|
+
|
|
381
|
+
// After next(), capture from c.res if available
|
|
382
|
+
if (c.res) {
|
|
383
|
+
try {
|
|
384
|
+
const ct = c.res.headers?.get('content-type') || '';
|
|
385
|
+
if (ct.includes('application/json')) {
|
|
386
|
+
const cloned = c.res.clone();
|
|
387
|
+
const body = await cloned.json();
|
|
388
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, body, undefined, durationMs);
|
|
389
|
+
} else {
|
|
390
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, { __response: true }, undefined, durationMs);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
const durationMs = performance.now() - startTime;
|
|
398
|
+
traceReturn(callId, routeName, 'hono', durationMs, (err instanceof Error ? err : new Error(String(err))).message);
|
|
399
|
+
emitHonoPayload(routeName, opts.environment, opts.maxDepth, input, undefined, err, durationMs);
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { GlobalOpts, TrickleOpts, WrapOptions } from './types';
|
|
|
5
5
|
import { instrumentExpress, trickleMiddleware } from './express';
|
|
6
6
|
import { instrumentFastify, tricklePlugin } from './fastify';
|
|
7
7
|
import { instrumentKoa, instrumentKoaRouter } from './koa';
|
|
8
|
+
import { instrumentHono, trickleHonoMiddleware } from './hono';
|
|
8
9
|
|
|
9
10
|
let globalOpts: GlobalOpts = {
|
|
10
11
|
backendUrl: 'http://localhost:4888',
|
|
@@ -135,7 +136,7 @@ export function trickleExpress(
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
138
|
-
* Auto-instrument a framework app. Supports Express, Fastify, and
|
|
139
|
+
* Auto-instrument a framework app. Supports Express, Fastify, Koa, and Hono.
|
|
139
140
|
*
|
|
140
141
|
* Usage:
|
|
141
142
|
* import { instrument } from 'trickle';
|
|
@@ -164,6 +165,13 @@ export function instrument(
|
|
|
164
165
|
maxDepth: opts?.maxDepth ?? 5,
|
|
165
166
|
};
|
|
166
167
|
|
|
168
|
+
// Detect Hono: has .fetch (bound method), .route(), .get(), but NOT .listen() on the app itself
|
|
169
|
+
// Hono apps use serve() from @hono/node-server rather than app.listen()
|
|
170
|
+
if (typeof app.fetch === 'function' && typeof app.get === 'function' && typeof app.route === 'function' && typeof app.fire === 'function') {
|
|
171
|
+
instrumentHono(app, mergedOpts);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
167
175
|
// Detect Fastify: has .route(), .register(), .addHook()
|
|
168
176
|
if (typeof app.route === 'function' && typeof app.register === 'function' && typeof app.addHook === 'function') {
|
|
169
177
|
instrumentFastify(app, mergedOpts);
|
|
@@ -227,6 +235,7 @@ export { flush } from './transport';
|
|
|
227
235
|
export { instrumentExpress, trickleMiddleware } from './express';
|
|
228
236
|
export { instrumentFastify, tricklePlugin } from './fastify';
|
|
229
237
|
export { instrumentKoa, instrumentKoaRouter } from './koa';
|
|
238
|
+
export { instrumentHono, trickleHonoMiddleware } from './hono';
|
|
230
239
|
export { observe, observeFn } from './observe';
|
|
231
240
|
export type { ObserveOpts } from './observe';
|
|
232
241
|
export { wrapFunction } from './wrap';
|
package/src/observe-register.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { patchFetch } from './fetch-observer';
|
|
|
38
38
|
import { instrumentExpress, trickleMiddleware } from './express';
|
|
39
39
|
import { instrumentFastify } from './fastify';
|
|
40
40
|
import { instrumentKoa } from './koa';
|
|
41
|
+
import { instrumentHono } from './hono';
|
|
41
42
|
import { initVarTracer, traceVar } from './trace-var';
|
|
42
43
|
import { initCallTrace } from './call-trace';
|
|
43
44
|
import { initLlmObserver } from './llm-observer';
|
|
@@ -1483,6 +1484,47 @@ if (enabled) {
|
|
|
1483
1484
|
} catch { /* fall through */ }
|
|
1484
1485
|
}
|
|
1485
1486
|
|
|
1487
|
+
// ── Hono auto-detection ──
|
|
1488
|
+
if (request === 'hono' && !expressPatched.has('hono')) {
|
|
1489
|
+
expressPatched.add('hono');
|
|
1490
|
+
try {
|
|
1491
|
+
const honoMod = exports;
|
|
1492
|
+
const HonoClass = honoMod.Hono || (honoMod.default && honoMod.default.Hono);
|
|
1493
|
+
if (HonoClass && typeof HonoClass === 'function') {
|
|
1494
|
+
const OrigHono = HonoClass;
|
|
1495
|
+
const WrappedHono = function (this: any, ...args: any[]): any {
|
|
1496
|
+
const app = new OrigHono(...args);
|
|
1497
|
+
try {
|
|
1498
|
+
instrumentHono(app, { environment });
|
|
1499
|
+
if (debug) {
|
|
1500
|
+
console.log('[trickle/observe] Auto-instrumented Hono app');
|
|
1501
|
+
}
|
|
1502
|
+
} catch (e: unknown) {
|
|
1503
|
+
if (debug) {
|
|
1504
|
+
console.log('[trickle/observe] Hono instrumentation error:', (e as Error).message);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return app;
|
|
1508
|
+
};
|
|
1509
|
+
WrappedHono.prototype = OrigHono.prototype;
|
|
1510
|
+
for (const key of Object.keys(OrigHono)) {
|
|
1511
|
+
(WrappedHono as any)[key] = (OrigHono as any)[key];
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (honoMod.Hono) {
|
|
1515
|
+
honoMod.Hono = WrappedHono;
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
const resolvedPath = M._resolveFilename(request, parent);
|
|
1519
|
+
if (require.cache[resolvedPath]) {
|
|
1520
|
+
const cached = require.cache[resolvedPath]!.exports;
|
|
1521
|
+
if (cached.Hono) cached.Hono = WrappedHono;
|
|
1522
|
+
}
|
|
1523
|
+
} catch { /* non-critical */ }
|
|
1524
|
+
}
|
|
1525
|
+
} catch { /* fall through */ }
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1486
1528
|
// ── Database auto-detection: patch database drivers to capture SQL queries ──
|
|
1487
1529
|
if (request === 'pg' && !expressPatched.has('pg')) {
|
|
1488
1530
|
expressPatched.add('pg');
|