Skip to content

Commit 8d7860b

Browse files
authored
Improved logging + DOCKERFILE changes (#190)
1 parent 4aef26e commit 8d7860b

54 files changed

Lines changed: 1047 additions & 196 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,30 @@ You can also run workspace-specific commands using:
120120
npm run <script> -w @mocker/backend
121121
npm run <script> -w @mocker/frontend
122122
```
123+
124+
## Docker Logs
125+
126+
The backend writes structured JSON logs to stdout so deployed failures can be investigated directly with `docker logs`.
127+
128+
Each log entry includes:
129+
130+
- `timestamp`
131+
- `level`
132+
- `module`
133+
- `message`
134+
- `context`
135+
- `error.name`
136+
- `error.message`
137+
- `error.stack`
138+
139+
Useful commands:
140+
141+
```bash
142+
docker logs <container-name>
143+
docker logs <container-name> | grep '"level":"error"'
144+
docker logs <container-name> | grep '"module":"AIService"'
145+
docker logs <container-name> | grep '"channelId":"C123"'
146+
docker logs <container-name> | jq .
147+
```
148+
149+
The `context` object is where request-specific identifiers live, such as `userId`, `teamId`, `channelId`, `itemId`, `symbol`, and prompt text. In production, start with `module` and `message`, then use `context` to isolate the failing request, and finally inspect `error.stack` for the root cause.

packages/backend/Dockerfile

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,18 @@ COPY packages/backend/src ./packages/backend/src
1313
# Build backend artifact inside Docker.
1414
RUN npm ci \
1515
&& npm run build:prod -w @mocker/backend \
16-
&& npm prune --omit=dev \
17-
&& mkdir -p /usr/src/app/images
16+
&& npm prune --omit=dev
1817

1918
FROM gcr.io/distroless/nodejs20-debian12:nonroot AS release
2019
ENV NODE_ENV=production \
21-
PORT=80
20+
PORT=80 \
21+
IMAGE_DIR=/tmp/mocker-images
2222

2323
WORKDIR /usr/src/app
2424

25-
# Copy backend build artifacts and writable path from build stage.
25+
# Copy backend build artifacts. Runtime-generated images are written under /tmp.
2626
COPY --from=build --chown=65532:65532 /usr/src/app/packages/backend/dist ./dist
2727
COPY --from=build --chown=65532:65532 /usr/src/app/node_modules ./node_modules
28-
COPY --from=build --chown=65532:65532 --chmod=700 /usr/src/app/images ./images
2928

3029
EXPOSE 80
3130

packages/backend/src/ai/ai.controller.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { suppressedMiddleware } from '../shared/middleware/suppression';
66
import { textMiddleware } from '../shared/middleware/textMiddleware';
77
import { aiMiddleware } from './middleware/aiMiddleware';
88
import type { SlashCommandRequest } from '../shared/models/slack/slack-models';
9+
import { logError } from '../shared/logger/error-logging';
910
import { logger } from '../shared/logger/logger';
1011

1112
export const aiController: Router = express.Router();
@@ -21,7 +22,12 @@ aiController.post('/text', (req, res) => {
2122
const { user_id, team_id, channel_id, text } = req.body;
2223
res.status(200).send('Processing your request. Please be patient...');
2324
void aiService.generateText(user_id, team_id, channel_id, text).catch((e) => {
24-
aiLogger.error(e);
25+
logError(aiLogger, 'Failed to generate AI text response', e, {
26+
userId: user_id,
27+
teamId: team_id,
28+
channelId: channel_id,
29+
prompt: text,
30+
});
2531
const errorMessage = `\`Sorry! Your request for ${text} failed. Please try again.\``;
2632
void webService.sendEphemeral(channel_id, errorMessage, user_id);
2733
return undefined;
@@ -32,7 +38,12 @@ aiController.post('/image', (req, res) => {
3238
const { user_id, team_id, channel_id, text } = req.body;
3339
res.status(200).send('Processing your request. Please be patient...');
3440
void aiService.generateImage(user_id, team_id, channel_id, text).catch((e) => {
35-
aiLogger.error(e);
41+
logError(aiLogger, 'Failed to generate AI image response', e, {
42+
userId: user_id,
43+
teamId: team_id,
44+
channelId: channel_id,
45+
prompt: text,
46+
});
3647
const errorMessage = `\`Sorry! Your request for ${text} failed. Please try again.\``;
3748
void webService.sendEphemeral(channel_id, errorMessage, user_id);
3849
return undefined;
@@ -43,7 +54,12 @@ aiController.post('/prompt-with-history', (req, res) => {
4354
const request: SlashCommandRequest = req.body;
4455
res.status(200).send('Processing your request. Please be patient...');
4556
void aiService.promptWithHistory(request).catch((e) => {
46-
aiLogger.error(e);
57+
logError(aiLogger, 'Failed to process AI prompt with history', e, {
58+
userId: request.user_id,
59+
teamId: request.team_id,
60+
channelId: request.channel_id,
61+
prompt: request.text,
62+
});
4763
const errorMessage = `\`Sorry! Your request for ${request.text} failed. Please try again.\``;
4864
void webService.sendEphemeral(request.channel_id, errorMessage, request.user_id);
4965
return undefined;

packages/backend/src/ai/ai.service.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import fs from 'fs';
2+
import os from 'os';
3+
import path from 'path';
14
import { AIService } from './ai.service';
25
import type { MessageWithName } from '../shared/models/message/message-with-name';
36
import { MOONBEAM_SLACK_ID } from './ai.constants';
@@ -155,6 +158,33 @@ describe('AIService', () => {
155158
});
156159
});
157160

161+
describe('writeToDiskAndReturnUrl', () => {
162+
it('creates the image directory before writing the file', async () => {
163+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-service-'));
164+
const imageDir = path.join(tempRoot, 'nested', 'images');
165+
const originalImageDir = process.env.IMAGE_DIR;
166+
167+
process.env.IMAGE_DIR = imageDir;
168+
169+
try {
170+
const imageUrl = await aiService.writeToDiskAndReturnUrl(Buffer.from('png-bytes').toString('base64'));
171+
const filename = imageUrl.split('/').pop();
172+
173+
expect(filename).toBeDefined();
174+
expect(fs.existsSync(imageDir)).toBe(true);
175+
expect(fs.readFileSync(path.join(imageDir, filename as string))).toEqual(Buffer.from('png-bytes'));
176+
} finally {
177+
if (originalImageDir === undefined) {
178+
delete process.env.IMAGE_DIR;
179+
} else {
180+
process.env.IMAGE_DIR = originalImageDir;
181+
}
182+
183+
fs.rmSync(tempRoot, { recursive: true, force: true });
184+
}
185+
});
186+
});
187+
158188
describe('redeployMoonbeam', () => {
159189
it('publishes deployment message with quote and image', async () => {
160190
(aiService.openAi.responses.create as jest.Mock).mockResolvedValue({

packages/backend/src/ai/ai.service.ts

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from './ai.constants';
2525
import { MemoryPersistenceService } from './memory/memory.persistence.service';
2626
import type { MemoryWithSlackId } from '../shared/db/models/Memory';
27+
import { logError } from '../shared/logger/error-logging';
2728
import { logger } from '../shared/logger/logger';
2829
import { SlackService } from '../shared/services/slack/slack.service';
2930
import { MuzzlePersistenceService } from '../muzzle/muzzle.persistence.service';
@@ -55,6 +56,8 @@ const extractAndParseOpenAiResponse = (response: OpenAI.Responses.Response): str
5556
return outputText?.trim();
5657
};
5758

59+
const DEFAULT_IMAGE_DIR = path.join('/tmp', 'mocker-images');
60+
5861
export class AIService {
5962
redis = new AIPersistenceService();
6063
openAi = new OpenAI({
@@ -110,27 +113,35 @@ export class AIService {
110113
}
111114
})
112115
.catch(async (e) => {
113-
this.aiServiceLogger.error(e);
116+
logError(this.aiServiceLogger, 'Failed to generate AI text response', e, {
117+
userId,
118+
teamId,
119+
channelId,
120+
prompt: text,
121+
});
114122
await this.redis.removeInflight(userId, teamId);
115123
await this.redis.decrementDailyRequests(userId, teamId);
116124
throw e;
117125
});
118126
}
119127

120128
public async writeToDiskAndReturnUrl(base64Image: string): Promise<string> {
121-
const dir = process.env.IMAGE_DIR ? process.env.IMAGE_DIR : path.join(__dirname, '../../../images');
129+
const dir = process.env.IMAGE_DIR ?? DEFAULT_IMAGE_DIR;
122130
const filename = `${uuidv4()}.png`;
123131
const filePath = path.join(dir, filename);
124132
const base64Data = base64Image.replace(/^data:image\/png;base64,/, '');
125-
return new Promise((resolve, reject) =>
126-
fs.writeFile(filePath, base64Data, 'base64', (err) => {
127-
if (err) {
128-
this.aiServiceLogger.error('Error writing image to disk:', err);
129-
reject(err);
130-
}
131-
resolve(`https://muzzle.lol/${filename}`);
132-
}),
133-
);
133+
134+
try {
135+
await fs.promises.mkdir(dir, { recursive: true });
136+
await fs.promises.writeFile(filePath, base64Data, 'base64');
137+
return `https://muzzle.lol/${filename}`;
138+
} catch (error) {
139+
logError(this.aiServiceLogger, 'Failed to write AI image to disk', error, {
140+
imageDirectory: dir,
141+
filePath,
142+
});
143+
throw error;
144+
}
134145
}
135146

136147
public async redeployMoonbeam(): Promise<void> {
@@ -191,8 +202,11 @@ export class AIService {
191202
if (x) {
192203
return this.writeToDiskAndReturnUrl(x);
193204
} else {
194-
this.aiServiceLogger.error(`No b64_json was returned for prompt: ${REDPLOY_MOONBEAM_IMAGE_PROMPT}`);
195-
throw new Error(`No b64_json was returned for prompt: ${REDPLOY_MOONBEAM_IMAGE_PROMPT}`);
205+
const error = new Error(`No b64_json was returned for prompt: ${REDPLOY_MOONBEAM_IMAGE_PROMPT}`);
206+
logError(this.aiServiceLogger, 'Gemini redeploy image generation returned no image data', error, {
207+
prompt: REDPLOY_MOONBEAM_IMAGE_PROMPT,
208+
});
209+
throw error;
196210
}
197211
});
198212

@@ -217,7 +231,7 @@ export class AIService {
217231
void this.webService.sendMessage('#muzzlefeedback', 'Moonbeam has been deployed.', blocks);
218232
})
219233
.catch((e) => {
220-
this.aiServiceLogger.error(e);
234+
logError(this.aiServiceLogger, 'Failed to redeploy Moonbeam assets', e);
221235
});
222236
}
223237

@@ -274,15 +288,26 @@ export class AIService {
274288
if (x) {
275289
return this.writeToDiskAndReturnUrl(x);
276290
} else {
277-
this.aiServiceLogger.error(`No b64_json was returned for prompt: ${text}`);
278-
throw new Error(`No b64_json was returned for prompt: ${text}`);
291+
const error = new Error(`No b64_json was returned for prompt: ${text}`);
292+
logError(this.aiServiceLogger, 'Gemini image generation returned no image data', error, {
293+
userId,
294+
teamId,
295+
channelId: channel,
296+
prompt: text,
297+
});
298+
throw error;
279299
}
280300
})
281301
.then((imageUrl) => {
282302
this.sendImage(imageUrl, userId, teamId, channel, text);
283303
})
284304
.catch(async (e) => {
285-
this.aiServiceLogger.error(e);
305+
logError(this.aiServiceLogger, 'Failed to generate AI image response', e, {
306+
userId,
307+
teamId,
308+
channelId: channel,
309+
prompt: text,
310+
});
286311
await this.redis.removeInflight(userId, teamId);
287312
await this.redis.decrementDailyRequests(userId, teamId);
288313
throw e;
@@ -296,7 +321,9 @@ export class AIService {
296321
return extractAndParseOpenAiResponse(x);
297322
})
298323
.catch(async (e) => {
299-
this.aiServiceLogger.error(e);
324+
logError(this.aiServiceLogger, 'Failed to generate corpo-speak response', e, {
325+
prompt: text,
326+
});
300327
throw e;
301328
});
302329
}
@@ -370,15 +397,25 @@ export class AIService {
370397
});
371398

372399
this.webService.sendMessage(request.channel_id, request.text, blocks).catch((e) => {
373-
this.aiServiceLogger.error(e);
400+
logError(this.aiServiceLogger, 'Failed to send prompt-with-history response to Slack', e, {
401+
userId: request.user_id,
402+
teamId: request.team_id,
403+
channelId: request.channel_id,
404+
prompt: request.text,
405+
});
374406
void this.webService.sendMessage(
375407
request.user_id,
376408
'Sorry, unable to send the requested text to Slack. You have been credited for your Moon Token. Perhaps you were trying to send in a private channel? If so, invite @MoonBeam and try again.',
377409
);
378410
});
379411
})
380412
.catch(async (e) => {
381-
this.aiServiceLogger.error(e);
413+
logError(this.aiServiceLogger, 'Failed to process prompt with history', e, {
414+
userId: request.user_id,
415+
teamId: request.team_id,
416+
channelId: request.channel_id,
417+
prompt: request.text,
418+
});
382419
await this.redis.removeInflight(user_id, team_id);
383420
await this.redis.decrementDailyRequests(user_id, team_id);
384421
throw e;
@@ -420,7 +457,12 @@ export class AIService {
420457
this.webService
421458
.sendMessage(channelId, result, [{ type: 'markdown', text: result }])
422459
.then(() => this.redis.setHasParticipated(teamId, channelId))
423-
.catch((e) => this.aiServiceLogger.error('Error sending AI Participation message:', e));
460+
.catch((e) =>
461+
logError(this.aiServiceLogger, 'Failed to send AI participation message', e, {
462+
teamId,
463+
channelId,
464+
}),
465+
);
424466

425467
// Fire-and-forget: extract memories from this conversation
426468
this.extractMemories(teamId, channelId, history, result, participantSlackIds).catch((e) =>
@@ -429,7 +471,11 @@ export class AIService {
429471
}
430472
})
431473
.catch(async (e) => {
432-
this.aiServiceLogger.error(e);
474+
logError(this.aiServiceLogger, 'Failed to generate AI participation response', e, {
475+
teamId,
476+
channelId,
477+
taggedMessage,
478+
});
433479
throw e;
434480
})
435481
.finally(() => {
@@ -655,7 +701,12 @@ export class AIService {
655701
},
656702
];
657703
this.webService.sendMessage(channel, text, blocks).catch((e) => {
658-
this.aiServiceLogger.error(e);
704+
logError(this.aiServiceLogger, 'Failed to send generated AI image to Slack', e, {
705+
userId,
706+
teamId,
707+
channelId: channel,
708+
prompt: text,
709+
});
659710
void this.decrementDaiyRequests(userId, teamId);
660711
void this.webService.sendMessage(
661712
userId,
@@ -686,7 +737,12 @@ export class AIService {
686737
});
687738

688739
this.webService.sendMessage(channelId, text, blocks).catch((e) => {
689-
this.aiServiceLogger.error(e);
740+
logError(this.aiServiceLogger, 'Failed to send generated AI text to Slack', e, {
741+
userId,
742+
teamId,
743+
channelId,
744+
prompt: query,
745+
});
690746
void this.decrementDaiyRequests(userId, teamId);
691747
void this.webService.sendMessage(
692748
userId,

0 commit comments

Comments
 (0)