a cache for slack profile pictures and emojis

feat: add fancy health check

dunkirk.sh 9e439f46 d8035dc6

verified
Changed files
+141 -3
src
handlers
routes
+80
src/cache.ts
··· 602 602 } 603 603 604 604 /** 605 + * Detailed health check with component status 606 + * @returns Object with detailed health information 607 + */ 608 + async detailedHealthCheck(): Promise<{ 609 + status: "healthy" | "degraded" | "unhealthy"; 610 + checks: { 611 + database: { status: boolean; latency?: number }; 612 + slackApi: { status: boolean; error?: string }; 613 + queueDepth: number; 614 + memoryUsage: { 615 + heapUsed: number; 616 + heapTotal: number; 617 + percentage: number; 618 + }; 619 + }; 620 + uptime: number; 621 + }> { 622 + const checks = { 623 + database: { status: false, latency: 0 }, 624 + slackApi: { status: false }, 625 + queueDepth: this.userUpdateQueue.size, 626 + memoryUsage: { 627 + heapUsed: 0, 628 + heapTotal: 0, 629 + percentage: 0, 630 + }, 631 + }; 632 + 633 + // Check database 634 + try { 635 + const start = Date.now(); 636 + this.db.query("SELECT 1").get(); 637 + checks.database = { status: true, latency: Date.now() - start }; 638 + } catch (error) { 639 + console.error("Database health check failed:", error); 640 + } 641 + 642 + // Check Slack API if wrapper is available 643 + if (this.slackWrapper) { 644 + try { 645 + await this.slackWrapper.getUserInfo("U062UG485EE"); // Use a known test user 646 + checks.slackApi = { status: true }; 647 + } catch (error) { 648 + checks.slackApi = { 649 + status: false, 650 + error: error instanceof Error ? error.message : "Unknown error", 651 + }; 652 + } 653 + } else { 654 + checks.slackApi = { status: true }; // No wrapper means not critical 655 + } 656 + 657 + // Check memory usage 658 + const memUsage = process.memoryUsage(); 659 + checks.memoryUsage = { 660 + heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), 661 + heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), 662 + percentage: Math.round( 663 + (memUsage.heapUsed / memUsage.heapTotal) * 100, 664 + ), 665 + }; 666 + 667 + // Determine overall status 668 + let status: "healthy" | "degraded" | "unhealthy" = "healthy"; 669 + if (!checks.database.status) { 670 + status = "unhealthy"; 671 + } else if (!checks.slackApi.status || checks.queueDepth > 100) { 672 + status = "degraded"; 673 + } else if (checks.memoryUsage.percentage > 90) { 674 + status = "degraded"; 675 + } 676 + 677 + return { 678 + status, 679 + checks, 680 + uptime: process.uptime(), 681 + }; 682 + } 683 + 684 + /** 605 685 * Sets the Slack wrapper for user updates 606 686 * @param slackWrapper SlackUserProvider instance for API calls 607 687 */
+11 -1
src/handlers/index.ts
··· 21 21 } 22 22 23 23 export const handleHealthCheck: RouteHandlerWithAnalytics = async ( 24 - _request, 24 + request, 25 25 recordAnalytics, 26 26 ) => { 27 + const url = new URL(request.url); 28 + const detailed = url.searchParams.get("detailed") === "true"; 29 + 30 + if (detailed) { 31 + const health = await cache.detailedHealthCheck(); 32 + const statusCode = health.status === "unhealthy" ? 503 : health.status === "degraded" ? 200 : 200; 33 + await recordAnalytics(statusCode); 34 + return Response.json(health, { status: statusCode }); 35 + } 36 + 27 37 const isHealthy = await cache.healthCheck(); 28 38 if (isHealthy) { 29 39 await recordAnalytics(200);
+50 -2
src/routes/api-routes.ts
··· 26 26 withAnalytics("/health", "GET", handlers.handleHealthCheck), 27 27 { 28 28 summary: "Health check", 29 - description: "Check if the service is healthy and operational", 29 + description: 30 + "Check if the service is healthy and operational. Add ?detailed=true for comprehensive health information including Slack API status, queue depth, and memory usage.", 30 31 tags: ["Health"], 32 + parameters: { 33 + query: [ 34 + queryParam( 35 + "detailed", 36 + "boolean", 37 + "Return detailed health check information", 38 + false, 39 + false, 40 + ), 41 + ], 42 + }, 31 43 responses: Object.fromEntries([ 32 44 apiResponse(200, "Service is healthy", { 33 45 type: "object", 34 46 properties: { 35 - status: { type: "string", example: "healthy" }, 47 + status: { 48 + type: "string", 49 + example: "healthy", 50 + enum: ["healthy", "degraded", "unhealthy"], 51 + }, 36 52 cache: { type: "boolean", example: true }, 37 53 uptime: { type: "number", example: 123456 }, 54 + checks: { 55 + type: "object", 56 + description: "Detailed checks (only with ?detailed=true)", 57 + properties: { 58 + database: { 59 + type: "object", 60 + properties: { 61 + status: { type: "boolean" }, 62 + latency: { type: "number", description: "ms" }, 63 + }, 64 + }, 65 + slackApi: { 66 + type: "object", 67 + properties: { 68 + status: { type: "boolean" }, 69 + error: { type: "string" }, 70 + }, 71 + }, 72 + queueDepth: { 73 + type: "number", 74 + description: "Number of users queued for update", 75 + }, 76 + memoryUsage: { 77 + type: "object", 78 + properties: { 79 + heapUsed: { type: "number", description: "MB" }, 80 + heapTotal: { type: "number", description: "MB" }, 81 + percentage: { type: "number" }, 82 + }, 83 + }, 84 + }, 85 + }, 38 86 }, 39 87 }), 40 88 apiResponse(503, "Service is unhealthy"),