@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,416 @@
1
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
2
+ import { v4 as uuidv4 } from 'uuid'
3
+ import { Logger } from '../../singletons/Logger.js'
4
+ import { LogBroadcaster } from '../../singletons/LogBroadcaster.js'
5
+
6
+ const COLLECTION_ENTRIES = 'logging_entries'
7
+ const COLLECTION_LOGS = 'logging_logs'
8
+
9
+ /**
10
+ * Parse filter string to extract conditions
11
+ * Basic implementation supporting common filters:
12
+ * - logName="projects/my-project/logs/my-log"
13
+ * - severity>=ERROR
14
+ * - timestamp>"2026-02-06T00:00:00Z"
15
+ * - labels.key="value"
16
+ */
17
+ const parseFilter = (filterString) => {
18
+ if (!filterString) return {}
19
+
20
+ const conditions = {}
21
+
22
+ // Extract logName filter
23
+ const logNameMatch = filterString.match(/logName="([^"]+)"/)
24
+ if (logNameMatch) {
25
+ conditions.logName = logNameMatch[1]
26
+ }
27
+
28
+ // Extract severity filter
29
+ const severityMatch = filterString.match(/severity(>=|>|=|<=|<)"?([^"\s]+)"?/)
30
+ if (severityMatch) {
31
+ conditions.severity = {
32
+ operator: severityMatch[1],
33
+ value: severityMatch[2]
34
+ }
35
+ }
36
+
37
+ // Extract timestamp filter
38
+ const timestampMatch = filterString.match(/timestamp(>=|>|=|<=|<)"([^"]+)"/)
39
+ if (timestampMatch) {
40
+ conditions.timestamp = {
41
+ operator: timestampMatch[1],
42
+ value: timestampMatch[2]
43
+ }
44
+ }
45
+
46
+ // Extract label filters
47
+ const labelMatches = filterString.matchAll(/labels\.(\w+)="([^"]+)"/g)
48
+ conditions.labels = {}
49
+ for (const match of labelMatches) {
50
+ conditions.labels[match[1]] = match[2]
51
+ }
52
+
53
+ return conditions
54
+ }
55
+
56
+ /**
57
+ * Apply filter conditions to an entry
58
+ */
59
+ const matchesFilter = (entry, conditions) => {
60
+ // Match logName
61
+ if (conditions.logName && entry.logName !== conditions.logName) {
62
+ return false
63
+ }
64
+
65
+ // Match severity
66
+ if (conditions.severity) {
67
+ const severityLevels = ['DEFAULT', 'DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY']
68
+ const entrySeverityIndex = severityLevels.indexOf(entry.severity || 'DEFAULT')
69
+ const filterSeverityIndex = severityLevels.indexOf(conditions.severity.value)
70
+
71
+ switch (conditions.severity.operator) {
72
+ case '>=':
73
+ if (entrySeverityIndex < filterSeverityIndex) return false
74
+ break
75
+ case '>':
76
+ if (entrySeverityIndex <= filterSeverityIndex) return false
77
+ break
78
+ case '=':
79
+ if (entrySeverityIndex !== filterSeverityIndex) return false
80
+ break
81
+ case '<=':
82
+ if (entrySeverityIndex > filterSeverityIndex) return false
83
+ break
84
+ case '<':
85
+ if (entrySeverityIndex >= filterSeverityIndex) return false
86
+ break
87
+ }
88
+ }
89
+
90
+ // Match timestamp
91
+ if (conditions.timestamp) {
92
+ const entryTime = new Date(entry.timestamp).getTime()
93
+ const filterTime = new Date(conditions.timestamp.value).getTime()
94
+
95
+ switch (conditions.timestamp.operator) {
96
+ case '>=':
97
+ if (entryTime < filterTime) return false
98
+ break
99
+ case '>':
100
+ if (entryTime <= filterTime) return false
101
+ break
102
+ case '=':
103
+ if (entryTime !== filterTime) return false
104
+ break
105
+ case '<=':
106
+ if (entryTime > filterTime) return false
107
+ break
108
+ case '<':
109
+ if (entryTime >= filterTime) return false
110
+ break
111
+ }
112
+ }
113
+
114
+ // Match labels
115
+ if (conditions.labels && Object.keys(conditions.labels).length > 0) {
116
+ if (!entry.labels) return false
117
+ for (const [key, value] of Object.entries(conditions.labels)) {
118
+ if (entry.labels[key] !== value) return false
119
+ }
120
+ }
121
+
122
+ return true
123
+ }
124
+
125
+ /**
126
+ * Generate page token
127
+ */
128
+ const generatePageToken = (offset) => {
129
+ return Buffer.from(JSON.stringify({ offset })).toString('base64')
130
+ }
131
+
132
+ /**
133
+ * Parse page token
134
+ */
135
+ const parsePageToken = (token) => {
136
+ try {
137
+ const decoded = Buffer.from(token, 'base64').toString('utf8')
138
+ return JSON.parse(decoded)
139
+ } catch {
140
+ return { offset: 0 }
141
+ }
142
+ }
143
+
144
+ export const Logic = {
145
+ /**
146
+ * Write log entries
147
+ */
148
+ async writeEntries (params) {
149
+ const { traceId, logName, resource, labels, entries, partialSuccess, dryRun } = params
150
+
151
+ Logger.log({
152
+ level: 'debug',
153
+ message: 'Writing log entries',
154
+ data: { traceId, logName, entriesCount: entries.length, dryRun }
155
+ })
156
+
157
+ if (dryRun) {
158
+ return {}
159
+ }
160
+
161
+ const now = new Date().toISOString()
162
+ const storedEntries = []
163
+
164
+ for (const entry of entries) {
165
+ // Merge entry with top-level fields
166
+ const logEntry = {
167
+ ...entry,
168
+ logName: entry.logName || logName,
169
+ resource: entry.resource || resource,
170
+ labels: { ...labels, ...entry.labels },
171
+ insertId: entry.insertId || uuidv4(),
172
+ timestamp: entry.timestamp || now,
173
+ receiveTimestamp: now,
174
+ // Add internal fields
175
+ _id: uuidv4(),
176
+ createdAt: now
177
+ }
178
+
179
+ // Extract nested fields from jsonPayload at write time
180
+ const payload = logEntry.jsonPayload
181
+ const json = typeof payload === 'string'
182
+ ? (() => { try { return JSON.parse(payload) } catch { return null } })()
183
+ : payload
184
+ if (json) {
185
+ // Stack trace
186
+ if (!logEntry.stackTrace) {
187
+ const stack = json.error?.stack || json.stack || json.exception?.stack
188
+ if (stack && typeof stack === 'string') logEntry.stackTrace = stack
189
+ }
190
+ // Error message
191
+ if (!logEntry.errorMessage) {
192
+ const errMsg = json.error?.message || json.exception?.message
193
+ if (errMsg) logEntry.errorMessage = errMsg
194
+ }
195
+ // Event (LogEvent enum)
196
+ if (!logEntry.event && json.event) logEntry.event = json.event
197
+ // Duration
198
+ if (logEntry.duration === undefined && json.duration !== undefined) {
199
+ logEntry.duration = Number(json.duration) || null
200
+ }
201
+ // @gokiteam/oops error.data fields
202
+ const errorData = json.error?.data
203
+ if (errorData && typeof errorData === 'object') {
204
+ if (!logEntry.errorCode && errorData.code) logEntry.errorCode = String(errorData.code)
205
+ if (!logEntry.errorStatus && errorData.status) logEntry.errorStatus = errorData.status
206
+ if (!logEntry.errorReason && errorData.reason) logEntry.errorReason = errorData.reason
207
+ if (!logEntry.errorResource && errorData.resource) logEntry.errorResource = errorData.resource
208
+ // Entity IDs from error.data.meta
209
+ const meta = errorData.meta
210
+ if (meta && typeof meta === 'object') {
211
+ if (!logEntry.propertyId && meta.propertyId) logEntry.propertyId = meta.propertyId
212
+ if (!logEntry.deviceId && meta.deviceId) logEntry.deviceId = meta.deviceId
213
+ if (!logEntry.doorId && meta.doorId) logEntry.doorId = meta.doorId
214
+ }
215
+ }
216
+ // Pub/Sub subscriber fields
217
+ if (!logEntry.subscriptionName && json.subscriptionName) logEntry.subscriptionName = json.subscriptionName
218
+ if (!logEntry.messageId && json.messageId) logEntry.messageId = json.messageId
219
+ // Domain entity IDs (top-level)
220
+ if (!logEntry.propertyId && json.propertyId) logEntry.propertyId = json.propertyId
221
+ if (!logEntry.deviceId && json.deviceId) logEntry.deviceId = json.deviceId
222
+ if (!logEntry.doorId && json.doorId) logEntry.doorId = json.doorId
223
+ // Trace ID (Goki services send traceId inside jsonPayload)
224
+ if (!logEntry.trace && json.traceId) logEntry.trace = json.traceId
225
+ }
226
+
227
+ // Store entry
228
+ SqliteStore.create(COLLECTION_ENTRIES, logEntry)
229
+ LogBroadcaster.broadcast(logEntry)
230
+ storedEntries.push(logEntry)
231
+
232
+ // Update logs collection (track unique log names)
233
+ const logNameParts = logEntry.logName.match(/^projects\/([^/]+)\/logs\/(.+)$/)
234
+ if (logNameParts) {
235
+ const [, projectId, logId] = logNameParts
236
+ const existingLog = SqliteStore.find(
237
+ COLLECTION_LOGS,
238
+ log => log.projectId === projectId && log.logId === logId
239
+ )[0]
240
+
241
+ if (!existingLog) {
242
+ SqliteStore.create(COLLECTION_LOGS, {
243
+ _id: uuidv4(),
244
+ projectId,
245
+ logId,
246
+ logName: logEntry.logName,
247
+ createdAt: now,
248
+ updatedAt: now
249
+ })
250
+ } else {
251
+ SqliteStore.update(COLLECTION_LOGS, existingLog.Id, {
252
+ updatedAt: now
253
+ }, '_id')
254
+ }
255
+ }
256
+ }
257
+
258
+ Logger.log({
259
+ level: 'info',
260
+ message: 'Log entries written successfully',
261
+ data: { traceId, entriesCount: storedEntries.length }
262
+ })
263
+
264
+ return {}
265
+ },
266
+
267
+ /**
268
+ * List log entries with filtering and pagination
269
+ */
270
+ async listEntries (params) {
271
+ const { traceId, resourceNames, filter, orderBy, pageSize = 50, pageToken } = params
272
+
273
+ Logger.log({
274
+ level: 'debug',
275
+ message: 'Listing log entries',
276
+ data: { traceId, filter, pageSize }
277
+ })
278
+
279
+ // Parse page token
280
+ const { offset = 0 } = pageToken ? parsePageToken(pageToken) : {}
281
+
282
+ // Parse filter
283
+ const conditions = parseFilter(filter)
284
+
285
+ // Filter entries
286
+ const filteredEntries = SqliteStore.find(COLLECTION_ENTRIES, entry => {
287
+ // Filter by resource names
288
+ if (resourceNames && resourceNames.length > 0) {
289
+ const entryResourceName = `projects/${entry.logName.split('/')[1]}`
290
+ if (!resourceNames.includes(entryResourceName)) return false
291
+ }
292
+
293
+ // Apply filter conditions
294
+ return matchesFilter(entry, conditions)
295
+ })
296
+
297
+ // Sort entries
298
+ const isDescending = orderBy === 'timestamp desc' || !orderBy
299
+ filteredEntries.sort((a, b) => {
300
+ const timeA = new Date(a.timestamp).getTime()
301
+ const timeB = new Date(b.timestamp).getTime()
302
+ return isDescending ? timeB - timeA : timeA - timeB
303
+ })
304
+
305
+ // Paginate
306
+ const total = filteredEntries.length
307
+ const paginatedEntries = filteredEntries.slice(offset, offset + pageSize)
308
+ const hasMore = offset + pageSize < total
309
+
310
+ // Generate next page token
311
+ const nextPageToken = hasMore ? generatePageToken(offset + pageSize) : undefined
312
+
313
+ // Remove internal fields from response
314
+ const entries = paginatedEntries.map(entry => {
315
+ const { _id, _createdAt, ...publicEntry } = entry
316
+ return publicEntry
317
+ })
318
+
319
+ Logger.log({
320
+ level: 'info',
321
+ message: 'Log entries listed successfully',
322
+ data: { traceId, entriesCount: entries.length, total, hasMore }
323
+ })
324
+
325
+ return {
326
+ entries,
327
+ nextPageToken
328
+ }
329
+ },
330
+
331
+ /**
332
+ * List all logs for a project
333
+ */
334
+ async listLogs (params) {
335
+ const { traceId, project, pageSize = 50, pageToken } = params
336
+
337
+ Logger.log({
338
+ level: 'debug',
339
+ message: 'Listing logs',
340
+ data: { traceId, project, pageSize }
341
+ })
342
+
343
+ // Parse page token
344
+ const { offset = 0 } = pageToken ? parsePageToken(pageToken) : {}
345
+
346
+ // Find logs for project
347
+ const logs = SqliteStore.find(COLLECTION_LOGS, log => log.projectId === project)
348
+
349
+ // Sort by creation time
350
+ logs.sort((a, b) => new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime())
351
+
352
+ // Paginate
353
+ const total = logs.length
354
+ const paginatedLogs = logs.slice(offset, offset + pageSize)
355
+ const hasMore = offset + pageSize < total
356
+
357
+ // Generate next page token
358
+ const nextPageToken = hasMore ? generatePageToken(offset + pageSize) : undefined
359
+
360
+ // Return log names only
361
+ const logNames = paginatedLogs.map(log => log.logName)
362
+
363
+ Logger.log({
364
+ level: 'info',
365
+ message: 'Logs listed successfully',
366
+ data: { traceId, logsCount: logNames.length, total, hasMore }
367
+ })
368
+
369
+ return {
370
+ logNames,
371
+ nextPageToken
372
+ }
373
+ },
374
+
375
+ /**
376
+ * Delete a log and all its entries
377
+ */
378
+ async deleteLog (params) {
379
+ const { traceId, project, logId } = params
380
+
381
+ Logger.log({
382
+ level: 'debug',
383
+ message: 'Deleting log',
384
+ data: { traceId, project, logId }
385
+ })
386
+
387
+ const logName = `projects/${project}/logs/${logId}`
388
+
389
+ // Find and delete log metadata
390
+ const log = SqliteStore.find(
391
+ COLLECTION_LOGS,
392
+ log => log.projectId === project && log.logId === logId
393
+ )[0]
394
+
395
+ if (log) {
396
+ SqliteStore.delete(COLLECTION_LOGS, log.Id, '_id')
397
+ }
398
+
399
+ // Delete all entries for this log
400
+ const entries = SqliteStore.find(COLLECTION_ENTRIES, entry => entry.logName === logName)
401
+ let deletedCount = 0
402
+
403
+ for (const entry of entries) {
404
+ SqliteStore.delete(COLLECTION_ENTRIES, entry.Id, '_id')
405
+ deletedCount++
406
+ }
407
+
408
+ Logger.log({
409
+ level: 'info',
410
+ message: 'Log deleted successfully',
411
+ data: { traceId, project, logId, deletedEntriesCount: deletedCount }
412
+ })
413
+
414
+ return {}
415
+ }
416
+ }
@@ -0,0 +1,36 @@
1
+ import KoaRouter from 'koa-router'
2
+ import { Controllers } from './Controllers.js'
3
+
4
+ const Router = new KoaRouter()
5
+
6
+ // Cloud Logging API v2 routes
7
+ const v2 = new KoaRouter({ prefix: '/v2' })
8
+
9
+ // Write log entries
10
+ v2.post('/entries\\:write', Controllers.writeEntries)
11
+
12
+ // List log entries (POST with filter in body)
13
+ v2.post('/entries\\:list', Controllers.listEntries)
14
+
15
+ // Debug endpoint
16
+ v2.get('/debug', async (ctx) => {
17
+ ctx.body = {
18
+ message: 'Logging API debug endpoint',
19
+ routes: [
20
+ 'POST /v2/entries:write',
21
+ 'POST /v2/entries:list',
22
+ 'GET /v2/projects/:project/logs',
23
+ 'DELETE /v2/projects/:project/logs/:logId'
24
+ ]
25
+ }
26
+ })
27
+
28
+ // List all logs for a project
29
+ v2.get('/projects/:project/logs', Controllers.listLogs)
30
+
31
+ // Delete a log
32
+ v2.delete('/projects/:project/logs/:logId', Controllers.deleteLog)
33
+
34
+ Router.use(v2.routes())
35
+
36
+ export { Router }
@@ -0,0 +1,82 @@
1
+ import Joi from 'joi'
2
+
3
+ // Cloud Logging severity levels
4
+ const SEVERITY_LEVELS = [
5
+ 'DEFAULT',
6
+ 'DEBUG',
7
+ 'INFO',
8
+ 'NOTICE',
9
+ 'WARNING',
10
+ 'ERROR',
11
+ 'CRITICAL',
12
+ 'ALERT',
13
+ 'EMERGENCY'
14
+ ]
15
+
16
+ // MonitoredResource schema
17
+ const MonitoredResourceSchema = Joi.object({
18
+ type: Joi.string().required(),
19
+ labels: Joi.object().pattern(Joi.string(), Joi.string()).optional()
20
+ })
21
+
22
+ // LogEntry schema
23
+ const LogEntrySchema = Joi.object({
24
+ logName: Joi.string().pattern(/^projects\/[^/]+\/logs\/[^/]+$/).optional(),
25
+ resource: MonitoredResourceSchema.optional(),
26
+ timestamp: Joi.string().isoDate().optional(),
27
+ receiveTimestamp: Joi.string().isoDate().optional(),
28
+ severity: Joi.string().valid(...SEVERITY_LEVELS).optional(),
29
+ insertId: Joi.string().optional(),
30
+ httpRequest: Joi.object().optional(),
31
+ labels: Joi.object().pattern(Joi.string(), Joi.string()).optional(),
32
+ operation: Joi.object({
33
+ id: Joi.string().optional(),
34
+ producer: Joi.string().optional(),
35
+ first: Joi.boolean().optional(),
36
+ last: Joi.boolean().optional()
37
+ }).optional(),
38
+ trace: Joi.string().optional(),
39
+ spanId: Joi.string().optional(),
40
+ traceSampled: Joi.boolean().optional(),
41
+ sourceLocation: Joi.object({
42
+ file: Joi.string().optional(),
43
+ line: Joi.number().optional(),
44
+ function: Joi.string().optional()
45
+ }).optional(),
46
+ // One of the payload fields must be present
47
+ textPayload: Joi.string().optional(),
48
+ jsonPayload: Joi.object().optional(),
49
+ protoPayload: Joi.object().optional()
50
+ }).or('textPayload', 'jsonPayload', 'protoPayload')
51
+
52
+ // Write entries request schema
53
+ export const WriteEntriesRequestSchema = Joi.object({
54
+ logName: Joi.string().pattern(/^projects\/[^/]+\/logs\/[^/]+$/).optional(),
55
+ resource: MonitoredResourceSchema.optional(),
56
+ labels: Joi.object().pattern(Joi.string(), Joi.string()).optional(),
57
+ entries: Joi.array().items(LogEntrySchema).min(1).required(),
58
+ partialSuccess: Joi.boolean().optional(),
59
+ dryRun: Joi.boolean().optional()
60
+ })
61
+
62
+ // List entries request schema
63
+ export const ListEntriesRequestSchema = Joi.object({
64
+ resourceNames: Joi.array().items(Joi.string()).optional(),
65
+ filter: Joi.string().optional(),
66
+ orderBy: Joi.string().optional(),
67
+ pageSize: Joi.number().integer().min(1).max(1000).optional(),
68
+ pageToken: Joi.string().optional()
69
+ })
70
+
71
+ // Delete log request schema (path parameters)
72
+ export const DeleteLogRequestSchema = Joi.object({
73
+ project: Joi.string().required(),
74
+ logId: Joi.string().required()
75
+ })
76
+
77
+ // List logs request schema
78
+ export const ListLogsRequestSchema = Joi.object({
79
+ project: Joi.string().required(),
80
+ pageSize: Joi.number().integer().min(1).max(1000).optional(),
81
+ pageToken: Joi.string().optional()
82
+ })
@@ -0,0 +1,108 @@
1
+ import Koa from 'koa'
2
+ import bodyParser from 'koa-bodyparser'
3
+ import KoaRouter from 'koa-router'
4
+ import { Application } from '../../configs/Application.js'
5
+ import { Logger } from '../../singletons/Logger.js'
6
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
7
+ import { ErrorCatcher } from '../../middleware/ErrorCatcher.js'
8
+ import { Reply } from '../../middleware/Reply.js'
9
+ import { TraceId } from '../../middleware/TraceId.js'
10
+ import { Router as LoggingRouter } from './Router.js'
11
+
12
+ const { environment, runId, ports } = Application
13
+ const LOGGING_PORT = ports.logging || 8087
14
+
15
+ // Register collections (no-op for SQLite - tables created at DB initialization)
16
+ const registerCollections = () => {
17
+ Logger.log({
18
+ level: 'info',
19
+ message: 'Logging tables ready (SQLite)',
20
+ data: { tables: ['logging_entries', 'logging_logs'] }
21
+ })
22
+ }
23
+
24
+ const start = async () => {
25
+ try {
26
+ // Register collections (no-op for SQLite)
27
+ registerCollections()
28
+
29
+ // Create Koa app
30
+ const app = new Koa()
31
+
32
+ // Middleware
33
+ app.use(ErrorCatcher())
34
+ app.use(TraceId())
35
+ app.use(Reply())
36
+ app.use(bodyParser())
37
+
38
+ // Router
39
+ const router = new KoaRouter()
40
+ router.use(LoggingRouter.routes())
41
+
42
+ app.use(router.routes())
43
+ app.use(router.allowedMethods())
44
+
45
+ // Start server
46
+ const server = app.listen(LOGGING_PORT, () => {
47
+ Logger.log({
48
+ level: 'info',
49
+ message: 'Cloud Logging emulation server started',
50
+ data: {
51
+ environment,
52
+ runId,
53
+ port: LOGGING_PORT,
54
+ endpoints: [
55
+ 'POST /v2/entries:write',
56
+ 'POST /v2/entries:list',
57
+ 'GET /v2/projects/{project}/logs',
58
+ 'DELETE /v2/projects/{project}/logs/{logId}'
59
+ ]
60
+ }
61
+ })
62
+ })
63
+
64
+ return server
65
+ } catch (error) {
66
+ Logger.log({
67
+ level: 'error',
68
+ message: 'Failed to start Cloud Logging server',
69
+ data: { error: error.message, stack: error.stack }
70
+ })
71
+ process.exit(1)
72
+ }
73
+ }
74
+
75
+ const shutdown = async () => {
76
+ Logger.log({
77
+ level: 'info',
78
+ message: 'Shutting down Cloud Logging server'
79
+ })
80
+ SqliteStore.shutdown()
81
+ process.exit(0)
82
+ }
83
+
84
+ // Start server if this file is run directly
85
+ if (import.meta.url === `file://${process.argv[1]}`) {
86
+ start()
87
+
88
+ process.on('SIGINT', shutdown)
89
+ process.on('SIGTERM', shutdown)
90
+
91
+ process.on('unhandledRejection', error => {
92
+ Logger.log({
93
+ level: 'error',
94
+ message: 'Unhandled rejection detected',
95
+ data: { error: error.message, stack: error.stack }
96
+ })
97
+ })
98
+
99
+ process.on('uncaughtException', error => {
100
+ Logger.log({
101
+ level: 'error',
102
+ message: 'Uncaught exception detected',
103
+ data: { error: error.message, stack: error.stack }
104
+ })
105
+ })
106
+ }
107
+
108
+ export { start, shutdown }