@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.
Files changed (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. 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 }