@getvision/server 0.0.0-develop-20251031183955
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/.env.example +10 -0
- package/.eslintrc.cjs +7 -0
- package/.turbo/turbo-build.log +1 -0
- package/README.md +542 -0
- package/package.json +42 -0
- package/src/event-bus.ts +286 -0
- package/src/event-registry.ts +158 -0
- package/src/index.ts +64 -0
- package/src/router.ts +100 -0
- package/src/service.ts +412 -0
- package/src/types.ts +74 -0
- package/src/vision-app.ts +685 -0
- package/src/vision.ts +319 -0
- package/tsconfig.json +9 -0
package/src/vision.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type { Hono, Context, MiddlewareHandler } from 'hono'
|
|
2
|
+
import { VisionCore } from '@getvision/core'
|
|
3
|
+
import { AsyncLocalStorage } from 'async_hooks'
|
|
4
|
+
import { readFileSync, existsSync } from 'fs'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vision context available in request handlers
|
|
9
|
+
*/
|
|
10
|
+
interface VisionContext {
|
|
11
|
+
vision: VisionCore
|
|
12
|
+
traceId: string
|
|
13
|
+
rootSpanId: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const visionContext = new AsyncLocalStorage<VisionContext>()
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get Vision context (available in route handlers)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const { vision, traceId } = getVisionContext()
|
|
24
|
+
* const withSpan = vision.createSpanHelper(traceId)
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function getVisionContext(): VisionContext {
|
|
28
|
+
const context = visionContext.getStore()
|
|
29
|
+
if (!context) {
|
|
30
|
+
throw new Error('Vision context not available. Make sure Vision is enabled.')
|
|
31
|
+
}
|
|
32
|
+
return context
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create span helper using current trace context
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const withSpan = useVisionSpan()
|
|
41
|
+
* const users = withSpan('db.select', { 'db.table': 'users' }, () => {
|
|
42
|
+
* return db.select().from(users).all()
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useVisionSpan() {
|
|
47
|
+
const { vision, traceId, rootSpanId } = getVisionContext()
|
|
48
|
+
const tracer = vision.getTracer()
|
|
49
|
+
|
|
50
|
+
return <T>(
|
|
51
|
+
name: string,
|
|
52
|
+
attributes: Record<string, any> = {},
|
|
53
|
+
fn: () => T
|
|
54
|
+
): T => {
|
|
55
|
+
const span = tracer.startSpan(name, traceId, rootSpanId)
|
|
56
|
+
|
|
57
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
58
|
+
tracer.setAttribute(span.id, key, value)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = fn()
|
|
63
|
+
const completedSpan = tracer.endSpan(span.id)
|
|
64
|
+
|
|
65
|
+
if (completedSpan) {
|
|
66
|
+
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
} catch (error) {
|
|
71
|
+
tracer.setAttribute(span.id, 'error', true)
|
|
72
|
+
tracer.setAttribute(
|
|
73
|
+
span.id,
|
|
74
|
+
'error.message',
|
|
75
|
+
error instanceof Error ? error.message : String(error)
|
|
76
|
+
)
|
|
77
|
+
const completedSpan = tracer.endSpan(span.id)
|
|
78
|
+
|
|
79
|
+
if (completedSpan) {
|
|
80
|
+
vision.getTraceStore().addSpan(traceId, completedSpan)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Vision middleware options
|
|
90
|
+
*/
|
|
91
|
+
export interface VisionOptions {
|
|
92
|
+
enabled?: boolean
|
|
93
|
+
port?: number
|
|
94
|
+
maxTraces?: number
|
|
95
|
+
maxLogs?: number
|
|
96
|
+
logging?: boolean
|
|
97
|
+
service?: {
|
|
98
|
+
name?: string
|
|
99
|
+
version?: string
|
|
100
|
+
description?: string
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let visionInstance: VisionCore | null = null
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get Vision instance
|
|
108
|
+
*/
|
|
109
|
+
export function getVisionInstance(): VisionCore | null {
|
|
110
|
+
return visionInstance
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Auto-detect package.json info
|
|
115
|
+
*/
|
|
116
|
+
function autoDetectPackageInfo(): { name: string; version: string } {
|
|
117
|
+
try {
|
|
118
|
+
const pkgPath = join(process.cwd(), 'package.json')
|
|
119
|
+
if (existsSync(pkgPath)) {
|
|
120
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
121
|
+
return {
|
|
122
|
+
name: pkg.name || 'unknown',
|
|
123
|
+
version: pkg.version || '0.0.0',
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Ignore errors
|
|
128
|
+
}
|
|
129
|
+
return { name: 'unknown', version: '0.0.0' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create Vision middleware for Hono
|
|
134
|
+
*
|
|
135
|
+
* This middleware automatically:
|
|
136
|
+
* - Creates traces for all requests
|
|
137
|
+
* - Adds request/response metadata
|
|
138
|
+
* - Provides Vision context to handlers
|
|
139
|
+
* - Broadcasts events to Vision Dashboard
|
|
140
|
+
*/
|
|
141
|
+
export function createVisionMiddleware(options: VisionOptions = {}): MiddlewareHandler {
|
|
142
|
+
const {
|
|
143
|
+
enabled = true,
|
|
144
|
+
port = 9500,
|
|
145
|
+
maxTraces = 1000,
|
|
146
|
+
maxLogs = 10000,
|
|
147
|
+
logging = true
|
|
148
|
+
} = options
|
|
149
|
+
|
|
150
|
+
if (!enabled) {
|
|
151
|
+
return async (c, next) => await next()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Initialize Vision Core once
|
|
155
|
+
if (!visionInstance) {
|
|
156
|
+
visionInstance = new VisionCore({ port, maxTraces, maxLogs })
|
|
157
|
+
|
|
158
|
+
// Auto-detect service info
|
|
159
|
+
const pkgInfo = autoDetectPackageInfo()
|
|
160
|
+
const serviceName = options.service?.name || pkgInfo.name
|
|
161
|
+
const serviceVersion = options.service?.version || pkgInfo.version
|
|
162
|
+
|
|
163
|
+
// Set app status
|
|
164
|
+
visionInstance.setAppStatus({
|
|
165
|
+
name: serviceName,
|
|
166
|
+
version: serviceVersion,
|
|
167
|
+
description: options.service?.description,
|
|
168
|
+
running: true,
|
|
169
|
+
pid: process.pid,
|
|
170
|
+
metadata: {
|
|
171
|
+
framework: 'vision-sdk',
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const vision = visionInstance
|
|
177
|
+
|
|
178
|
+
// Middleware to trace requests
|
|
179
|
+
return async (c: Context, next) => {
|
|
180
|
+
// Skip OPTIONS requests
|
|
181
|
+
if (c.req.method === 'OPTIONS') {
|
|
182
|
+
return next()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const startTime = Date.now()
|
|
186
|
+
|
|
187
|
+
// Create trace
|
|
188
|
+
const trace = vision.createTrace(c.req.method, c.req.path)
|
|
189
|
+
|
|
190
|
+
// Run request in AsyncLocalStorage context
|
|
191
|
+
return visionContext.run({ vision, traceId: trace.id, rootSpanId: '' }, async () => {
|
|
192
|
+
// Start main span
|
|
193
|
+
const tracer = vision.getTracer()
|
|
194
|
+
const rootSpan = tracer.startSpan('http.request', trace.id)
|
|
195
|
+
|
|
196
|
+
// Update context with rootSpanId
|
|
197
|
+
const ctx = visionContext.getStore()
|
|
198
|
+
if (ctx) {
|
|
199
|
+
ctx.rootSpanId = rootSpan.id
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Add request attributes
|
|
203
|
+
tracer.setAttribute(rootSpan.id, 'http.method', c.req.method)
|
|
204
|
+
tracer.setAttribute(rootSpan.id, 'http.path', c.req.path)
|
|
205
|
+
tracer.setAttribute(rootSpan.id, 'http.url', c.req.url)
|
|
206
|
+
|
|
207
|
+
// Add query params if any
|
|
208
|
+
const url = new URL(c.req.url)
|
|
209
|
+
if (url.search) {
|
|
210
|
+
tracer.setAttribute(rootSpan.id, 'http.query', url.search)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Capture request metadata
|
|
214
|
+
try {
|
|
215
|
+
const rawReq = c.req.raw
|
|
216
|
+
const headers: Record<string, string> = {}
|
|
217
|
+
rawReq.headers.forEach((v, k) => { headers[k] = v })
|
|
218
|
+
|
|
219
|
+
const urlObj = new URL(c.req.url)
|
|
220
|
+
const query: Record<string, string> = {}
|
|
221
|
+
urlObj.searchParams.forEach((v, k) => { query[k] = v })
|
|
222
|
+
|
|
223
|
+
let body: unknown = undefined
|
|
224
|
+
const ct = headers['content-type'] || headers['Content-Type']
|
|
225
|
+
if (ct && ct.includes('application/json')) {
|
|
226
|
+
try {
|
|
227
|
+
body = await rawReq.clone().json()
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sessionId = headers['x-vision-session']
|
|
232
|
+
if (sessionId) {
|
|
233
|
+
tracer.setAttribute(rootSpan.id, 'session.id', sessionId)
|
|
234
|
+
trace.metadata = { ...(trace.metadata || {}), sessionId }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const requestMeta = {
|
|
238
|
+
method: c.req.method,
|
|
239
|
+
url: urlObj.pathname + (urlObj.search || ''),
|
|
240
|
+
headers,
|
|
241
|
+
query: Object.keys(query).length ? query : undefined,
|
|
242
|
+
body,
|
|
243
|
+
}
|
|
244
|
+
tracer.setAttribute(rootSpan.id, 'http.request', requestMeta)
|
|
245
|
+
trace.metadata = { ...(trace.metadata || {}), request: requestMeta }
|
|
246
|
+
|
|
247
|
+
// Emit start log
|
|
248
|
+
if (logging) {
|
|
249
|
+
const parts = [
|
|
250
|
+
`method=${c.req.method}`,
|
|
251
|
+
`path=${c.req.path}`,
|
|
252
|
+
]
|
|
253
|
+
if (sessionId) parts.push(`sessionId=${sessionId}`)
|
|
254
|
+
parts.push(`traceId=${trace.id}`)
|
|
255
|
+
console.info(`INF starting request ${parts.join(' ')}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Execute request
|
|
259
|
+
await next()
|
|
260
|
+
|
|
261
|
+
// Add response attributes
|
|
262
|
+
tracer.setAttribute(rootSpan.id, 'http.status_code', c.res.status)
|
|
263
|
+
const resHeaders: Record<string, string> = {}
|
|
264
|
+
c.res.headers?.forEach((v, k) => { resHeaders[k] = v as unknown as string })
|
|
265
|
+
|
|
266
|
+
let respBody: unknown = undefined
|
|
267
|
+
const resCt = c.res.headers?.get('content-type') || ''
|
|
268
|
+
try {
|
|
269
|
+
const clone = c.res.clone()
|
|
270
|
+
if (resCt.includes('application/json')) {
|
|
271
|
+
const txt = await clone.text()
|
|
272
|
+
if (txt && txt.length <= 65536) {
|
|
273
|
+
try { respBody = JSON.parse(txt) } catch { respBody = txt }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch {}
|
|
277
|
+
|
|
278
|
+
const responseMeta = {
|
|
279
|
+
status: c.res.status,
|
|
280
|
+
headers: Object.keys(resHeaders).length ? resHeaders : undefined,
|
|
281
|
+
body: respBody,
|
|
282
|
+
}
|
|
283
|
+
tracer.setAttribute(rootSpan.id, 'http.response', responseMeta)
|
|
284
|
+
trace.metadata = { ...(trace.metadata || {}), response: responseMeta }
|
|
285
|
+
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// Track error
|
|
288
|
+
tracer.addEvent(rootSpan.id, 'error', {
|
|
289
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
290
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
tracer.setAttribute(rootSpan.id, 'error', true)
|
|
294
|
+
throw error
|
|
295
|
+
|
|
296
|
+
} finally {
|
|
297
|
+
// End span and add it to trace
|
|
298
|
+
const completedSpan = tracer.endSpan(rootSpan.id)
|
|
299
|
+
if (completedSpan) {
|
|
300
|
+
vision.getTraceStore().addSpan(trace.id, completedSpan)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Complete trace
|
|
304
|
+
const duration = Date.now() - startTime
|
|
305
|
+
vision.completeTrace(trace.id, c.res.status, duration)
|
|
306
|
+
|
|
307
|
+
// Add trace ID to response headers
|
|
308
|
+
c.header('X-Vision-Trace-Id', trace.id)
|
|
309
|
+
|
|
310
|
+
// Emit completion log
|
|
311
|
+
if (logging) {
|
|
312
|
+
console.info(
|
|
313
|
+
`INF request completed code=${c.res.status} duration=${duration}ms method=${c.req.method} path=${c.req.path} traceId=${trace.id}`
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|