@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,687 @@
|
|
|
1
|
+
import Docker from 'dockerode'
|
|
2
|
+
import { PassThrough } from 'stream'
|
|
3
|
+
import { SqliteStore } from '../singletons/SqliteStore.js'
|
|
4
|
+
import { LOGGING_ENTRIES, LOGGING_LOGS } from '../db/Tables.js'
|
|
5
|
+
import { Logger } from '../singletons/Logger.js'
|
|
6
|
+
import { LogBroadcaster } from '../singletons/LogBroadcaster.js'
|
|
7
|
+
|
|
8
|
+
// Infrastructure containers to exclude from log capture
|
|
9
|
+
const DEFAULT_EXCLUDED_CONTAINERS = [
|
|
10
|
+
'goki-dev-tools-backend',
|
|
11
|
+
'goki-dev-tools-frontend',
|
|
12
|
+
'goki-redis',
|
|
13
|
+
'goki-pubsub-emulator',
|
|
14
|
+
'goki-firestore-emulator'
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export class DockerLogsConsumer {
|
|
18
|
+
constructor (options = {}) {
|
|
19
|
+
this.docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
|
20
|
+
this.streams = new Map()
|
|
21
|
+
this.lineBuffers = new Map()
|
|
22
|
+
this.enabled = options.enabled !== false
|
|
23
|
+
this.networkName = options.networkName || 'goki-network'
|
|
24
|
+
this.excludedContainers = options.excludedContainers || DEFAULT_EXCLUDED_CONTAINERS
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async start () {
|
|
28
|
+
if (!this.enabled) {
|
|
29
|
+
Logger.log({
|
|
30
|
+
level: 'info',
|
|
31
|
+
message: 'Docker console log capture disabled'
|
|
32
|
+
})
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// Find all containers on the goki-network (except excluded infrastructure)
|
|
37
|
+
const containers = await this.docker.listContainers({
|
|
38
|
+
filters: { network: [this.networkName] }
|
|
39
|
+
})
|
|
40
|
+
const eligible = containers.filter(c => {
|
|
41
|
+
const name = c.Names[0].replace(/^\//, '')
|
|
42
|
+
return !this.excludedContainers.includes(name)
|
|
43
|
+
})
|
|
44
|
+
Logger.log({
|
|
45
|
+
level: 'info',
|
|
46
|
+
message: `Found ${eligible.length} containers for log capture on ${this.networkName}`,
|
|
47
|
+
data: {
|
|
48
|
+
capturing: eligible.map(c => c.Names[0]),
|
|
49
|
+
excluded: containers.length - eligible.length
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
for (const containerInfo of eligible) {
|
|
53
|
+
await this.attachToContainer(containerInfo)
|
|
54
|
+
}
|
|
55
|
+
this.watchForNewContainers()
|
|
56
|
+
} catch (error) {
|
|
57
|
+
Logger.log({
|
|
58
|
+
level: 'error',
|
|
59
|
+
message: 'Failed to start Docker log capture',
|
|
60
|
+
data: { error: error.message }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async attachToContainer (containerInfo) {
|
|
66
|
+
const containerId = containerInfo.Id
|
|
67
|
+
const containerName = containerInfo.Names[0].replace(/^\//, '')
|
|
68
|
+
try {
|
|
69
|
+
const container = this.docker.getContainer(containerId)
|
|
70
|
+
const containerDetails = await container.inspect()
|
|
71
|
+
const serviceName = containerDetails.Config.Labels?.['goki.service'] || containerName
|
|
72
|
+
const stream = await container.logs({
|
|
73
|
+
follow: true,
|
|
74
|
+
stdout: true,
|
|
75
|
+
stderr: true,
|
|
76
|
+
timestamps: true
|
|
77
|
+
})
|
|
78
|
+
Logger.log({
|
|
79
|
+
level: 'info',
|
|
80
|
+
message: 'Attached to container logs',
|
|
81
|
+
data: { container: containerName, service: serviceName }
|
|
82
|
+
})
|
|
83
|
+
const stdout = new PassThrough()
|
|
84
|
+
const stderr = new PassThrough()
|
|
85
|
+
this.docker.modem.demuxStream(stream, stdout, stderr)
|
|
86
|
+
stdout.on('data', (chunk) => {
|
|
87
|
+
this.parseLogLines(chunk.toString('utf-8'), containerName, serviceName, 1)
|
|
88
|
+
})
|
|
89
|
+
stderr.on('data', (chunk) => {
|
|
90
|
+
this.parseLogLines(chunk.toString('utf-8'), containerName, serviceName, 2)
|
|
91
|
+
})
|
|
92
|
+
stream.on('end', () => {
|
|
93
|
+
Logger.log({
|
|
94
|
+
level: 'info',
|
|
95
|
+
message: 'Container log stream ended',
|
|
96
|
+
data: { container: containerName, service: serviceName }
|
|
97
|
+
})
|
|
98
|
+
this.flushLineBuffer(containerName, serviceName, 1)
|
|
99
|
+
this.flushLineBuffer(containerName, serviceName, 2)
|
|
100
|
+
this.lineBuffers.delete(`${containerName}:1`)
|
|
101
|
+
this.lineBuffers.delete(`${containerName}:2`)
|
|
102
|
+
this.streams.delete(containerId)
|
|
103
|
+
})
|
|
104
|
+
stream.on('error', (error) => {
|
|
105
|
+
Logger.log({
|
|
106
|
+
level: 'error',
|
|
107
|
+
message: 'Container log stream error',
|
|
108
|
+
data: { container: containerName, service: serviceName, error: error.message }
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
this.streams.set(containerId, stream)
|
|
112
|
+
} catch (error) {
|
|
113
|
+
Logger.log({
|
|
114
|
+
level: 'error',
|
|
115
|
+
message: 'Failed to attach to container',
|
|
116
|
+
data: { container: containerName, error: error.message }
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getLineBuffer (containerName, streamType) {
|
|
122
|
+
const key = `${containerName}:${streamType}`
|
|
123
|
+
if (!this.lineBuffers.has(key)) {
|
|
124
|
+
this.lineBuffers.set(key, { lines: [], braceDepth: 0, firstTimestamp: null, timer: null })
|
|
125
|
+
}
|
|
126
|
+
return this.lineBuffers.get(key)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
flushLineBuffer (containerName, serviceName, streamType) {
|
|
130
|
+
const buf = this.getLineBuffer(containerName, streamType)
|
|
131
|
+
if (buf.timer) {
|
|
132
|
+
clearTimeout(buf.timer)
|
|
133
|
+
buf.timer = null
|
|
134
|
+
}
|
|
135
|
+
if (buf.lines.length === 0) return
|
|
136
|
+
const merged = buf.lines.join('\n')
|
|
137
|
+
const timestamp = buf.firstTimestamp
|
|
138
|
+
buf.lines = []
|
|
139
|
+
buf.braceDepth = 0
|
|
140
|
+
buf.firstTimestamp = null
|
|
141
|
+
this.processLogLine(timestamp ? `${timestamp} ${merged}` : merged, containerName, serviceName, streamType)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
parseLogLines (data, containerName, serviceName, streamType) {
|
|
145
|
+
const lines = data.split('\n').filter(line => line.trim())
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(.*)/)
|
|
148
|
+
const timestamp = timestampMatch ? timestampMatch[1] : null
|
|
149
|
+
const content = timestampMatch ? timestampMatch[2] : line
|
|
150
|
+
const buf = this.getLineBuffer(containerName, streamType)
|
|
151
|
+
// Inside an active multi-line object buffer
|
|
152
|
+
if (buf.braceDepth > 0) {
|
|
153
|
+
buf.lines.push(content)
|
|
154
|
+
buf.braceDepth += (content.match(/\{/g) || []).length
|
|
155
|
+
buf.braceDepth -= (content.match(/\}/g) || []).length
|
|
156
|
+
if (buf.braceDepth <= 0) {
|
|
157
|
+
buf.braceDepth = 0
|
|
158
|
+
this.flushLineBuffer(containerName, serviceName, streamType)
|
|
159
|
+
} else {
|
|
160
|
+
this.resetBufferTimer(containerName, serviceName, streamType)
|
|
161
|
+
}
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
const trimmed = content.trim()
|
|
165
|
+
// Opening brace on its own line — start new object buffer
|
|
166
|
+
if (trimmed === '{') {
|
|
167
|
+
if (buf.lines.length > 0) {
|
|
168
|
+
this.flushLineBuffer(containerName, serviceName, streamType)
|
|
169
|
+
}
|
|
170
|
+
buf.lines.push(content)
|
|
171
|
+
buf.braceDepth = 1
|
|
172
|
+
buf.firstTimestamp = timestamp
|
|
173
|
+
this.resetBufferTimer(containerName, serviceName, streamType)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
// Orphan object property line (from a previous chunk that split mid-object)
|
|
177
|
+
// Pattern: " key: value," or " key: {" — starts with optional whitespace + word + colon
|
|
178
|
+
if (this.isObjectPropertyLine(trimmed)) {
|
|
179
|
+
if (!buf.firstTimestamp) buf.firstTimestamp = timestamp
|
|
180
|
+
buf.lines.push(content)
|
|
181
|
+
buf.braceDepth += (content.match(/\{/g) || []).length
|
|
182
|
+
buf.braceDepth -= (content.match(/\}/g) || []).length
|
|
183
|
+
if (buf.braceDepth < 0) buf.braceDepth = 0
|
|
184
|
+
this.resetBufferTimer(containerName, serviceName, streamType)
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
// Lone closing brace — end of orphan object from previous chunk
|
|
188
|
+
if (trimmed === '}' && buf.lines.length > 0) {
|
|
189
|
+
buf.lines.push(content)
|
|
190
|
+
buf.braceDepth = 0
|
|
191
|
+
this.flushLineBuffer(containerName, serviceName, streamType)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
this.processLogLine(line, containerName, serviceName, streamType)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect if a line looks like a JS object property (key: value pattern).
|
|
200
|
+
* Used to identify orphan continuation lines from objects split across chunks.
|
|
201
|
+
*/
|
|
202
|
+
isObjectPropertyLine (trimmed) {
|
|
203
|
+
// Match: word followed by colon and a value (string, number, object, array, etc.)
|
|
204
|
+
// e.g. "level: 'info'," or "messageAttributes: { traceId: 'abc' }," or "count: 42,"
|
|
205
|
+
// But NOT things like "INFO: Starting server" (uppercase log level prefix)
|
|
206
|
+
if (/^\w+:\s+/.test(trimmed)) {
|
|
207
|
+
// Exclude plain text log patterns: "LEVEL: message"
|
|
208
|
+
if (/^(INFO|WARNING|WARN|ERROR|SEVERE|DEBUG|FINE|FINER|FINEST|CONFIG):\s/i.test(trimmed)) {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
return false
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
resetBufferTimer (containerName, serviceName, streamType) {
|
|
217
|
+
const buf = this.getLineBuffer(containerName, streamType)
|
|
218
|
+
if (buf.timer) clearTimeout(buf.timer)
|
|
219
|
+
buf.timer = setTimeout(() => {
|
|
220
|
+
this.flushLineBuffer(containerName, serviceName, streamType)
|
|
221
|
+
}, 500)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
tryParseJsObject (text) {
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(text)
|
|
227
|
+
} catch {
|
|
228
|
+
// Try converting JS object notation to JSON
|
|
229
|
+
try {
|
|
230
|
+
// Quote unquoted keys and convert single-quoted values to double-quoted
|
|
231
|
+
let jsonified = text
|
|
232
|
+
// Replace all single-quoted strings with double-quoted (handles colons in values)
|
|
233
|
+
jsonified = jsonified.replace(/'([^']*)'/g, (_, content) => `"${content.replace(/"/g, '\\"')}"`)
|
|
234
|
+
|
|
235
|
+
// Quote unquoted object keys (word chars before colon, not inside quotes)
|
|
236
|
+
jsonified = jsonified.replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":')
|
|
237
|
+
// Remove trailing commas
|
|
238
|
+
jsonified = jsonified.replace(/,(\s*[}\]])/g, '$1')
|
|
239
|
+
return JSON.parse(jsonified)
|
|
240
|
+
} catch {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async processLogLine (line, containerName, serviceName, streamType) {
|
|
247
|
+
try {
|
|
248
|
+
const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+([\s\S]*)/)
|
|
249
|
+
const timestamp = timestampMatch ? timestampMatch[1] : new Date().toISOString()
|
|
250
|
+
const message = timestampMatch ? timestampMatch[2] : line
|
|
251
|
+
let logEntry, dedupeKey
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(message)
|
|
254
|
+
logEntry = this.parseWinstonLog(parsed, containerName, serviceName, timestamp)
|
|
255
|
+
dedupeKey = {
|
|
256
|
+
timestamp: parsed.timestamp,
|
|
257
|
+
message: parsed.message,
|
|
258
|
+
level: parsed.level
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
const jsObj = message.includes('\n') ? this.tryParseJsObject(message) : null
|
|
262
|
+
if (jsObj) {
|
|
263
|
+
logEntry = this.parseWinstonLog(jsObj, containerName, serviceName, timestamp)
|
|
264
|
+
logEntry.protoPayload = message
|
|
265
|
+
dedupeKey = {
|
|
266
|
+
timestamp: jsObj.timestamp || timestamp,
|
|
267
|
+
message: jsObj.message,
|
|
268
|
+
level: jsObj.level
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
const isMultiLineObject = message.includes('\n') && message.trim().startsWith('{')
|
|
272
|
+
if (isMultiLineObject) {
|
|
273
|
+
logEntry = this.parseConsoleObject(message, containerName, serviceName, timestamp)
|
|
274
|
+
} else {
|
|
275
|
+
logEntry = this.parsePlainTextLog(message, containerName, serviceName, timestamp, streamType)
|
|
276
|
+
}
|
|
277
|
+
dedupeKey = {
|
|
278
|
+
timestamp,
|
|
279
|
+
textPayload: logEntry.textPayload
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const timeWindow = 2000
|
|
284
|
+
const timestampDate = new Date(dedupeKey.timestamp)
|
|
285
|
+
const textToCheck = dedupeKey.message || dedupeKey.textPayload
|
|
286
|
+
const startTime = new Date(timestampDate.getTime() - timeWindow).toISOString()
|
|
287
|
+
const endTime = new Date(timestampDate.getTime() + timeWindow).toISOString()
|
|
288
|
+
const isDuplicate = SqliteStore.exists(LOGGING_ENTRIES,
|
|
289
|
+
'text_payload = ? AND service_name = ? AND timestamp >= ? AND timestamp <= ?',
|
|
290
|
+
[textToCheck, serviceName, startTime, endTime]
|
|
291
|
+
)
|
|
292
|
+
if (isDuplicate) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
const logId = serviceName || containerName
|
|
296
|
+
try {
|
|
297
|
+
SqliteStore.create(LOGGING_LOGS, {
|
|
298
|
+
projectId: 'dev',
|
|
299
|
+
logId,
|
|
300
|
+
logName: `projects/dev/logs/${serviceName}`,
|
|
301
|
+
createdAt: new Date().toISOString(),
|
|
302
|
+
updatedAt: new Date().toISOString()
|
|
303
|
+
})
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (!error.message.includes('UNIQUE')) {
|
|
306
|
+
throw error
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
await SqliteStore.create(LOGGING_ENTRIES, logEntry)
|
|
310
|
+
LogBroadcaster.broadcast(logEntry)
|
|
311
|
+
} catch (error) {
|
|
312
|
+
Logger.log({
|
|
313
|
+
level: 'error',
|
|
314
|
+
message: 'Failed to process log line',
|
|
315
|
+
data: { container: containerName, error: error.message }
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
findNestedValue (obj, key, maxDepth = 3) {
|
|
321
|
+
if (!obj || typeof obj !== 'object' || maxDepth <= 0) return undefined
|
|
322
|
+
if (obj[key] !== undefined) return obj[key]
|
|
323
|
+
for (const val of Object.values(obj)) {
|
|
324
|
+
if (val && typeof val === 'object') {
|
|
325
|
+
const found = this.findNestedValue(val, key, maxDepth - 1)
|
|
326
|
+
if (found !== undefined) return found
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return undefined
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
parseWinstonLog (parsed, containerName, serviceName, timestamp) {
|
|
333
|
+
const severityMap = {
|
|
334
|
+
error: 'ERROR',
|
|
335
|
+
warn: 'WARNING',
|
|
336
|
+
info: 'INFO',
|
|
337
|
+
http: 'INFO',
|
|
338
|
+
verbose: 'DEBUG',
|
|
339
|
+
debug: 'DEBUG'
|
|
340
|
+
}
|
|
341
|
+
const logName = `projects/dev/logs/${serviceName}`
|
|
342
|
+
const severity = severityMap[parsed.level] || 'INFO'
|
|
343
|
+
// Merge log.metadata into labels (minus traceId/requestId which get own columns)
|
|
344
|
+
const metadata = parsed.metadata || {}
|
|
345
|
+
const labels = { container: containerName, ...metadata }
|
|
346
|
+
delete labels.traceId
|
|
347
|
+
delete labels.requestId
|
|
348
|
+
// Build jsonPayload: original object minus standard logger envelope fields
|
|
349
|
+
// These fields are already extracted into dedicated columns
|
|
350
|
+
const envelopeFields = ['level', 'message', 'msg', 'timestamp', 'metadata']
|
|
351
|
+
const payload = {}
|
|
352
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
353
|
+
if (!envelopeFields.includes(key)) {
|
|
354
|
+
payload[key] = value
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const hasPayload = Object.keys(payload).length > 0
|
|
358
|
+
const logEntry = {
|
|
359
|
+
logName,
|
|
360
|
+
serviceName,
|
|
361
|
+
severity,
|
|
362
|
+
source: 'docker',
|
|
363
|
+
level: parsed.level || null,
|
|
364
|
+
timestamp: parsed.timestamp || timestamp,
|
|
365
|
+
receiveTimestamp: new Date().toISOString(),
|
|
366
|
+
textPayload: parsed.message || parsed.msg || parsed.event,
|
|
367
|
+
jsonPayload: hasPayload ? payload : null,
|
|
368
|
+
labels
|
|
369
|
+
}
|
|
370
|
+
// Extract traceId — check metadata first, then search entire object
|
|
371
|
+
const traceId = metadata.traceId || this.findNestedValue(parsed, 'traceId')
|
|
372
|
+
if (traceId) logEntry.trace = traceId
|
|
373
|
+
// Extract requestId
|
|
374
|
+
const requestId = metadata.requestId || parsed.requestId
|
|
375
|
+
if (requestId) logEntry.insertId = requestId
|
|
376
|
+
// Extract error info and stack trace
|
|
377
|
+
const errorObj = parsed.error || metadata.error
|
|
378
|
+
if (errorObj || parsed.stack) {
|
|
379
|
+
if (typeof errorObj === 'string') {
|
|
380
|
+
logEntry.errorMessage = errorObj
|
|
381
|
+
} else if (errorObj) {
|
|
382
|
+
logEntry.errorMessage = errorObj.message || parsed.message
|
|
383
|
+
const stack = errorObj.stack || parsed.stack
|
|
384
|
+
if (stack && typeof stack === 'string') logEntry.stackTrace = stack
|
|
385
|
+
// Extract @gokiteam/oops error.data fields
|
|
386
|
+
const errorData = errorObj.data
|
|
387
|
+
if (errorData && typeof errorData === 'object') {
|
|
388
|
+
if (errorData.code) logEntry.errorCode = String(errorData.code)
|
|
389
|
+
if (errorData.status) logEntry.errorStatus = errorData.status
|
|
390
|
+
if (errorData.reason) logEntry.errorReason = errorData.reason
|
|
391
|
+
if (errorData.resource) logEntry.errorResource = errorData.resource
|
|
392
|
+
// Extract entity IDs from error.data.meta
|
|
393
|
+
const meta = errorData.meta
|
|
394
|
+
if (meta && typeof meta === 'object') {
|
|
395
|
+
if (meta.propertyId) logEntry.propertyId = meta.propertyId
|
|
396
|
+
if (meta.deviceId) logEntry.deviceId = meta.deviceId
|
|
397
|
+
if (meta.doorId) logEntry.doorId = meta.doorId
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
// parsed.stack without error object
|
|
402
|
+
logEntry.errorMessage = parsed.message
|
|
403
|
+
if (typeof parsed.stack === 'string') logEntry.stackTrace = parsed.stack
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Extract event (LogEvent enum - very common across all services)
|
|
407
|
+
if (parsed.event) logEntry.event = parsed.event
|
|
408
|
+
// Extract duration (timing metrics)
|
|
409
|
+
if (parsed.duration !== undefined && parsed.duration !== null) {
|
|
410
|
+
logEntry.duration = Number(parsed.duration) || null
|
|
411
|
+
}
|
|
412
|
+
// Extract Pub/Sub subscriber fields
|
|
413
|
+
if (parsed.subscriptionName) logEntry.subscriptionName = parsed.subscriptionName
|
|
414
|
+
if (parsed.messageId) logEntry.messageId = parsed.messageId
|
|
415
|
+
// Extract domain entity IDs (spread at top level in Winston data)
|
|
416
|
+
if (parsed.propertyId && !logEntry.propertyId) logEntry.propertyId = parsed.propertyId
|
|
417
|
+
if (parsed.deviceId && !logEntry.deviceId) logEntry.deviceId = parsed.deviceId
|
|
418
|
+
if (parsed.doorId && !logEntry.doorId) logEntry.doorId = parsed.doorId
|
|
419
|
+
return logEntry
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parse a multi-line console.log() object that couldn't be converted to JSON.
|
|
424
|
+
* Extracts message, level, error, and traceId via regex from the raw text.
|
|
425
|
+
* The full original text is preserved in protoPayload since we can't reliably
|
|
426
|
+
* parse the original structure (contains non-JSON values like Buffer, Circular, etc.).
|
|
427
|
+
*/
|
|
428
|
+
parseConsoleObject (text, containerName, serviceName, timestamp) {
|
|
429
|
+
const logName = `projects/dev/logs/${serviceName}`
|
|
430
|
+
const labels = { container: containerName }
|
|
431
|
+
// Extract key fields from JS object notation via regex
|
|
432
|
+
const extractField = (key) => {
|
|
433
|
+
const match = text.match(new RegExp(`${key}:\\s*'([^']*)'`))
|
|
434
|
+
return match ? match[1] : null
|
|
435
|
+
}
|
|
436
|
+
const message = extractField('message') || extractField('msg') || extractField('event')
|
|
437
|
+
const level = extractField('level')
|
|
438
|
+
const traceId = extractField('traceId')
|
|
439
|
+
const error = extractField('error')
|
|
440
|
+
const severityMap = { error: 'ERROR', warn: 'WARNING', info: 'INFO', debug: 'DEBUG' }
|
|
441
|
+
const logEntry = {
|
|
442
|
+
logName,
|
|
443
|
+
serviceName,
|
|
444
|
+
severity: severityMap[level] || this.detectSeverity(text),
|
|
445
|
+
source: 'docker',
|
|
446
|
+
level: level || null,
|
|
447
|
+
timestamp: extractField('timestamp') || timestamp,
|
|
448
|
+
receiveTimestamp: new Date().toISOString(),
|
|
449
|
+
textPayload: message || text.split('\n').map(l => l.trim()).join(' '),
|
|
450
|
+
protoPayload: text,
|
|
451
|
+
labels
|
|
452
|
+
}
|
|
453
|
+
if (traceId) logEntry.trace = traceId
|
|
454
|
+
if (error) logEntry.errorMessage = error
|
|
455
|
+
// Extract stack trace from raw text
|
|
456
|
+
// JS object notation uses string concatenation: stack: 'Error\n' + ' at func (file:1:1)\n' + ...
|
|
457
|
+
// Reconstruct by extracting all quoted segments after "stack:" and joining them
|
|
458
|
+
const stackFieldMatch = text.match(/stack:\s*'/)
|
|
459
|
+
if (stackFieldMatch) {
|
|
460
|
+
const startIdx = stackFieldMatch.index + stackFieldMatch[0].length - 1
|
|
461
|
+
const stackStr = this.extractConcatenatedString(text, startIdx)
|
|
462
|
+
if (stackStr && stackStr.includes('\n at ')) {
|
|
463
|
+
logEntry.stackTrace = stackStr
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Extract event field
|
|
467
|
+
const event = extractField('event')
|
|
468
|
+
if (event) logEntry.event = event
|
|
469
|
+
// Extract duration (number, not quoted)
|
|
470
|
+
const durationMatch = text.match(/duration:\s*(\d+(?:\.\d+)?)/)
|
|
471
|
+
if (durationMatch) logEntry.duration = Number(durationMatch[1]) || null
|
|
472
|
+
// Extract Pub/Sub fields
|
|
473
|
+
const subscriptionName = extractField('subscriptionName')
|
|
474
|
+
if (subscriptionName) logEntry.subscriptionName = subscriptionName
|
|
475
|
+
const messageId = extractField('messageId')
|
|
476
|
+
if (messageId) logEntry.messageId = messageId
|
|
477
|
+
// Extract domain entity IDs
|
|
478
|
+
const propertyId = extractField('propertyId')
|
|
479
|
+
if (propertyId) logEntry.propertyId = propertyId
|
|
480
|
+
const deviceId = extractField('deviceId')
|
|
481
|
+
if (deviceId) logEntry.deviceId = deviceId
|
|
482
|
+
const doorId = extractField('doorId')
|
|
483
|
+
if (doorId) logEntry.doorId = doorId
|
|
484
|
+
// Extract error.data fields (code, status, reason, resource)
|
|
485
|
+
const errorCode = extractField('code')
|
|
486
|
+
if (errorCode) logEntry.errorCode = errorCode
|
|
487
|
+
const errorReason = extractField('reason')
|
|
488
|
+
if (errorReason) logEntry.errorReason = errorReason
|
|
489
|
+
const errorResource = extractField('resource')
|
|
490
|
+
if (errorResource) logEntry.errorResource = errorResource
|
|
491
|
+
const statusMatch = text.match(/status:\s*(\d+)/)
|
|
492
|
+
if (statusMatch) logEntry.errorStatus = Number(statusMatch[1]) || null
|
|
493
|
+
return logEntry
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Extract a concatenated string from JS object notation starting at a quote.
|
|
498
|
+
* Handles: 'part1\n' + ' part2\n' + ' part3'
|
|
499
|
+
* Returns the joined string or null.
|
|
500
|
+
*/
|
|
501
|
+
extractConcatenatedString (text, startIdx) {
|
|
502
|
+
const parts = []
|
|
503
|
+
let pos = startIdx
|
|
504
|
+
while (pos < text.length) {
|
|
505
|
+
// Expect a single-quoted string
|
|
506
|
+
if (text[pos] !== "'") break
|
|
507
|
+
const endQuote = text.indexOf("'", pos + 1)
|
|
508
|
+
if (endQuote === -1) break
|
|
509
|
+
parts.push(text.substring(pos + 1, endQuote))
|
|
510
|
+
pos = endQuote + 1
|
|
511
|
+
// Skip whitespace and look for + concatenation
|
|
512
|
+
const afterQuote = text.substring(pos).match(/^\s*\+\s*/)
|
|
513
|
+
if (afterQuote) {
|
|
514
|
+
pos += afterQuote[0].length
|
|
515
|
+
} else {
|
|
516
|
+
break
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (parts.length === 0) return null
|
|
520
|
+
// Unescape \\n → \n within the joined string
|
|
521
|
+
return parts.join('').replace(/\\n/g, '\n').replace(/\\'/g, "'").replace(/\\t/g, '\t')
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Parse structured plain-text log formats (Java logger, tagged logs, etc.)
|
|
526
|
+
* Supports:
|
|
527
|
+
* [tag] LEVEL: message
|
|
528
|
+
* [tag] Jan 01, 2026 12:00:00 AM class.method
|
|
529
|
+
* LEVEL: message
|
|
530
|
+
* Plain text fallback
|
|
531
|
+
*/
|
|
532
|
+
parsePlainTextLog (message, containerName, serviceName, timestamp, streamType) {
|
|
533
|
+
const logName = `projects/dev/logs/${serviceName}`
|
|
534
|
+
const labels = {
|
|
535
|
+
container: containerName,
|
|
536
|
+
stream: streamType === 1 ? 'stdout' : 'stderr'
|
|
537
|
+
}
|
|
538
|
+
// Try tagged format: [tag] LEVEL: message OR [tag] Java date class
|
|
539
|
+
const taggedMatch = message.match(/^\[([^\]]+)\]\s+(.*)$/)
|
|
540
|
+
if (taggedMatch) {
|
|
541
|
+
const tag = taggedMatch[1]
|
|
542
|
+
const rest = taggedMatch[2]
|
|
543
|
+
labels.tag = tag
|
|
544
|
+
// Check for LEVEL: message pattern
|
|
545
|
+
const levelMsgMatch = rest.match(/^(INFO|WARNING|WARN|ERROR|SEVERE|DEBUG|FINE|FINER|FINEST|CONFIG):\s*(.*)$/i)
|
|
546
|
+
if (levelMsgMatch) {
|
|
547
|
+
const severity = this.mapJavaSeverity(levelMsgMatch[1])
|
|
548
|
+
return {
|
|
549
|
+
logName,
|
|
550
|
+
serviceName,
|
|
551
|
+
severity,
|
|
552
|
+
source: 'docker',
|
|
553
|
+
level: levelMsgMatch[1].toLowerCase(),
|
|
554
|
+
timestamp,
|
|
555
|
+
receiveTimestamp: new Date().toISOString(),
|
|
556
|
+
textPayload: levelMsgMatch[2],
|
|
557
|
+
labels
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Check for Java date pattern: Feb 19, 2026 11:21:41 AM class.method
|
|
561
|
+
const javaDateMatch = rest.match(/^[A-Z][a-z]{2}\s+\d{1,2},\s+\d{4}\s+\d{1,2}:\d{2}:\d{2}\s+[AP]M\s+(.*)$/)
|
|
562
|
+
if (javaDateMatch) {
|
|
563
|
+
return {
|
|
564
|
+
logName,
|
|
565
|
+
serviceName,
|
|
566
|
+
severity: 'DEBUG',
|
|
567
|
+
source: 'docker',
|
|
568
|
+
level: 'debug',
|
|
569
|
+
timestamp,
|
|
570
|
+
receiveTimestamp: new Date().toISOString(),
|
|
571
|
+
textPayload: javaDateMatch[1],
|
|
572
|
+
labels
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Tagged but unrecognized format — use rest as message
|
|
576
|
+
return {
|
|
577
|
+
logName,
|
|
578
|
+
serviceName,
|
|
579
|
+
severity: this.detectSeverity(rest),
|
|
580
|
+
source: 'docker',
|
|
581
|
+
level: null,
|
|
582
|
+
timestamp,
|
|
583
|
+
receiveTimestamp: new Date().toISOString(),
|
|
584
|
+
textPayload: rest,
|
|
585
|
+
labels
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Untagged LEVEL: message (e.g., "INFO: starting server")
|
|
589
|
+
const levelMatch = message.match(/^(INFO|WARNING|WARN|ERROR|SEVERE|DEBUG):\s*(.*)$/i)
|
|
590
|
+
if (levelMatch) {
|
|
591
|
+
const severity = this.mapJavaSeverity(levelMatch[1])
|
|
592
|
+
return {
|
|
593
|
+
logName,
|
|
594
|
+
serviceName,
|
|
595
|
+
severity,
|
|
596
|
+
source: 'docker',
|
|
597
|
+
level: levelMatch[1].toLowerCase(),
|
|
598
|
+
timestamp,
|
|
599
|
+
receiveTimestamp: new Date().toISOString(),
|
|
600
|
+
textPayload: levelMatch[2],
|
|
601
|
+
labels
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Plain text fallback
|
|
605
|
+
return {
|
|
606
|
+
logName,
|
|
607
|
+
serviceName,
|
|
608
|
+
severity: this.detectSeverity(message),
|
|
609
|
+
source: 'docker',
|
|
610
|
+
level: null,
|
|
611
|
+
timestamp,
|
|
612
|
+
receiveTimestamp: new Date().toISOString(),
|
|
613
|
+
textPayload: message,
|
|
614
|
+
labels
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
mapJavaSeverity (level) {
|
|
619
|
+
const map = {
|
|
620
|
+
severe: 'ERROR',
|
|
621
|
+
error: 'ERROR',
|
|
622
|
+
warning: 'WARNING',
|
|
623
|
+
warn: 'WARNING',
|
|
624
|
+
info: 'INFO',
|
|
625
|
+
config: 'INFO',
|
|
626
|
+
fine: 'DEBUG',
|
|
627
|
+
finer: 'DEBUG',
|
|
628
|
+
finest: 'DEBUG',
|
|
629
|
+
debug: 'DEBUG'
|
|
630
|
+
}
|
|
631
|
+
return map[level.toLowerCase()] || 'INFO'
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
detectSeverity (text) {
|
|
635
|
+
if (/error|exception|fatal|severe/i.test(text)) return 'ERROR'
|
|
636
|
+
if (/warn/i.test(text)) return 'WARNING'
|
|
637
|
+
if (/debug|trace/i.test(text)) return 'DEBUG'
|
|
638
|
+
return 'INFO'
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
watchForNewContainers () {
|
|
642
|
+
this.docker.getEvents({ filters: { event: ['start'] } }, (err, stream) => {
|
|
643
|
+
if (err) {
|
|
644
|
+
Logger.log({
|
|
645
|
+
level: 'error',
|
|
646
|
+
message: 'Failed to watch Docker events',
|
|
647
|
+
data: { error: err.message }
|
|
648
|
+
})
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
stream.on('data', async (chunk) => {
|
|
652
|
+
try {
|
|
653
|
+
const event = JSON.parse(chunk.toString())
|
|
654
|
+
const containerId = event.id
|
|
655
|
+
const container = this.docker.getContainer(containerId)
|
|
656
|
+
const info = await container.inspect()
|
|
657
|
+
const containerName = info.Name.replace(/^\//, '')
|
|
658
|
+
// Skip excluded infrastructure containers
|
|
659
|
+
if (this.excludedContainers.includes(containerName)) return
|
|
660
|
+
// Skip if already attached
|
|
661
|
+
if (this.streams.has(containerId)) return
|
|
662
|
+
// Check if container is on the target network
|
|
663
|
+
const networks = info.NetworkSettings?.Networks || {}
|
|
664
|
+
if (networks[this.networkName]) {
|
|
665
|
+
await this.attachToContainer({
|
|
666
|
+
Id: containerId,
|
|
667
|
+
Names: [info.Name]
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
} catch (error) {
|
|
671
|
+
// Ignore errors from short-lived containers
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async stop () {
|
|
678
|
+
for (const [containerId, stream] of this.streams) {
|
|
679
|
+
stream.destroy()
|
|
680
|
+
}
|
|
681
|
+
this.streams.clear()
|
|
682
|
+
Logger.log({
|
|
683
|
+
level: 'info',
|
|
684
|
+
message: 'Docker log capture stopped'
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
}
|