Merged in fix/redis-api-model-validation (pull request #2000)

* fix: model response validation for 204
* log: add more logging when deleting keys
* add version to health endpoint


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-05-07 13:45:35 +00:00
parent bf7992da5b
commit 3ea26259df
5 changed files with 62 additions and 23 deletions

View File

@@ -2,6 +2,7 @@ import { Environment, EnvironmentVar } from '../types.bicep'
targetScope = 'subscription' targetScope = 'subscription'
param version string
param environment Environment param environment Environment
param containerImageTag string param containerImageTag string
param redisConnection string param redisConnection string
@@ -45,6 +46,7 @@ module containerApp 'containerApp.bicep' = {
{ name: 'SENTRY_DSN', value: sentryDSN } { name: 'SENTRY_DSN', value: sentryDSN }
{ name: 'SENTRY_ENABLED', value: sentryEnabled } { name: 'SENTRY_ENABLED', value: sentryEnabled }
{ name: 'SENTRY_TRACE_SAMPLE_RATE', value: sentryTraceSampleRate } { name: 'SENTRY_TRACE_SAMPLE_RATE', value: sentryTraceSampleRate }
{ name: 'VERSION', value: version }
{ name: 'timestamp', value: timestamp } { name: 'timestamp', value: timestamp }
] ]

View File

@@ -4,6 +4,7 @@ targetScope = 'subscription'
param environment Environment param environment Environment
param containerImageTag string = 'latest' param containerImageTag string = 'latest'
param version string
param primaryApiKey string param primaryApiKey string
param secondaryApiKey string param secondaryApiKey string
@@ -51,5 +52,6 @@ module containerApp 'app/main.bicep' = {
sentryDSN: sentryDSN sentryDSN: sentryDSN
sentryEnabled: sentryEnabled sentryEnabled: sentryEnabled
sentryTraceSampleRate: sentryTraceSampleRate sentryTraceSampleRate: sentryTraceSampleRate
version: version
} }
} }

View File

