feat(p2): vitest + 27 tests + /api/health enrichi + /api/metrics + workflow CI
Some checks failed
CI / test (pull_request) Failing after 2m13s
Some checks failed
CI / test (pull_request) Failing after 2m13s
This commit is contained in:
parent
56e5c48a84
commit
14fd9a5940
10 changed files with 2572 additions and 9 deletions
|
|
@ -1,7 +1,101 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { S3Client, HeadBucketCommand } from "@aws-sdk/client-s3";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Probe = {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
latencyMs: number;
|
||||
details?: string;
|
||||
};
|
||||
|
||||
async function probeDb(): Promise<Probe> {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1 AS ok`;
|
||||
return { name: "database", ok: true, latencyMs: Date.now() - t0 };
|
||||
} catch (e) {
|
||||
return {
|
||||
name: "database",
|
||||
ok: false,
|
||||
latencyMs: Date.now() - t0,
|
||||
details: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function probeS3(): Promise<Probe> {
|
||||
const t0 = Date.now();
|
||||
const bucket = process.env.S3_BUCKET;
|
||||
const endpoint = process.env.S3_ENDPOINT;
|
||||
if (!bucket || !endpoint) {
|
||||
return { name: "s3", ok: false, latencyMs: 0, details: "S3_BUCKET ou S3_ENDPOINT manquant" };
|
||||
}
|
||||
try {
|
||||
const client = new S3Client({
|
||||
endpoint,
|
||||
region: process.env.S3_REGION ?? "us-east-1",
|
||||
forcePathStyle: (process.env.S3_FORCE_PATH_STYLE ?? "false") === "true",
|
||||
credentials: {
|
||||
accessKeyId: process.env.MINIO_ROOT_USER ?? process.env.S3_ACCESS_KEY ?? "",
|
||||
secretAccessKey: process.env.MINIO_ROOT_PASSWORD ?? process.env.S3_SECRET_KEY ?? "",
|
||||
},
|
||||
});
|
||||
await client.send(new HeadBucketCommand({ Bucket: bucket }));
|
||||
return { name: "s3", ok: true, latencyMs: Date.now() - t0 };
|
||||
} catch (e) {
|
||||
return {
|
||||
name: "s3",
|
||||
ok: false,
|
||||
latencyMs: Date.now() - t0,
|
||||
details: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function probeResend(): Probe {
|
||||
return {
|
||||
name: "resend",
|
||||
ok: Boolean(process.env.RESEND_API_KEY?.trim()),
|
||||
latencyMs: 0,
|
||||
details: process.env.RESEND_API_KEY ? undefined : "RESEND_API_KEY non configuré (dry-run)",
|
||||
};
|
||||
}
|
||||
|
||||
function probeStripe(): Probe {
|
||||
const key = (process.env.STRIPE_SECRET_KEY ?? "").trim();
|
||||
const configured = key.length > 0 && !key.includes("REPLACE_ME");
|
||||
return {
|
||||
name: "stripe",
|
||||
ok: configured,
|
||||
latencyMs: 0,
|
||||
details: configured ? undefined : "STRIPE_SECRET_KEY non configuré",
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ status: "ok" });
|
||||
const t0 = Date.now();
|
||||
const [db, s3] = await Promise.all([probeDb(), probeS3()]);
|
||||
const resend = probeResend();
|
||||
const stripe = probeStripe();
|
||||
const probes = [db, s3, resend, stripe];
|
||||
|
||||
// DB est critique (503 si down). Le reste = non bloquant.
|
||||
const critical = db.ok;
|
||||
const status = critical ? 200 : 503;
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: critical ? "ok" : "degraded",
|
||||
version: process.env.DEPLOYMENT_VERSION ?? "unknown",
|
||||
uptimeSeconds: Math.round(process.uptime()),
|
||||
latencyMs: Date.now() - t0,
|
||||
probes,
|
||||
},
|
||||
{ status },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
78
src/app/api/metrics/route.ts
Normal file
78
src/app/api/metrics/route.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { BookingStatus, CarbetStatus, UserRole } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Metrics publiques, agrégées (jamais de PII).
|
||||
* Format JSON simple — consommable par un script cron ou un dashboard léger.
|
||||
*/
|
||||
export async function GET() {
|
||||
const now = new Date();
|
||||
const last24h = new Date(now.getTime() - 86_400_000);
|
||||
const last7d = new Date(now.getTime() - 7 * 86_400_000);
|
||||
const last30d = new Date(now.getTime() - 30 * 86_400_000);
|
||||
|
||||
const [
|
||||
carbetsPublished,
|
||||
carbetsTotal,
|
||||
bookings24h,
|
||||
bookings7d,
|
||||
bookings30d,
|
||||
bookingsByStatus,
|
||||
usersTotal,
|
||||
usersByRole,
|
||||
mediaTotal,
|
||||
auditLast24h,
|
||||
] = await Promise.all([
|
||||
prisma.carbet.count({ where: { status: CarbetStatus.PUBLISHED } }),
|
||||
prisma.carbet.count(),
|
||||
prisma.booking.count({ where: { createdAt: { gte: last24h } } }),
|
||||
prisma.booking.count({ where: { createdAt: { gte: last7d } } }),
|
||||
prisma.booking.count({ where: { createdAt: { gte: last30d } } }),
|
||||
prisma.booking.groupBy({
|
||||
by: ["status"],
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.user.count(),
|
||||
prisma.user.groupBy({
|
||||
by: ["role"],
|
||||
_count: { _all: true },
|
||||
}),
|
||||
prisma.media.count(),
|
||||
prisma.auditLog.count({ where: { createdAt: { gte: last24h } } }),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
generatedAt: now.toISOString(),
|
||||
carbets: {
|
||||
total: carbetsTotal,
|
||||
published: carbetsPublished,
|
||||
},
|
||||
bookings: {
|
||||
last24h: bookings24h,
|
||||
last7d: bookings7d,
|
||||
last30d: bookings30d,
|
||||
byStatus: Object.fromEntries(
|
||||
Object.values(BookingStatus).map((s) => [
|
||||
s,
|
||||
bookingsByStatus.find((b) => b.status === s)?._count._all ?? 0,
|
||||
]),
|
||||
),
|
||||
},
|
||||
users: {
|
||||
total: usersTotal,
|
||||
byRole: Object.fromEntries(
|
||||
Object.values(UserRole).map((r) => [
|
||||
r,
|
||||
usersByRole.find((u) => u.role === r)?._count._all ?? 0,
|
||||
]),
|
||||
),
|
||||
},
|
||||
media: { total: mediaTotal },
|
||||
audit: { last24h: auditLast24h },
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue