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'
param version string
param environment Environment
param containerImageTag string
param redisConnection string
@@ -45,6 +46,7 @@ module containerApp 'containerApp.bicep' = {
{ name: 'SENTRY_DSN', value: sentryDSN }
{ name: 'SENTRY_ENABLED', value: sentryEnabled }
{ name: 'SENTRY_TRACE_SAMPLE_RATE', value: sentryTraceSampleRate }
{ name: 'VERSION', value: version }
{ name: 'timestamp', value: timestamp }
]

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
"/",
async ({ query: { key }, status }) => {
key = validateKey(key);
cacheRouteLogger.info("GET /cache", key);
cacheRouteLogger.info(`GET /cache ${key}`);
const value = await redis.get(key);
if (!value) {
@@ -48,7 +48,7 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
"/",
async ({ query: { key }, body, status, set }) => {
key = validateKey(key);
cacheRouteLogger.info("PUT /cache", key);
cacheRouteLogger.info(`PUT /cache ${key}`);
if (!body.ttl || body.ttl < 0) {
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);
return status(204, void 0);
set.status = 204;
return undefined;
},
{
body: t.Object({ data: t.Any(), ttl: t.Number() }),
query: QUERY_TYPE,
response: { 204: t.Void(), 400: t.String() },
response: { 204: t.Undefined(), 400: t.String() },
}
)
.delete(
"/",
async ({ query: { key, fuzzy }, status }) => {
async ({ query: { key, fuzzy }, set }) => {
key = validateKey(key);
cacheRouteLogger.info("DELETE /cache", key, { fuzzy });
cacheRouteLogger.info(
`DELETE /cache ${key} ${fuzzy ? "fuzzy" : ""}`
);
if (fuzzy) {
await deleteWithPattern(`*${key}*`);
} else {
await redis.del(key);
cacheRouteLogger.info("Deleted key: ", key);
const deletedKeys = await redis.del(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_TYPE.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("*")) {
throw new ModelValidationError("Key cannot contain wildcards");
}
@@ -128,7 +135,13 @@ async function deleteWithPattern(pattern: string) {
} while (cursor !== "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 { 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(
"/health",
async ({ set, status }) => {
async ({ status }) => {
const perf = performance.now();
let healthy = true;
try {
await redis.ping();
} catch (e) {
baseLogger.error("Redis connection error:", e);
console.log("Redis connection error:", e);
healthLogger.error("Redis connection error:", e);
return status(503, { healthy: false });
healthy = false;
}
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: {
200: t.Object({
healthy: t.Boolean(),
version: t.String(),
duration: t.String(),
}),
503: t.Object({
healthy: t.Boolean(),
version: t.String(),
duration: t.String(),
}),
},
}