@gokiteam/goki-dev 0.2.0
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 +478 -0
- package/bin/goki-dev.js +452 -0
- package/bin/mcp-server.js +16 -0
- package/bin/secrets-cli.js +302 -0
- package/cli/ComposeOverrideGenerator.js +226 -0
- package/cli/ComposeParser.js +73 -0
- package/cli/ConfigGenerator.js +304 -0
- package/cli/ConfigManager.js +46 -0
- package/cli/DatabaseManager.js +94 -0
- package/cli/DevToolsChecker.js +21 -0
- package/cli/DevToolsDir.js +66 -0
- package/cli/DevToolsManager.js +451 -0
- package/cli/DockerManager.js +138 -0
- package/cli/FunctionManager.js +95 -0
- package/cli/HttpProxyRewriter.js +91 -0
- package/cli/Logger.js +10 -0
- package/cli/McpConfigManager.js +123 -0
- package/cli/NgrokManager.js +431 -0
- package/cli/ProjectCLI.js +2322 -0
- package/cli/PubSubManager.js +129 -0
- package/cli/SnapshotManager.js +88 -0
- package/cli/UiFormatter.js +292 -0
- package/cli/WebhookUrlRewriter.js +32 -0
- package/cli/secrets/BiometricAuth.js +125 -0
- package/cli/secrets/SecretInjector.js +47 -0
- package/cli/secrets/SecretsConfig.js +141 -0
- package/cli/secrets/SecretsDoctor.js +384 -0
- package/cli/secrets/SecretsManager.js +255 -0
- package/client/dist/client.d.ts +332 -0
- package/client/dist/client.js +507 -0
- package/client/dist/helpers.d.ts +62 -0
- package/client/dist/helpers.js +122 -0
- package/client/dist/index.d.ts +59 -0
- package/client/dist/index.js +78 -0
- package/client/dist/package.json +1 -0
- package/client/dist/types.d.ts +280 -0
- package/client/dist/types.js +7 -0
- package/config.development +46 -0
- package/config.test +18 -0
- package/guidelines/CodingStyleGuideline.md +148 -0
- package/guidelines/CommentingGuideline.md +10 -0
- package/guidelines/HttpApiImplementationGuideline.md +137 -0
- package/guidelines/NamingGuideline.md +182 -0
- package/package.json +138 -0
- package/patterns/api/[collectionName]/Controllers.md +62 -0
- package/patterns/api/[collectionName]/Logic.md +154 -0
- package/patterns/api/[collectionName]/Permissions.md +81 -0
- package/patterns/api/[collectionName]/Router.md +83 -0
- package/patterns/api/[collectionName]/Schemas.md +197 -0
- package/patterns/configs/Patterns.md +7 -0
- package/patterns/enums/Patterns.md +24 -0
- package/patterns/errorHandling/Patterns.md +185 -0
- package/patterns/testing/Patterns.md +232 -0
- package/src/Server.js +238 -0
- package/src/api/dashboard/Controllers.js +9 -0
- package/src/api/dashboard/Logic.js +76 -0
- package/src/api/dashboard/Router.js +11 -0
- package/src/api/dashboard/Schemas.js +47 -0
- package/src/api/data/Controllers.js +26 -0
- package/src/api/data/Logic.js +188 -0
- package/src/api/data/Router.js +16 -0
- package/src/api/docker/Controllers.js +33 -0
- package/src/api/docker/Logic.js +268 -0
- package/src/api/docker/Router.js +15 -0
- package/src/api/docker/Schemas.js +80 -0
- package/src/api/docs/Controllers.js +15 -0
- package/src/api/docs/Logic.js +85 -0
- package/src/api/docs/Router.js +12 -0
- package/src/api/export/Controllers.js +30 -0
- package/src/api/export/Logic.js +143 -0
- package/src/api/export/Router.js +18 -0
- package/src/api/export/Schemas.js +104 -0
- package/src/api/firestore/Controllers.js +152 -0
- package/src/api/firestore/Logic.js +474 -0
- package/src/api/firestore/Router.js +23 -0
- package/src/api/functions/Controllers.js +261 -0
- package/src/api/functions/Logic.js +710 -0
- package/src/api/functions/Router.js +50 -0
- package/src/api/functions/Schemas.js +193 -0
- package/src/api/gateway/Controllers.js +72 -0
- package/src/api/gateway/Logic.js +74 -0
- package/src/api/gateway/Router.js +10 -0
- package/src/api/gateway/Schemas.js +19 -0
- package/src/api/health/Controllers.js +14 -0
- package/src/api/health/Logic.js +24 -0
- package/src/api/health/Router.js +12 -0
- package/src/api/httpTraffic/Controllers.js +29 -0
- package/src/api/httpTraffic/Logic.js +33 -0
- package/src/api/httpTraffic/Router.js +9 -0
- package/src/api/httpTraffic/Schemas.js +23 -0
- package/src/api/logging/Controllers.js +80 -0
- package/src/api/logging/Logic.js +461 -0
- package/src/api/logging/Router.js +24 -0
- package/src/api/logging/Schemas.js +43 -0
- package/src/api/mqtt/Controllers.js +17 -0
- package/src/api/mqtt/Logic.js +66 -0
- package/src/api/mqtt/Router.js +12 -0
- package/src/api/postgres/Controllers.js +97 -0
- package/src/api/postgres/Logic.js +221 -0
- package/src/api/postgres/Router.js +21 -0
- package/src/api/pubsub/Controllers.js +236 -0
- package/src/api/pubsub/Logic.js +732 -0
- package/src/api/pubsub/Router.js +41 -0
- package/src/api/pubsub/Schemas.js +355 -0
- package/src/api/redis/Controllers.js +63 -0
- package/src/api/redis/Logic.js +239 -0
- package/src/api/redis/Router.js +21 -0
- package/src/api/scheduler/Controllers.js +27 -0
- package/src/api/scheduler/Logic.js +49 -0
- package/src/api/scheduler/Router.js +16 -0
- package/src/api/services/Controllers.js +26 -0
- package/src/api/services/Logic.js +205 -0
- package/src/api/services/Router.js +14 -0
- package/src/api/services/Schemas.js +66 -0
- package/src/api/snapshots/Controllers.js +37 -0
- package/src/api/snapshots/Logic.js +797 -0
- package/src/api/snapshots/Router.js +15 -0
- package/src/api/snapshots/Schemas.js +23 -0
- package/src/api/webhooks/Controllers.js +49 -0
- package/src/api/webhooks/Logic.js +137 -0
- package/src/api/webhooks/Router.js +12 -0
- package/src/api/webhooks/Schemas.js +31 -0
- package/src/configs/Application.js +147 -0
- package/src/configs/Default.js +13 -0
- package/src/consumers/BlackboxLogsConsumer.js +235 -0
- package/src/consumers/DockerLogsConsumer.js +687 -0
- package/src/db/Tables.js +66 -0
- package/src/db/schemas/firestore.js +18 -0
- package/src/db/schemas/functions.js +65 -0
- package/src/db/schemas/httpTraffic.js +43 -0
- package/src/db/schemas/logging.js +74 -0
- package/src/db/schemas/migrations.js +64 -0
- package/src/db/schemas/mqtt.js +56 -0
- package/src/db/schemas/pubsub.js +90 -0
- package/src/db/schemas/pubsubRegistry.js +22 -0
- package/src/db/schemas/webhooks.js +28 -0
- package/src/emulation/awsiot/Controllers.js +91 -0
- package/src/emulation/awsiot/Logic.js +70 -0
- package/src/emulation/awsiot/Router.js +19 -0
- package/src/emulation/awsiot/Server.js +100 -0
- package/src/emulation/firestore/Server.js +136 -0
- package/src/emulation/logging/Controllers.js +212 -0
- package/src/emulation/logging/Logic.js +416 -0
- package/src/emulation/logging/Router.js +36 -0
- package/src/emulation/logging/Schemas.js +82 -0
- package/src/emulation/logging/Server.js +108 -0
- package/src/emulation/pubsub/Controllers.js +279 -0
- package/src/emulation/pubsub/DefaultTopics.js +162 -0
- package/src/emulation/pubsub/Logic.js +427 -0
- package/src/emulation/pubsub/README.md +309 -0
- package/src/emulation/pubsub/Router.js +33 -0
- package/src/emulation/pubsub/Server.js +104 -0
- package/src/emulation/pubsub/ShadowPoller.js +276 -0
- package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
- package/src/enums/ContainerNames.js +106 -0
- package/src/enums/ErrorReason.js +28 -0
- package/src/enums/FunctionStatuses.js +15 -0
- package/src/enums/FunctionTriggerTypes.js +15 -0
- package/src/enums/GatewayState.js +7 -0
- package/src/enums/ServiceNames.js +68 -0
- package/src/jobs/DatabaseMaintenance.js +184 -0
- package/src/jobs/MessageHistoryCleanup.js +152 -0
- package/src/mcp/ApiClient.js +25 -0
- package/src/mcp/Server.js +52 -0
- package/src/mcp/prompts/debugging.js +104 -0
- package/src/mcp/resources/platform.js +118 -0
- package/src/mcp/tools/data.js +84 -0
- package/src/mcp/tools/docker.js +166 -0
- package/src/mcp/tools/firestore.js +162 -0
- package/src/mcp/tools/functions.js +380 -0
- package/src/mcp/tools/httpTraffic.js +69 -0
- package/src/mcp/tools/logging.js +174 -0
- package/src/mcp/tools/mqtt.js +37 -0
- package/src/mcp/tools/postgres.js +130 -0
- package/src/mcp/tools/pubsub.js +316 -0
- package/src/mcp/tools/redis.js +146 -0
- package/src/mcp/tools/services.js +169 -0
- package/src/mcp/tools/snapshots.js +88 -0
- package/src/mcp/tools/webhooks.js +115 -0
- package/src/middleware/DevProxy.js +67 -0
- package/src/middleware/ErrorCatcher.js +35 -0
- package/src/middleware/HttpProxy.js +215 -0
- package/src/middleware/Reply.js +24 -0
- package/src/middleware/TraceId.js +9 -0
- package/src/middleware/WebhookProxy.js +234 -0
- package/src/protocols/mqtt/Broker.js +92 -0
- package/src/protocols/mqtt/Handlers.js +175 -0
- package/src/protocols/mqtt/PubSubBridge.js +162 -0
- package/src/protocols/mqtt/Server.js +116 -0
- package/src/runtime/FunctionRunner.js +179 -0
- package/src/services/AppGatewayService.js +582 -0
- package/src/singletons/FirestoreBroadcaster.js +367 -0
- package/src/singletons/FunctionTriggerDispatcher.js +456 -0
- package/src/singletons/FunctionsService.js +418 -0
- package/src/singletons/HttpProxy.js +224 -0
- package/src/singletons/LogBroadcaster.js +159 -0
- package/src/singletons/Logger.js +49 -0
- package/src/singletons/MemoryJsonStore.js +175 -0
- package/src/singletons/MessageBroadcaster.js +190 -0
- package/src/singletons/PostgresBroadcaster.js +367 -0
- package/src/singletons/PostgresClient.js +180 -0
- package/src/singletons/RedisClient.js +184 -0
- package/src/singletons/SqliteStore.js +480 -0
- package/src/singletons/TickService.js +151 -0
- package/src/singletons/WebhookProxy.js +223 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { SqliteStore } from '../../singletons/SqliteStore.js'
|
|
2
|
+
import { LOGGING_ENTRIES, LOGGING_LOGS } from '../../db/Tables.js'
|
|
3
|
+
|
|
4
|
+
const COMPACT_COLUMNS = '_id, timestamp, severity, service_name, text_payload, error_message, trace, event'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert MongoDB-style filter to SQL WHERE clause
|
|
8
|
+
* Supports: exact match, $in, $regex, $gte, $lte, $gt, $lt, $ne, $or, $nor, $not, $search
|
|
9
|
+
*/
|
|
10
|
+
const filterToSQL = (filter) => {
|
|
11
|
+
const conditions = []
|
|
12
|
+
const params = []
|
|
13
|
+
|
|
14
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
15
|
+
// Special operators
|
|
16
|
+
if (key === '$or') {
|
|
17
|
+
// OR operator: { $or: [{...}, {...}] }
|
|
18
|
+
const orConditions = value.map(subFilter => filterToSQL(subFilter))
|
|
19
|
+
const orSQL = orConditions.map(({ sql }) => `(${sql})`).join(' OR ')
|
|
20
|
+
conditions.push(`(${orSQL})`)
|
|
21
|
+
orConditions.forEach(({ params: subParams }) => params.push(...subParams))
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (key === '$nor') {
|
|
26
|
+
// NOR operator: { $nor: [{...}, {...}] }
|
|
27
|
+
const norConditions = value.map(subFilter => filterToSQL(subFilter))
|
|
28
|
+
const norSQL = norConditions.map(({ sql }) => `(${sql})`).join(' OR ')
|
|
29
|
+
conditions.push(`NOT (${norSQL})`)
|
|
30
|
+
norConditions.forEach(({ params: subParams }) => params.push(...subParams))
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (key === '$search') {
|
|
35
|
+
// SEARCH function: { $search: { term: "...", fields: [...] } }
|
|
36
|
+
const { term, fields } = value
|
|
37
|
+
const searchConditions = fields.map(field => {
|
|
38
|
+
const column = mapFieldToColumn(field)
|
|
39
|
+
params.push(`%${term}%`)
|
|
40
|
+
return `${column} LIKE ?`
|
|
41
|
+
})
|
|
42
|
+
conditions.push(`(${searchConditions.join(' OR ')})`)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Regular field conditions
|
|
47
|
+
const column = mapFieldToColumn(key)
|
|
48
|
+
|
|
49
|
+
if (value === null || value === undefined) {
|
|
50
|
+
conditions.push(`${column} IS NULL`)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
55
|
+
// MongoDB operators
|
|
56
|
+
if (value.$in) {
|
|
57
|
+
const placeholders = value.$in.map(() => '?').join(', ')
|
|
58
|
+
conditions.push(`${column} IN (${placeholders})`)
|
|
59
|
+
params.push(...value.$in)
|
|
60
|
+
} else if (value.$nin) {
|
|
61
|
+
const placeholders = value.$nin.map(() => '?').join(', ')
|
|
62
|
+
conditions.push(`${column} NOT IN (${placeholders})`)
|
|
63
|
+
params.push(...value.$nin)
|
|
64
|
+
} else if (value.$regex) {
|
|
65
|
+
const pattern = `%${value.$regex}%`
|
|
66
|
+
conditions.push(`${column} LIKE ?`)
|
|
67
|
+
params.push(pattern)
|
|
68
|
+
} else if (value.$not) {
|
|
69
|
+
if (value.$not.$regex) {
|
|
70
|
+
const pattern = `%${value.$not.$regex}%`
|
|
71
|
+
conditions.push(`${column} NOT LIKE ?`)
|
|
72
|
+
params.push(pattern)
|
|
73
|
+
} else {
|
|
74
|
+
// Generic NOT
|
|
75
|
+
const notFilter = { [key]: value.$not }
|
|
76
|
+
const { sql: notSQL, params: notParams } = filterToSQL(notFilter)
|
|
77
|
+
conditions.push(`NOT (${notSQL})`)
|
|
78
|
+
params.push(...notParams)
|
|
79
|
+
}
|
|
80
|
+
} else if (value.$gte !== undefined) {
|
|
81
|
+
conditions.push(`${column} >= ?`)
|
|
82
|
+
params.push(value.$gte)
|
|
83
|
+
} else if (value.$lte !== undefined) {
|
|
84
|
+
conditions.push(`${column} <= ?`)
|
|
85
|
+
params.push(value.$lte)
|
|
86
|
+
} else if (value.$gt !== undefined) {
|
|
87
|
+
conditions.push(`${column} > ?`)
|
|
88
|
+
params.push(value.$gt)
|
|
89
|
+
} else if (value.$lt !== undefined) {
|
|
90
|
+
conditions.push(`${column} < ?`)
|
|
91
|
+
params.push(value.$lt)
|
|
92
|
+
} else if (value.$ne !== undefined) {
|
|
93
|
+
conditions.push(`${column} != ?`)
|
|
94
|
+
params.push(value.$ne)
|
|
95
|
+
} else if (value.$eq !== undefined) {
|
|
96
|
+
conditions.push(`${column} = ?`)
|
|
97
|
+
params.push(value.$eq)
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Exact match
|
|
101
|
+
conditions.push(`${column} = ?`)
|
|
102
|
+
params.push(value)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sql = conditions.length > 0 ? conditions.join(' AND ') : '1=1'
|
|
107
|
+
return { sql, params }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Map camelCase field names to snake_case column names
|
|
112
|
+
*/
|
|
113
|
+
const mapFieldToColumn = (field) => {
|
|
114
|
+
const fieldMap = {
|
|
115
|
+
severity: 'severity',
|
|
116
|
+
logName: 'log_name',
|
|
117
|
+
serviceName: 'service_name',
|
|
118
|
+
timestamp: 'timestamp',
|
|
119
|
+
textPayload: 'text_payload',
|
|
120
|
+
jsonPayload: 'json_payload',
|
|
121
|
+
protoPayload: 'proto_payload',
|
|
122
|
+
trace: 'trace',
|
|
123
|
+
spanId: 'span_id',
|
|
124
|
+
traceSampled: 'trace_sampled',
|
|
125
|
+
insertId: 'insert_id',
|
|
126
|
+
receiveTimestamp: 'receive_timestamp',
|
|
127
|
+
httpRequest: 'http_request',
|
|
128
|
+
operation: 'operation',
|
|
129
|
+
sourceLocation: 'source_location',
|
|
130
|
+
source: 'source',
|
|
131
|
+
level: 'level',
|
|
132
|
+
errorMessage: 'error_message',
|
|
133
|
+
stackTrace: 'stack_trace',
|
|
134
|
+
event: 'event',
|
|
135
|
+
duration: 'duration',
|
|
136
|
+
errorCode: 'error_code',
|
|
137
|
+
errorStatus: 'error_status',
|
|
138
|
+
errorReason: 'error_reason',
|
|
139
|
+
errorResource: 'error_resource',
|
|
140
|
+
propertyId: 'property_id',
|
|
141
|
+
deviceId: 'device_id',
|
|
142
|
+
doorId: 'door_id',
|
|
143
|
+
subscriptionName: 'subscription_name',
|
|
144
|
+
messageId: 'message_id',
|
|
145
|
+
labels: 'labels'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle nested fields (e.g., labels.environment)
|
|
149
|
+
if (field.includes('.')) {
|
|
150
|
+
const parts = field.split('.')
|
|
151
|
+
const base = fieldMap[parts[0]] || parts[0]
|
|
152
|
+
// For JSON fields, use SQLite JSON operators
|
|
153
|
+
if (base === 'labels' || base === 'json_payload') {
|
|
154
|
+
return `json_extract(${base}, '$.${parts.slice(1).join('.')}')`
|
|
155
|
+
}
|
|
156
|
+
return field
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return fieldMap[field] || field
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
|
163
|
+
|
|
164
|
+
export const Logic = {
|
|
165
|
+
async listServices (params) {
|
|
166
|
+
const { traceId } = params
|
|
167
|
+
try {
|
|
168
|
+
// Use raw SQL for efficiency - get unique service names directly from database
|
|
169
|
+
const query = `
|
|
170
|
+
SELECT DISTINCT service_name
|
|
171
|
+
FROM ${LOGGING_ENTRIES}
|
|
172
|
+
WHERE service_name IS NOT NULL
|
|
173
|
+
ORDER BY service_name ASC
|
|
174
|
+
`
|
|
175
|
+
const rows = SqliteStore.db.prepare(query).all()
|
|
176
|
+
const services = rows.map(row => row.service_name)
|
|
177
|
+
return {
|
|
178
|
+
services,
|
|
179
|
+
total: services.length,
|
|
180
|
+
traceId
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
status: 'error',
|
|
185
|
+
message: error.message,
|
|
186
|
+
traceId
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
async listEntries (params) {
|
|
191
|
+
const { filter = {}, page = {}, compact = false, traceId } = params
|
|
192
|
+
const startTime = Date.now()
|
|
193
|
+
try {
|
|
194
|
+
// Convert MongoDB-style filter to SQL WHERE clause
|
|
195
|
+
const { sql: whereSQL, params: whereParams } = filterToSQL(filter)
|
|
196
|
+
// Select only needed columns in compact mode
|
|
197
|
+
const columns = compact ? COMPACT_COLUMNS : '*'
|
|
198
|
+
let sql = `SELECT ${columns} FROM ${LOGGING_ENTRIES}`
|
|
199
|
+
const queryParams = []
|
|
200
|
+
if (whereSQL && whereSQL !== '1=1') {
|
|
201
|
+
sql += ` WHERE ${whereSQL}`
|
|
202
|
+
queryParams.push(...whereParams)
|
|
203
|
+
}
|
|
204
|
+
// Count total matching entries
|
|
205
|
+
const countSQL = `SELECT COUNT(*) as count FROM ${LOGGING_ENTRIES}` +
|
|
206
|
+
(whereSQL && whereSQL !== '1=1' ? ` WHERE ${whereSQL}` : '')
|
|
207
|
+
const totalResult = SqliteStore.db.prepare(countSQL).get(...whereParams)
|
|
208
|
+
const total = totalResult.count
|
|
209
|
+
// Add ordering (newest first)
|
|
210
|
+
sql += ' ORDER BY timestamp DESC'
|
|
211
|
+
// Add pagination
|
|
212
|
+
const limit = page.limit || 100
|
|
213
|
+
const offset = page.offset || 0
|
|
214
|
+
sql += ' LIMIT ? OFFSET ?'
|
|
215
|
+
queryParams.push(limit, offset)
|
|
216
|
+
// Log query for debugging (only in development)
|
|
217
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
218
|
+
console.log('[Query Debug]', {
|
|
219
|
+
sql,
|
|
220
|
+
params: queryParams,
|
|
221
|
+
filter
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
// Execute query
|
|
225
|
+
const stmt = SqliteStore.db.prepare(sql)
|
|
226
|
+
let entries = stmt.all(...queryParams)
|
|
227
|
+
// Convert snake_case to camelCase and parse JSON fields
|
|
228
|
+
entries = entries.map(row => SqliteStore.toCamelCase(row))
|
|
229
|
+
if (!compact) {
|
|
230
|
+
entries = entries.map(row => SqliteStore.parseJsonFields(LOGGING_ENTRIES, row))
|
|
231
|
+
}
|
|
232
|
+
const executionTime = Date.now() - startTime
|
|
233
|
+
// Log performance metrics
|
|
234
|
+
if (process.env.NODE_ENV !== 'production' || executionTime > 1000) {
|
|
235
|
+
console.log('[Query Performance]', {
|
|
236
|
+
executionTime: `${executionTime}ms`,
|
|
237
|
+
total,
|
|
238
|
+
returned: entries.length,
|
|
239
|
+
hasFilter: Object.keys(filter).length > 0
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
entries,
|
|
244
|
+
total,
|
|
245
|
+
executionTime,
|
|
246
|
+
traceId
|
|
247
|
+
}
|
|
248
|
+
} catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
status: 'error',
|
|
251
|
+
message: error.message,
|
|
252
|
+
executionTime: Date.now() - startTime,
|
|
253
|
+
traceId
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
async getEntriesById (params) {
|
|
258
|
+
const { ids, traceId } = params
|
|
259
|
+
try {
|
|
260
|
+
const placeholders = ids.map(() => '?').join(', ')
|
|
261
|
+
const sql = `SELECT * FROM ${LOGGING_ENTRIES} WHERE _id IN (${placeholders}) ORDER BY timestamp DESC`
|
|
262
|
+
let entries = SqliteStore.db.prepare(sql).all(...ids)
|
|
263
|
+
entries = entries.map(row => SqliteStore.toCamelCase(row))
|
|
264
|
+
.map(row => SqliteStore.parseJsonFields(LOGGING_ENTRIES, row))
|
|
265
|
+
return {
|
|
266
|
+
entries,
|
|
267
|
+
total: entries.length,
|
|
268
|
+
traceId
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
status: 'error',
|
|
273
|
+
message: error.message,
|
|
274
|
+
traceId
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async clearEntries (params) {
|
|
279
|
+
const { traceId } = params
|
|
280
|
+
try {
|
|
281
|
+
SqliteStore.clear(LOGGING_ENTRIES)
|
|
282
|
+
SqliteStore.clear(LOGGING_LOGS)
|
|
283
|
+
return {
|
|
284
|
+
message: 'All log entries cleared',
|
|
285
|
+
traceId
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw error
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
async exportEntries (params) {
|
|
292
|
+
const { traceId } = params
|
|
293
|
+
try {
|
|
294
|
+
const result = SqliteStore.list(LOGGING_ENTRIES)
|
|
295
|
+
return {
|
|
296
|
+
entries: result.data,
|
|
297
|
+
total: result.total,
|
|
298
|
+
exportedAt: new Date().toISOString(),
|
|
299
|
+
traceId
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
throw error
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
async getTrace (params) {
|
|
306
|
+
const { traceId } = params
|
|
307
|
+
try {
|
|
308
|
+
// Get all entries with the specified traceId using SQL WHERE
|
|
309
|
+
const result = SqliteStore.list(LOGGING_ENTRIES, {
|
|
310
|
+
where: { trace: traceId },
|
|
311
|
+
orderBy: 'timestamp ASC'
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const entries = result.data
|
|
315
|
+
|
|
316
|
+
// Extract unique services
|
|
317
|
+
const services = [...new Set(entries.map(entry => {
|
|
318
|
+
const logName = entry.logName || ''
|
|
319
|
+
const parts = logName.split('/')
|
|
320
|
+
return parts[parts.length - 1] || logName
|
|
321
|
+
}))]
|
|
322
|
+
|
|
323
|
+
// Calculate total duration
|
|
324
|
+
let duration = 0
|
|
325
|
+
if (entries.length > 1) {
|
|
326
|
+
const firstTimestamp = new Date(entries[0].timestamp)
|
|
327
|
+
const lastTimestamp = new Date(entries[entries.length - 1].timestamp)
|
|
328
|
+
duration = lastTimestamp - firstTimestamp
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Count errors and warnings
|
|
332
|
+
const errorCount = entries.filter(e =>
|
|
333
|
+
e.severity === 'ERROR' || e.severity === 'CRITICAL'
|
|
334
|
+
).length
|
|
335
|
+
const warningCount = entries.filter(e =>
|
|
336
|
+
e.severity === 'WARNING'
|
|
337
|
+
).length
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
trace: {
|
|
341
|
+
traceId,
|
|
342
|
+
entries,
|
|
343
|
+
services,
|
|
344
|
+
duration,
|
|
345
|
+
errorCount,
|
|
346
|
+
warningCount
|
|
347
|
+
},
|
|
348
|
+
traceId
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
return {
|
|
352
|
+
status: 'error',
|
|
353
|
+
message: error.message,
|
|
354
|
+
traceId
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Wait for a log entry matching filter criteria
|
|
361
|
+
*/
|
|
362
|
+
async waitForLog (params) {
|
|
363
|
+
const { filter, timeout = 5000, traceId } = params
|
|
364
|
+
const startTime = Date.now()
|
|
365
|
+
const pollInterval = 100
|
|
366
|
+
try {
|
|
367
|
+
while (Date.now() - startTime < timeout) {
|
|
368
|
+
const logFilter = {}
|
|
369
|
+
if (filter.traceId) logFilter.trace = filter.traceId
|
|
370
|
+
if (filter.service) logFilter.service_name = filter.service
|
|
371
|
+
if (filter.severity) logFilter.severity = filter.severity
|
|
372
|
+
const logsResult = await Logic.listEntries({
|
|
373
|
+
filter: logFilter,
|
|
374
|
+
page: { limit: 50 },
|
|
375
|
+
traceId
|
|
376
|
+
})
|
|
377
|
+
if (logsResult.entries && logsResult.entries.length > 0) {
|
|
378
|
+
for (const entry of logsResult.entries) {
|
|
379
|
+
if (!filter.textContains) {
|
|
380
|
+
return {
|
|
381
|
+
entry,
|
|
382
|
+
foundAt: Date.now() - startTime,
|
|
383
|
+
traceId
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const textPayload = entry.textPayload || entry.jsonPayload?.message || ''
|
|
387
|
+
if (textPayload.includes(filter.textContains)) {
|
|
388
|
+
return {
|
|
389
|
+
entry,
|
|
390
|
+
foundAt: Date.now() - startTime,
|
|
391
|
+
traceId
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
await sleep(pollInterval)
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
status: 'error',
|
|
400
|
+
message: 'Timeout waiting for log entry',
|
|
401
|
+
traceId
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
return {
|
|
405
|
+
status: 'error',
|
|
406
|
+
message: error.message,
|
|
407
|
+
traceId
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Assert that no errors occurred for a given trace
|
|
414
|
+
*/
|
|
415
|
+
async assertNoErrors (params) {
|
|
416
|
+
const { targetTraceId, sinceTimestamp, traceId } = params
|
|
417
|
+
try {
|
|
418
|
+
const logFilter = {
|
|
419
|
+
trace: targetTraceId
|
|
420
|
+
}
|
|
421
|
+
const logsResult = await Logic.listEntries({
|
|
422
|
+
filter: logFilter,
|
|
423
|
+
page: { limit: 1000 },
|
|
424
|
+
traceId
|
|
425
|
+
})
|
|
426
|
+
let errors = []
|
|
427
|
+
let criticalErrors = []
|
|
428
|
+
if (logsResult.entries && logsResult.entries.length > 0) {
|
|
429
|
+
const filteredEntries = sinceTimestamp
|
|
430
|
+
? logsResult.entries.filter(e => new Date(e.timestamp) >= new Date(sinceTimestamp))
|
|
431
|
+
: logsResult.entries
|
|
432
|
+
errors = filteredEntries.filter(e => e.severity === 'ERROR')
|
|
433
|
+
criticalErrors = filteredEntries.filter(e => e.severity === 'CRITICAL')
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
hasErrors: errors.length > 0 || criticalErrors.length > 0,
|
|
437
|
+
errorCount: errors.length,
|
|
438
|
+
criticalCount: criticalErrors.length,
|
|
439
|
+
errors: [...errors, ...criticalErrors],
|
|
440
|
+
traceId
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return {
|
|
444
|
+
status: 'error',
|
|
445
|
+
message: error.message,
|
|
446
|
+
traceId
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
async getQueueStats (params) {
|
|
452
|
+
const { traceId } = params
|
|
453
|
+
try {
|
|
454
|
+
const stats = await global.logsConsumer.getStats()
|
|
455
|
+
const health = await global.logsConsumer.getHealth()
|
|
456
|
+
return { stats, health, traceId }
|
|
457
|
+
} catch (error) {
|
|
458
|
+
return { status: 'error', message: error.message, traceId }
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import KoaRouter from 'koa-router'
|
|
2
|
+
import { Controllers } from './Controllers.js'
|
|
3
|
+
|
|
4
|
+
const Router = new KoaRouter()
|
|
5
|
+
const v1 = new KoaRouter({ prefix: '/v1/logging' })
|
|
6
|
+
|
|
7
|
+
v1.post('/services/list', Controllers.listServices)
|
|
8
|
+
v1.post('/entries/list', Controllers.listEntries)
|
|
9
|
+
v1.post('/entries/get', Controllers.getEntriesById)
|
|
10
|
+
v1.post('/entries/clear', Controllers.clearEntries)
|
|
11
|
+
v1.post('/entries/export', Controllers.exportEntries)
|
|
12
|
+
v1.get('/entries/stream', Controllers.streamLogs)
|
|
13
|
+
v1.post('/trace/:traceId', Controllers.getTrace)
|
|
14
|
+
|
|
15
|
+
// Testing Helpers
|
|
16
|
+
v1.post('/entries/wait-for', Controllers.waitForLog)
|
|
17
|
+
v1.post('/entries/assert-no-errors', Controllers.assertNoErrors)
|
|
18
|
+
|
|
19
|
+
// Queue Stats
|
|
20
|
+
v1.post('/queue-stats', Controllers.getQueueStats)
|
|
21
|
+
|
|
22
|
+
Router.use(v1.routes())
|
|
23
|
+
|
|
24
|
+
export { Router }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Joi } from '@gokiteam/koa'
|
|
2
|
+
|
|
3
|
+
export const Schemas = {
|
|
4
|
+
waitForLog: {
|
|
5
|
+
request: {
|
|
6
|
+
body: {
|
|
7
|
+
filter: Joi.object({
|
|
8
|
+
traceId: Joi.string().optional(),
|
|
9
|
+
service: Joi.string().optional(),
|
|
10
|
+
severity: Joi.string().valid('DEFAULT', 'DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY').optional(),
|
|
11
|
+
textContains: Joi.string().optional()
|
|
12
|
+
}).required(),
|
|
13
|
+
timeout: Joi.number().integer().min(100).max(30000).default(5000),
|
|
14
|
+
traceId: Joi.string().optional()
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
responses: {
|
|
18
|
+
success: {
|
|
19
|
+
entry: Joi.object().required(),
|
|
20
|
+
foundAt: Joi.number().integer().required(),
|
|
21
|
+
traceId: Joi.string().required()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
assertNoErrors: {
|
|
27
|
+
request: {
|
|
28
|
+
body: {
|
|
29
|
+
traceId: Joi.string().required(),
|
|
30
|
+
sinceTimestamp: Joi.string().isoDate().optional()
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
responses: {
|
|
34
|
+
success: {
|
|
35
|
+
hasErrors: Joi.boolean().required(),
|
|
36
|
+
errorCount: Joi.number().integer().min(0).required(),
|
|
37
|
+
criticalCount: Joi.number().integer().min(0).required(),
|
|
38
|
+
errors: Joi.array().items(Joi.object()).optional(),
|
|
39
|
+
traceId: Joi.string().required()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Logic } from './Logic.js'
|
|
2
|
+
|
|
3
|
+
export const Controllers = {
|
|
4
|
+
async listClients (ctx) {
|
|
5
|
+
const { traceId } = ctx.state
|
|
6
|
+
const { filter, page } = ctx.request.body
|
|
7
|
+
const result = await Logic.listClients({ filter, page, traceId })
|
|
8
|
+
ctx.reply(result)
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
async listMessages (ctx) {
|
|
12
|
+
const { traceId } = ctx.state
|
|
13
|
+
const { filter, page } = ctx.request.body
|
|
14
|
+
const result = await Logic.listMessages({ filter, page, traceId })
|
|
15
|
+
ctx.reply(result)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SqliteStore } from '../../singletons/SqliteStore.js'
|
|
2
|
+
import { MQTT_CLIENTS, MQTT_MESSAGES, MQTT_SUBSCRIPTIONS } from '../../db/Tables.js'
|
|
3
|
+
|
|
4
|
+
export const Logic = {
|
|
5
|
+
async listClients (params) {
|
|
6
|
+
const { filter = {}, page = {}, traceId } = params
|
|
7
|
+
try {
|
|
8
|
+
const options = {
|
|
9
|
+
limit: page.limit || 50,
|
|
10
|
+
offset: page.offset || 0,
|
|
11
|
+
// Only return currently connected clients (disconnectedAt is null)
|
|
12
|
+
where: { disconnectedAt: null }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Convert object filter to predicate function if needed
|
|
16
|
+
if (Object.keys(filter).length > 0) {
|
|
17
|
+
options.filter = (item) => {
|
|
18
|
+
return Object.entries(filter).every(([key, value]) => item[key] === value)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = SqliteStore.list(MQTT_CLIENTS, options)
|
|
23
|
+
return {
|
|
24
|
+
clients: result.data,
|
|
25
|
+
total: result.total,
|
|
26
|
+
traceId
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
status: 'error',
|
|
31
|
+
message: error.message,
|
|
32
|
+
traceId
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async listMessages (params) {
|
|
38
|
+
const { filter = {}, page = {}, traceId } = params
|
|
39
|
+
try {
|
|
40
|
+
const options = {
|
|
41
|
+
limit: page.limit || 50,
|
|
42
|
+
offset: page.offset || 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Convert object filter to predicate function if needed
|
|
46
|
+
if (Object.keys(filter).length > 0) {
|
|
47
|
+
options.filter = (item) => {
|
|
48
|
+
return Object.entries(filter).every(([key, value]) => item[key] === value)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = SqliteStore.list(MQTT_MESSAGES, options)
|
|
53
|
+
return {
|
|
54
|
+
messages: result.data,
|
|
55
|
+
total: result.total,
|
|
56
|
+
traceId
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
status: 'error',
|
|
61
|
+
message: error.message,
|
|
62
|
+
traceId
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import KoaRouter from 'koa-router'
|
|
2
|
+
import { Controllers } from './Controllers.js'
|
|
3
|
+
|
|
4
|
+
const Router = new KoaRouter()
|
|
5
|
+
const v1 = new KoaRouter({ prefix: '/v1/mqtt' })
|
|
6
|
+
|
|
7
|
+
v1.post('/clients/list', Controllers.listClients)
|
|
8
|
+
v1.post('/messages/list', Controllers.listMessages)
|
|
9
|
+
|
|
10
|
+
Router.use(v1.routes())
|
|
11
|
+
|
|
12
|
+
export { Router }
|