@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,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 }
|