api: route all backend logging through a shared pino logger
Only the HTTP access line used pino; queue workers, job processors, mail services and the cron syncs logged through raw console calls, and the error handler stayed silent outside development. All of them now share one logger, the error handler reports structured errors with request context, and each request carries an X-Request-Id that is echoed back on the response pino-http is registered before the body parser so errors raised there still have a request-scoped logger, and the logger only pretty-prints on a TTY so containers emit single-line JSON
Pedro Lucas Porcellis porcellis@eletrotupi.com · 14 days ago
ee68bbccb28c68fc7c4fb62c41355e4bd2e50eaf Browse filesParents
a135a13Changed files
23
Additions
+233
Deletions
-73
api/package.json
1 1
{
2 2
"name": "orbit-api",
3 3
"version": "0.0.0",
4 4
"description": "",
5 5
"license": "ISC",
37 37
"csv-stringify": "^6.9.0",
38 38
"date-fns": "^4.1.0",
39 39
"express": "^5.2.1",
40 40
"jsonwebtoken": "^9.0.3",
41 41
"jszip": "^3.10.2",
42 -
"morgan": "^1.10.1",
43 42
"multer": "^2.1.1",
44 43
"multer-s3": "^3.0.1",
45 44
"pg": "^8.20.0",
46 45
"pino": "^10.3.1",
47 46
"pino-http": "^11.0.0",
51 50
"zod-validation-error": "^5.0.0"
52 51
},
53 52
"devDependencies": {
54 53
"@types/better-sqlite3": "^7.6.13",
55 54
"@types/express": "^5.0.6",
56 -
"@types/morgan": "^1.9.10",
57 55
"@types/multer": "^2.1.0",
58 56
"@types/multer-s3": "^3.0.3",
59 57
"@types/node": "^25.5.2",
60 58
"@types/react": "^19.2.17",
61 59
"@types/supertest": "^6.0.3",
70 68
"typescript": "^6.0.2",
71 69
"vite-tsconfig-paths": "^5.1.4",
72 70
"vitest": "^3.0.0"
73 71
}
74 72
}
api/src/createApp.ts
1 1
import express from "express";
2 2
import cors from "cors";
3 3
import bodyParser from "body-parser";
4 -
import morgan from "morgan";
5 4
import mainRouter from "@app/routes/main";
6 5
import userRouter from "@app/routes/users";
7 6
import authRouter from "@app/routes/auth";
8 7
import moodRouter from "@app/routes/moods";
9 8
import triggerRouter from "@app/routes/triggers";
12 11
import careActionsRouter from "@app/routes/careActions";
13 12
import medicineRegimensRouter from "@app/routes/medicineRegimens";
14 13
import exportsRouter from "@app/routes/exports";
15 14
import reportRouter from "@app/reports/report.router";
16 15
import { errorHandler } from "@app/middleware/errorHandler";
16 +
import { logger } from "@app/lib/logger";
17 17
import { PrismaClient } from "@prisma/client";
18 18
import {
19 19
bootWorkers,
20 20
closeAllWorkers,
21 21
closeAllQueues,
22 22
scheduleInsights,
23 23
} from "@app/lib/queue";
24 24
25 -
import pino from "pino";
26 25
import pinoHttp from "pino-http";
26 +
import { randomUUID } from "crypto";
27 27
import path from "path";
28 28
29 29
export function createApp() {
30 30
const app = express();
31 31
bootWorkers();
32 32
scheduleInsights();
33 33
34 +
// Use pino-http for HTTP logging (better than Morgan for structured logs)
35 +
// Registered before the rest so every request has a logger attached,
36 +
// including errors raised by the CORS and body-parsing middleware below
37 +
app.use(
38 +
pinoHttp({
39 +
logger,
40 +
genReqId: (req, res) => {
41 +
const header = req.headers["x-request-id"];
42 +
const requestId =
43 +
(Array.isArray(header) ? header[0] : header) || randomUUID();
44 +
45 +
res.setHeader("X-Request-Id", requestId);
46 +
47 +
return requestId;
48 +
},
49 +
}),
50 +
);
51 +
34 52
// Configure CORS
35 53
app.use(
36 54
cors({
37 55
origin: process.env.FRONTEND_URL || "*",
38 56
credentials: true,
39 57
}),
40 58
);
41 59
42 60
app.use(bodyParser.json());
43 -
44 -
const isDev = process.env.NODE_ENV !== "production";
45 -
46 -
const logger = pino({
47 -
level: process.env.LOG_LEVEL || "info",
48 -
...(isDev && {
49 -
transport: {
50 -
target: "pino-pretty",
51 -
options: {
52 -
colorize: true,
53 -
singleLine: false,
54 -
messageFormat: "{if levelLabel}{levelLabel} - {end}{msg}",
55 -
},
56 -
},
57 -
}),
58 -
});
59 -
60 -
// Use pino-http for HTTP logging (better than Morgan for structured logs)
61 -
app.use(pinoHttp({ logger }));
62 61
63 62
app.use("/users", userRouter);
64 63
app.use("/auth", authRouter);
65 64
app.use("/moods", moodRouter);
66 65
app.use("/sleep_records", sleepRecordRouter);
87 86
88 87
const shutdown = async () => {
89 88
await Promise.all([closeAllWorkers(), closeAllQueues()]);
90 89
process.exit(0);
91 90
};
api/src/index.ts
1 1
import { createApp } from '@app/createApp';
2 +
import { logger } from '@app/lib/logger';
2 3
3 4
const port = process.env.PORT || 3000;
4 5
5 6
const app = createApp();
6 7
7 8
app.listen(port, () => {
8 -
console.log(`Server booted on port ${port}`)
9 +
logger.info(`Server booted on port ${port}`)
9 10
})
10 -
/dev/null → api/src/lib/logger.ts
1 +
import pino from "pino";
2 +
3 +
// Pretty only on a TTY: in a container stdout is a pipe, so emit plain
4 +
// single-line JSON that the log collector can parse
5 +
const pretty = process.env.NODE_ENV !== "production" && process.stdout.isTTY;
6 +
7 +
export const logger = pino({
8 +
level: process.env.LOG_LEVEL || "info",
9 +
...(pretty && {
10 +
transport: {
11 +
target: "pino-pretty",
12 +
options: {
13 +
colorize: true,
14 +
singleLine: false,
15 +
messageFormat: "{if levelLabel}{levelLabel} - {end}{msg}",
16 +
},
17 +
},
18 +
}),
19 +
});
api/src/lib/queue/WorkerRegistry.ts
1 1
import { Worker, WorkerOptions, Processor } from "bullmq";
2 2
import { getQueue, QueueName } from "@app/lib/queue/QueueRegistry";
3 3
import { mailProcessor } from "@app/lib/queue/processors/mail";
4 4
import { insightProcessor } from "@app/lib/queue/processors/insights";
5 5
import { medicineReminderProcessor } from "@app/lib/queue/processors/medicineReminders";
6 6
import { dailyReminderProcessor } from "@app/lib/queue/processors/dailyReminders";
7 7
import { exportProcessor } from "@app/lib/queue/processors/exports";
8 8
import { InsightJobName } from "@app/lib/queue/types";
9 +
import { logger } from "@app/lib/logger";
9 10
10 11
type ProcessorMap = Record<QueueName, Processor>;
11 12
12 13
const processorMap: ProcessorMap = {
13 14
mail: mailProcessor,
28 29
};
29 30
30 31
const workers: Worker[] = [];
31 32
32 33
export function bootWorkers(): void {
33 -
console.log("Booting workers...");
34 +
logger.info("Booting workers...");
34 35
for (const [name, processor] of Object.entries(processorMap)) {
35 36
const queueName = name as QueueName;
36 37
37 -
console.log(`Worker ${queueName} booting...`);
38 +
logger.info({ queue: queueName }, "Worker booting...");
38 39
const worker = new Worker(queueName, processor, {
39 40
connection: {
40 41
host: process.env.VALKEY_HOST,
41 42
port: Number(process.env.VALKEY_PORT),
42 43
},
43 44
concurrency: concurrencyMap[queueName],
44 45
} satisfies WorkerOptions);
45 46
46 47
worker.on("failed", (job, err) => {
47 -
console.error(`[${queueName}] job ${job?.id} failed:`, err.message);
48 +
logger.error({ err, queue: queueName, jobId: job?.id }, "job failed");
48 49
});
49 50
50 51
worker.on("error", (err) => {
51 -
console.error(`[${queueName}] worker error:`, err.message);
52 +
logger.error({ err, queue: queueName }, "worker error");
52 53
});
53 54
54 55
workers.push(worker);
55 56
}
56 57
}
68 69
}
69 70
70 71
export async function closeAllWorkers(): Promise<void> {
71 72
await Promise.all(workers.map((w) => w.close()));
72 73
}
api/src/lib/queue/processors/dailyReminders.ts
1 1
import { Job } from "bullmq";
2 2
import { prisma } from "@app/lib/prisma";
3 3
import { sendPushNotification } from "@app/lib/sendPushNotification";
4 4
import { DailyReminderPayload } from "@app/lib/queue/types";
5 +
import { logger } from "@app/lib/logger";
5 6
6 7
export async function dailyReminderProcessor(job: Job): Promise<void> {
7 8
const { userId } = job.data as DailyReminderPayload;
8 9
9 -
console.log(`[daily-reminder] job ${job.id}: user=${userId}`);
10 +
logger.info({ jobId: job.id, userId }, "[daily-reminder] job started");
10 11
11 12
const user = await prisma.user.findUnique({
12 13
where: { id: userId },
13 14
select: { pushToken: true, notificationsEnabled: true },
14 15
});
15 16
16 17
if (!user) {
17 -
console.warn(`[daily-reminder] user ${userId} not found, skipping`);
18 +
logger.warn({ userId }, "[daily-reminder] user not found, skipping");
18 19
19 20
return;
20 21
}
21 22
22 23
if (!user.notificationsEnabled) {
23 -
console.warn(`[daily-reminder] user ${userId} has notifications disabled, skipping`);
24 +
logger.warn({ userId }, "[daily-reminder] notifications disabled, skipping");
24 25
25 26
return;
26 27
}
27 28
28 29
if (!user.pushToken) {
29 -
console.warn(`[daily-reminder] user ${userId} has no pushToken, skipping`);
30 +
logger.warn({ userId }, "[daily-reminder] no pushToken, skipping");
30 31
31 32
return;
32 33
}
33 34
34 35
// TODO: These should be a side-effect from a new entry on the notifications
38 39
title: "Como você está?",
39 40
body: "Registre seu humor para acompanhar sua saúde.",
40 41
data: { screen: "moods" },
41 42
});
42 43
43 -
console.log(`[daily-reminder] sent OK to user ${userId}`);
44 +
logger.info({ userId }, "[daily-reminder] push sent");
44 45
}
api/src/lib/queue/processors/exports.ts
1 1
import { Job } from "bullmq";
2 2
3 3
import {
4 4
ExportJobData,
5 5
ExportJobName,
8 8
import { buildExportArchive } from "@app/services/export/export.service";
9 9
import {
10 10
sendExportFailedEmail,
11 11
sendExportReadyEmail,
12 12
} from "@app/services/mail";
13 +
import { logger } from "@app/lib/logger";
13 14
14 15
export async function exportProcessor(
15 16
job: Job<ExportJobData["data"]>,
16 17
): Promise<void> {
17 18
switch (job.name as ExportJobName) {
28 29
29 30
if (isFinalAttempt) {
30 31
try {
31 32
await sendExportFailedEmail(userId);
32 33
} catch (mailErr) {
33 -
console.error(
34 -
`[exports] failed to send the failure email for user ${userId}:`,
35 -
mailErr,
34 +
logger.error(
35 +
{ err: mailErr, userId },
36 +
"[exports] failed to send the failure email",
36 37
);
37 38
}
38 39
}
39 40
40 41
throw err;
45 46
46 47
default:
47 48
throw new Error(`Unknown export job: ${job.name}`);
48 49
}
49 50
}
api/src/lib/queue/processors/insights/dailySleep.ts
1 1
import { prisma } from "@app/lib/prisma";
2 2
import { InsightType, InsightPeriod } from "@prisma/client";
3 3
import { InsightPeriodPayload } from "@app/lib/queue/types";
4 +
import { logger } from "@app/lib/logger";
4 5
// XXX: date-fns?
5 6
import { formatHours, formatDiff } from "@app/utils/calc";
6 7
import { startOfDay, addDays, subDays } from "date-fns";
7 8
8 9
export async function processDailySleep({
22 23
where: { userId, date: { gte: yesterday, lt: today } },
23 24
select: { average: true },
24 25
}),
25 26
]);
26 27
27 -
console.log("todayRecords, yesterdayRecords", todayRecords, yesterdayRecords);
28 +
logger.debug(
29 +
{ todayRecords, yesterdayRecords },
30 +
"[insights] daily sleep records",
31 +
);
28 32
29 33
if (todayRecords.length === 0) return;
30 34
31 35
// average is stored in hours
32 36
// sum across all records for the day
102 106
function buildBody(total: number): string {
103 107
const totalStr = formatHours(total);
104 108
105 109
return `${totalStr} de sono hoje`;
106 110
}
api/src/lib/queue/processors/insights/index.ts
1 1
import { Job } from "bullmq";
2 2
import {
3 3
InsightJobName,
4 4
InsightJobData,
5 5
InsightPeriodPayload,
6 6
} from "@app/lib/queue/types";
7 7
import { getQueue } from "@app/lib/queue/QueueRegistry";
8 8
import { prisma } from "@app/lib/prisma";
9 +
import { logger } from "@app/lib/logger";
9 10
import { processMoodTrend } from "@app/lib/queue/processors/insights/moodTrend";
10 11
import { processEnergySleep } from "@app/lib/queue/processors/insights/energySleep";
11 12
import { processTriggerPattern } from "@app/lib/queue/processors/insights/triggerPattern";
12 13
import { processDailyEnergy } from "@app/lib/queue/processors/insights/dailyEnergy";
13 14
import { processDailySleep } from "@app/lib/queue/processors/insights/dailySleep";
14 15
15 16
export async function insightProcessor(
16 17
job: Job<InsightJobData["data"]>,
17 18
): Promise<void> {
18 -
console.log(`Insight job ${job.name} started`);
19 +
logger.info({ job: job.name }, "[insights] job started");
19 20
20 21
switch (job.name as InsightJobName) {
21 22
case InsightJobName.FanOut:
22 23
return fanOut();
23 24
66 67
data: { userId: u.id, ...payload },
67 68
},
68 69
]),
69 70
);
70 71
}
api/src/lib/queue/processors/mail.ts
1 1
import { Job } from "bullmq";
2 2
import {
3 3
MailJobName,
4 4
MailJobData,
5 5
WelcomeEmailPayload,
6 6
PasswordResetPayload,
7 7
ActivateAccountEmailPayload,
8 8
} from "@app/lib/queue/types";
9 +
import { logger } from "@app/lib/logger";
9 10
10 11
import {
11 12
sendWelcomeEmail,
12 13
sendResetPasswordEmail,
13 14
sendActivateAccountEmail,
18 19
): Promise<void> {
19 20
// job.name is the discriminant
20 21
switch (job.name as MailJobName) {
21 22
case MailJobName.WelcomeEmail: {
22 23
const data = job.data as WelcomeEmailPayload;
23 -
console.log("Dispatching a welcome email for user", data.userId);
24 +
logger.info({ userId: data.userId }, "[mail] dispatching welcome email");
24 25
25 26
await sendWelcomeEmail(Number(data.userId), data.code);
26 27
27 28
break;
28 29
}
29 30
30 31
case MailJobName.ActivateAccountEmail: {
31 32
const data = job.data as ActivateAccountEmailPayload;
32 -
console.log("Dispatching an activation email for user", data.userId);
33 +
logger.info(
34 +
{ userId: data.userId },
35 +
"[mail] dispatching activation email",
36 +
);
33 37
34 38
await sendActivateAccountEmail(Number(data.userId), data.code);
35 39
36 40
break;
37 41
}
38 42
39 43
case MailJobName.PasswordReset: {
40 44
const data = job.data as PasswordResetPayload;
41 -
console.log("Dispatching a password reset email for user", data.userId);
45 +
logger.info(
46 +
{ userId: data.userId },
47 +
"[mail] dispatching password reset email",
48 +
);
42 49
43 50
await sendResetPasswordEmail(data.userId, data.token);
44 51
45 52
break;
46 53
}
47 54
48 55
default:
49 56
throw new Error(`Unknown mail job: ${job.name}`);
50 57
}
51 58
}
api/src/lib/queue/processors/medicineReminders.ts
1 1
import { Job } from "bullmq";
2 2
import { prisma } from "@app/lib/prisma";
3 3
import { sendPushNotification } from "@app/lib/sendPushNotification";
4 4
import { MedicineReminderPayload } from "@app/lib/queue/types";
5 +
import { logger } from "@app/lib/logger";
5 6
6 7
export async function medicineReminderProcessor(job: Job): Promise<void> {
7 8
const { userId, regimenId, medicineName, dosage } =
8 9
job.data as MedicineReminderPayload;
9 10
10 -
console.log(`[medicine-reminder] job ${job.id}: user=${userId} regimen=${regimenId} medicine=${medicineName}`);
11 +
logger.info(
12 +
{ jobId: job.id, userId, regimenId, medicine: medicineName },
13 +
"[medicine-reminder] job started",
14 +
);
11 15
12 16
const user = await prisma.user.findUnique({
13 17
where: { id: userId },
14 18
select: { pushToken: true },
15 19
});
16 20
17 21
if (!user) {
18 -
console.warn(`[medicine-reminder] user ${userId} not found, skipping`);
22 +
logger.warn({ userId }, "[medicine-reminder] user not found, skipping");
19 23
return;
20 24
}
21 25
if (!user.pushToken) {
22 -
console.warn(`[medicine-reminder] user ${userId} has no pushToken, skipping`);
26 +
logger.warn({ userId }, "[medicine-reminder] no pushToken, skipping");
23 27
return;
24 28
}
25 29
26 30
await sendPushNotification({
27 31
token: user.pushToken,
28 32
title: `Hora de tomar ${medicineName}`,
29 33
body: dosage,
30 34
data: { screen: "medicine-regimens", regimenId },
31 35
});
32 36
33 -
console.log(`[medicine-reminder] sent OK to user ${userId}`);
37 +
logger.info({ userId }, "[medicine-reminder] push sent");
34 38
}
api/src/lib/sendPushNotification.ts
1 +
import { logger } from "@app/lib/logger";
2 +
1 3
const EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
2 4
3 5
type PushPayload = {
4 6
token: string;
5 7
title: string;
11 13
token,
12 14
title,
13 15
body,
14 16
data,
15 17
}: PushPayload): Promise<void> {
16 -
console.log(`[push] => token ${token.slice(0, 24)}... title="${title}"`);
18 +
logger.info(
19 +
{ tokenPrefix: token.slice(0, 24), title },
20 +
"[push] sending notification",
21 +
);
17 22
18 23
const response = await fetch(EXPO_PUSH_URL, {
19 24
method: "POST",
20 25
headers: { "Content-Type": "application/json" },
21 26
body: JSON.stringify({ to: token, title, body, data }),
22 27
});
23 28
24 29
if (!response.ok) {
25 30
const errBody = await response.text();
26 31
27 -
console.error(`[push] Expo API ${response.status}: ${errBody}`);
32 +
logger.error(
33 +
{ status: response.status, body: errBody },
34 +
"[push] Expo API error",
35 +
);
28 36
throw new Error(`Expo push API returned ${response.status}`);
29 37
}
30 38
}
api/src/middleware/errorHandler.ts
1 1
import { Request, Response, NextFunction } from 'express';
2 2
import { Prisma } from '@prisma/client' // Bleh
3 3
import { isDomainError } from "@app/lib/errors/base";
4 4
5 5
export const errorHandler = (
6 6
err: unknown,
7 -
_req: Request,
7 +
req: Request,
8 8
res: Response,
9 9
_next: NextFunction
10 10
) => {
11 -
if (process.env.NODE_ENV === "development") {
12 -
// TODO: Rig some improved, centralized error logging here
13 -
console.error(`[ErrorHandler]: ${err}`);
14 -
}
11 +
req.log.error({ err }, "request failed");
15 12
16 13
if (isDomainError(err)) {
17 14
return res.status(err.status)
18 15
.json({ error: err.message });
19 16
}
27 24
.json({ error: "Unique constraint violation" });
28 25
}
29 26
30 27
res.status(500).json({ error: "Internal server error" });
31 28
}
api/src/reports/report.data.ts
1 1
import { eachDayOfInterval, format } from 'date-fns'
2 2
import { prisma } from '@app/lib/prisma'
3 +
import { logger } from '@app/lib/logger'
3 4
import {
4 5
BaseMoodOption,
5 6
MoodComponentOption,
6 7
IntensityLevel,
7 8
} from '@prisma/client'
57 58
}),
58 59
])
59 60
60 61
const total = moods.length
61 62
62 -
console.log(`[report] found ${moods.length} mood entries from ${start} - ${end}`)
63 +
logger.info(
64 +
{ count: moods.length, start, end },
65 +
'[report] found mood entries',
66 +
)
63 67
64 68
const moodCounts = Object.fromEntries(
65 69
MOOD_ORDER.map(m => [m, 0]),
66 70
) as Record<BaseMoodOption, number>
67 71
134 138
dailyMoods,
135 139
topComponents,
136 140
checkinLog,
137 141
}
138 142
}
api/src/services/dailyReminder.sync.ts
1 1
import { User } from "@prisma/client";
2 2
import { getQueue } from "@app/lib/queue/QueueRegistry";
3 3
import { DailyReminderJobName } from "@app/lib/queue/types";
4 +
import { logger } from "@app/lib/logger";
4 5
5 6
// XXX: Might be moved to the utilities?
6 7
function timeToCron(time: string): string {
7 8
const [hour, minute] = time.split(":").map(Number);
8 9
return `${minute} ${hour} * * *`;
9 10
}
10 11
11 12
export async function syncDailyReminderJob(user: User): Promise<void> {
12 -
console.log(`[daily-sync] syncing user ${user.id}`);
13 +
logger.info({ userId: user.id }, "[daily-sync] syncing user");
13 14
14 15
const queue = getQueue("daily-reminders");
15 16
16 17
const existing = await queue.getRepeatableJobs();
17 18
for (const job of existing) {
19 20
await queue.removeRepeatableByKey(job.key);
20 21
}
21 22
}
22 23
23 24
if (!user.notificationsEnabled || !user.dailyReminderTime) {
24 -
console.log(`[daily-sync] user ${user.id} skipped (enabled=${user.notificationsEnabled}, time=${user.dailyReminderTime})`);
25 +
logger.info(
26 +
{
27 +
userId: user.id,
28 +
enabled: user.notificationsEnabled,
29 +
time: user.dailyReminderTime,
30 +
},
31 +
"[daily-sync] user skipped",
32 +
);
25 33
26 34
return;
27 35
}
28 36
29 37
const cron = timeToCron(user.dailyReminderTime);
35 43
repeat: { pattern: cron, tz: "America/Sao_Paulo" },
36 44
jobId: `daily-reminder-user-${user.id}`,
37 45
}
38 46
);
39 47
40 -
console.log(`[daily-sync] scheduled daily reminder for user ${user.id} at ${user.dailyReminderTime} => cron ${cron}`);
48 +
logger.info(
49 +
{ userId: user.id, time: user.dailyReminderTime, cron },
50 +
"[daily-sync] scheduled daily reminder",
51 +
);
41 52
}
api/src/services/mail/sendActivateAccountEmail.ts
1 1
import { findUserById } from '@app/services/user.service';
2 2
import { sendEmail } from '@app/lib/mail';
3 +
import { logger } from '@app/lib/logger';
3 4
4 5
const email = (firstName: string, code: string) => {
5 6
return (`
6 7
Oi, ${firstName}.
7 8
22 23
subject: "Ative sua conta no nexo!",
23 24
text: email(user!.firstName, code)
24 25
});
25 26
26 27
if (error) {
27 -
console.error("Error sending email: ", error);
28 +
logger.error({ err: error }, "Error sending email");
28 29
}
29 30
30 -
console.log("Email sent successfully!");
31 -
console.log("Email ID:", data?.id);
31 +
logger.info({ emailId: data?.id }, "Email sent successfully");
32 32
}
api/src/services/mail/sendExportFailed.ts
1 1
import { sendEmail } from '@app/lib/mail';
2 2
import { findUserById } from '@app/services/user.service';
3 +
import { logger } from '@app/lib/logger';
3 4
4 5
const email = (firstName: string) => (`
5 6
Oi, ${firstName}.
6 7
7 8
Infelizmente não conseguimos preparar o arquivo com os seus dados do nexo
19 20
subject: 'Não conseguimos exportar seus dados',
20 21
text: email(user!.firstName),
21 22
});
22 23
23 24
if (error) {
24 -
console.error("Error sending export failure email: ", error);
25 +
logger.error({ err: error }, "Error sending export failure email");
25 26
throw new Error("Não foi possível enviar o e-mail de falha da exportação");
26 27
}
27 28
}
api/src/services/mail/sendExportReady.ts
1 1
import { sendEmail } from '@app/lib/mail';
2 2
import { findUserById } from '@app/services/user.service';
3 +
import { logger } from '@app/lib/logger';
3 4
4 5
const email = (firstName: string) => (`
5 6
Oi, ${firstName}!
6 7
7 8
Preparamos um arquivo com os seus dados do nexo e anexamos a este e-mail. Ele
24 25
text: email(user!.firstName),
25 26
attachments: [{ filename: attachment.filename, content: attachment.buffer }],
26 27
});
27 28
28 29
if (error) {
29 -
console.error("Error sending export email: ", error);
30 +
logger.error({ err: error }, "Error sending export email");
30 31
throw new Error("Não foi possível enviar o e-mail com seus dados");
31 32
}
32 33
}
api/src/services/mail/sendPasswordReset.ts
1 1
import { findUserById } from '@app/services/user.service';
2 2
import { sendEmail } from '@app/lib/mail';
3 +
import { logger } from '@app/lib/logger';
3 4
4 5
const email = (firstName: string, token: string) => {
5 6
return (`
6 7
Oi, ${firstName}.
7 8
21 22
subject: "Esqueceu a senha do nexo?",
22 23
text: email(user!.firstName, token)
23 24
});
24 25
25 26
if (error) {
26 -
console.error("Error sending email: ", error);
27 +
logger.error({ err: error }, "Error sending email");
27 28
}
28 29
29 -
console.log("Email sent successfully!");
30 -
console.log("Email ID:", data?.id);
30 +
logger.info({ emailId: data?.id }, "Email sent successfully");
31 31
}
api/src/services/mail/welcome.ts
1 1
import { findUserById } from '@app/services/user.service';
2 2
import { sendEmail } from '@app/lib/mail';
3 +
import { logger } from '@app/lib/logger';
3 4
4 5
const email = (firstName: string, code: string) => {
5 6
return (`
6 7
Oi! Seja bem vindo, ${firstName}.
7 8
20 21
subject: "Bem vindo ao nexo!",
21 22
text: email(user!.firstName, code)
22 23
});
23 24
24 25
if (error) {
25 -
console.error("Error sending email: ", error);
26 +
logger.error({ err: error }, "Error sending email");
26 27
}
27 28
28 -
console.log("Email sent successfully!");
29 -
console.log("Email ID:", data?.id);
29 +
logger.info({ emailId: data?.id }, "Email sent successfully");
30 30
}
api/src/services/medicineReminder.sync.ts
1 1
import { MedicineRegimen } from "@prisma/client";
2 2
import { getQueue } from "@app/lib/queue/QueueRegistry";
3 3
import { MedicineReminderJobName } from "@app/lib/queue/types";
4 +
import { logger } from "@app/lib/logger";
4 5
5 6
function timeToCron(time: string): string {
6 7
const [hour, minute] = time.split(":").map(Number);
7 8
return `${minute} ${hour} * * *`;
8 9
}
9 10
10 11
export async function syncMedicineReminderJobs(
11 12
regimen: MedicineRegimen
12 13
): Promise<void> {
13 -
console.log(`[medicine-sync] syncing regimen ${regimen.id} (active=${regimen.active}, times=[${regimen.scheduledAt}])`);
14 +
logger.info(
15 +
{
16 +
regimenId: regimen.id,
17 +
active: regimen.active,
18 +
scheduledAt: regimen.scheduledAt,
19 +
},
20 +
"[medicine-sync] syncing regimen",
21 +
);
14 22
15 23
const queue = getQueue("medicine-reminders");
16 24
17 25
const existing = await queue.getRepeatableJobs();
18 26
let removedCount = 0;
22 30
await queue.removeRepeatableByKey(job.key);
23 31
removedCount++;
24 32
}
25 33
}
26 34
27 -
console.log(`[medicine-sync] removed ${removedCount} existing jobs for regimen ${regimen.id}`);
35 +
logger.info(
36 +
{ regimenId: regimen.id, removedCount },
37 +
"[medicine-sync] removed existing jobs",
38 +
);
28 39
29 40
if (!regimen.active) {
30 -
console.log(`[medicine-sync] regimen ${regimen.id} inactive, no jobs scheduled`);
41 +
logger.info(
42 +
{ regimenId: regimen.id },
43 +
"[medicine-sync] regimen inactive, no jobs scheduled",
44 +
);
31 45
32 46
return;
33 47
}
34 48
35 49
if (regimen.scheduledAt.length === 0) {
36 -
console.log(`[medicine-sync] regimen ${regimen.id} has no scheduledAt times, skipping`);
50 +
logger.info(
51 +
{ regimenId: regimen.id },
52 +
"[medicine-sync] regimen has no scheduledAt times, skipping",
53 +
);
37 54
38 55
return;
39 56
}
40 57
41 58
for (const time of regimen.scheduledAt) {
53 70
repeat: { pattern: cron, tz: "America/Sao_Paulo" },
54 71
jobId: `regimen-${regimen.id}-${time}`,
55 72
}
56 73
);
57 74
58 -
console.log(`[medicine-sync] scheduled job for regimen ${regimen.id} at ${time} => cron ${cron}`);
75 +
logger.info(
76 +
{ regimenId: regimen.id, time, cron },
77 +
"[medicine-sync] scheduled job",
78 +
);
59 79
}
60 80
}
/dev/null → api/tests/middleware/error-handler.test.ts
1 +
import { describe, it, expect, vi } from 'vitest'
2 +
import request from 'supertest'
3 +
import { NextFunction, Request, Response } from 'express'
4 +
import { createApp } from '@app/createApp'
5 +
import { errorHandler } from '@app/middleware/errorHandler'
6 +
import { DomainError } from '@app/lib/errors/base'
7 +
8 +
const app = createApp()
9 +
10 +
function buildRequest() {
11 +
return { log: { error: vi.fn() } }
12 +
}
13 +
14 +
function buildResponse() {
15 +
return {
16 +
status: vi.fn().mockReturnThis(),
17 +
json: vi.fn().mockReturnThis(),
18 +
}
19 +
}
20 +
21 +
describe('errorHandler', () => {
22 +
it('returns 500 for an error that reaches it through the app', async () => {
23 +
const res = await request(app)
24 +
.post('/auth/login')
25 +
.set('Content-Type', 'application/json')
26 +
.send('{"email": ')
27 +
28 +
expect(res.status).toBe(500)
29 +
expect(res.body).toEqual({ error: 'Internal server error' })
30 +
})
31 +
32 +
it('logs the error through the request logger', () => {
33 +
const req = buildRequest()
34 +
const res = buildResponse()
35 +
const err = new Error('kaboom')
36 +
37 +
errorHandler(
38 +
err,
39 +
req as unknown as Request,
40 +
res as unknown as Response,
41 +
vi.fn() as unknown as NextFunction,
42 +
)
43 +
44 +
expect(req.log.error).toHaveBeenCalledWith({ err }, 'request failed')
45 +
expect(res.status).toHaveBeenCalledWith(500)
46 +
})
47 +
48 +
it('maps domain errors to their status and message', () => {
49 +
const req = buildRequest()
50 +
const res = buildResponse()
51 +
52 +
errorHandler(
53 +
new DomainError('nope', 409),
54 +
req as unknown as Request,
55 +
res as unknown as Response,
56 +
vi.fn() as unknown as NextFunction,
57 +
)
58 +
59 +
expect(res.status).toHaveBeenCalledWith(409)
60 +
expect(res.json).toHaveBeenCalledWith({ error: 'nope' })
61 +
})
62 +
})
/dev/null → api/tests/middleware/request-id.test.ts
1 +
import { describe, it, expect } from 'vitest'
2 +
import request from 'supertest'
3 +
import { createApp } from '@app/createApp'
4 +
5 +
const app = createApp()
6 +
7 +
describe('request id', () => {
8 +
it('echoes an inbound X-Request-Id back on the response', async () => {
9 +
const res = await request(app)
10 +
.get('/moods')
11 +
.set('X-Request-Id', 'test-request-id')
12 +
13 +
expect(res.headers['x-request-id']).toBe('test-request-id')
14 +
})
15 +
16 +
it('generates a request id when the client does not send one', async () => {
17 +
const res = await request(app).get('/moods')
18 +
19 +
expect(res.headers['x-request-id']).toBeTruthy()
20 +
})
21 +
})