@@ -47,7 +47,7 @@ export const env = createEnv({
}, },
createFinalSchema: (shape) => { createFinalSchema: (shape) => {
return z.object(shape).transform((env, ctx) => { return z.object(shape).transform((env, ctx) => {
if (!env.SENTRY_ENABLED || !env.SENTRY_DSN) { if (env.SENTRY_ENABLED && !env.SENTRY_DSN) {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: message:

View File

@@ -16,7 +16,7 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
"/", "/",
async ({ query: { key }, status }) => { async ({ query: { key }, status }) => {
key = validateKey(key); key = validateKey(key);
cacheRouteLogger.info("GET /cache", key); cacheRouteLogger.info(`GET /cache ${key}`);
const value = await redis.get(key); const value = await redis.get(key);
if (!value) { if (!value) {
@@ -48,7 +48,7 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
"/", "/",
async ({ query: { key }, body, status, set }) => { async ({ query: { key }, body, status, set }) => {
key = validateKey(key); key = validateKey(key);
cacheRouteLogger.info("PUT /cache", key); cacheRouteLogger.info(`PUT /cache ${key}`);
if (!body.ttl || body.ttl < 0) { if (!body.ttl || body.ttl < 0) {
return status("Bad Request", "ttl is required"); return status("Bad Request", "ttl is required");
@@ -56,35 +56,45 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
await redis.set(key, JSON.stringify(body.data), "EX", body.ttl); await redis.set(key, JSON.stringify(body.data), "EX", body.ttl);
return status(204, void 0); set.status = 204;
return undefined;
}, },
{ {
body: t.Object({ data: t.Any(), ttl: t.Number() }), body: t.Object({ data: t.Any(), ttl: t.Number() }),
query: QUERY_TYPE, query: QUERY_TYPE,
response: { 204: t.Void(), 400: t.String() }, response: { 204: t.Undefined(), 400: t.String() },
} }
) )
.delete( .delete(
"/", "/",
async ({ query: { key, fuzzy }, status }) => { async ({ query: { key, fuzzy }, set }) => {
key = validateKey(key); key = validateKey(key);
cacheRouteLogger.info("DELETE /cache", key, { fuzzy }); cacheRouteLogger.info(
`DELETE /cache ${key} ${fuzzy ? "fuzzy" : ""}`
);
if (fuzzy) { if (fuzzy) {
await deleteWithPattern(`*${key}*`); await deleteWithPattern(`*${key}*`);
} else { } else {
await redis.del(key); const deletedKeys = await redis.del(key);
cacheRouteLogger.info("Deleted key: ", key); if (deletedKeys === 0) {
cacheRouteLogger.info(
`Key '${key}' not found, nothing deleted`
);
} else {
cacheRouteLogger.info(`Deleted key '${key}'`);
}
} }
return status(204, void 0); set.status = 204;
return undefined;
}, },
{ {
query: t.Object({ query: t.Object({
...QUERY_TYPE.properties, ...QUERY_TYPE.properties,
...t.Object({ fuzzy: t.Optional(t.Boolean()) }).properties, ...t.Object({ fuzzy: t.Optional(t.Boolean()) }).properties,
}), }),
response: { 204: t.Void(), 400: t.String() }, response: { 204: t.Undefined(), 400: t.String() },
} }
); );
@@ -97,9 +107,6 @@ function validateKey(key: string) {
); );
} }
if (parsedKey.includes("*")) {
throw new ModelValidationError("Key cannot contain wildcards");
}
if (parsedKey.includes("*")) { if (parsedKey.includes("*")) {
throw new ModelValidationError("Key cannot contain wildcards"); throw new ModelValidationError("Key cannot contain wildcards");
} }
@@ -128,7 +135,13 @@ async function deleteWithPattern(pattern: string) {
} while (cursor !== "0"); } while (cursor !== "0");
if (keys.length > 0) { if (keys.length > 0) {
await redis.del(...keys); const deleteCount = await redis.del(...keys);
keys.map((key, idx) => {
cacheRouteLogger.info(
`Deleted key ${idx + 1}/${deleteCount}: ${key}`
);
});
cacheRouteLogger.info(`Deleted number of keys: ${deleteCount}`);
} }
cacheRouteLogger.info("Deleted number of keys: ", keys.length);
} }

View File

@@ -1,33 +1,55 @@
import Elysia, { t } from "elysia"; import Elysia, { t } from "elysia";
import { redis } from "@/services/redis"; import { redis } from "@/services/redis";
import { baseLogger } from "@/utils/logger"; import { baseLogger, loggerModule } from "@/utils/logger";
import { env } from "@/env";
const healthLogger = baseLogger.child({
module: "health",
});
export const healthRoutes = new Elysia().get( export const healthRoutes = new Elysia().get(
"/health", "/health",
async ({ set, status }) => { async ({ status }) => {
const perf = performance.now(); const perf = performance.now();
let healthy = true;
try { try {
await redis.ping(); await redis.ping();
} catch (e) { } catch (e) {
baseLogger.error("Redis connection error:", e); healthLogger.error("Redis connection error:", e);
console.log("Redis connection error:", e);
return status(503, { healthy: false }); healthy = false;
} }
const duration = performance.now() - perf; const duration = performance.now() - perf;
baseLogger.info(`Service healthy: ${duration.toFixed(2)} ms`); const durationString = `${duration.toFixed(2)} ms`;
return { healthy: true }; if (!healthy) {
healthLogger.error("Health check failed");
return status(503, {
healthy,
version: env.VERSION,
duration: durationString,
});
}
return {
healthy,
version: env.VERSION,
duration: durationString,
};
}, },
{ {
response: { response: {
200: t.Object({ 200: t.Object({
healthy: t.Boolean(), healthy: t.Boolean(),
version: t.String(),
duration: t.String(),
}), }),
503: t.Object({ 503: t.Object({
healthy: t.Boolean(), healthy: t.Boolean(),
version: t.String(),
duration: t.String(),
}), }),
}, },
} }