feat: make HTTP API optional (default false) and add /dev system command for in-bot telemetry
swadhinbiswas committed 1 months ago
3eb23ce
1 parent 6bdafc3
commit 3eb23ce
5 files changed
+117 -18
Showing 5 changed files+117-18
βΌ.env.example
+4-0
| @@ -4,6 +4,10 @@ | ||
4 | 4 | |
5 | 5 | NODE_ENV=development |
6 | 6 | LOG_LEVEL=info |
7 | + | |
8 | +# Optional HTTP API server (Disabled by default: pure Discord bot mode with zero open ports) | |
9 | +# Set to 'true' only if deploying to hosting platforms (Render/Fly.io) requiring an HTTP health check port | |
10 | +ENABLE_HTTP_API=false | |
7 | 11 | PORT=3000 |
8 | 12 | HOST=0.0.0.0 |
9 | 13 | |
10 | 14 | |
βΌapps/bot/src/commands/dev.ts
+84-0
| @@ -1,4 +1,5 @@ | ||
1 | 1 | import { analyticsService } from "@devpulse/analytics"; |
2 | +import { formatBytes } from "@devpulse/core"; | |
2 | 3 | import { db, users } from "@devpulse/database"; |
3 | 4 | import { githubClient } from "@devpulse/github"; |
4 | 5 | import { SlashCommandBuilder } from "discord.js"; |
| @@ -18,6 +19,13 @@ export const devCommand: Command = { | ||
18 | 19 | "View your personal developer productivity dashboard (activity, focus, cycle time)", |
19 | 20 | ), |
20 | 21 | ) |
22 | + .addSubcommand((sub) => | |
23 | + sub | |
24 | + .setName("system") | |
25 | + .setDescription( | |
26 | + "View DevPulse bot system health, database status, memory stats, and gateway latency", | |
27 | + ), | |
28 | + ) | |
21 | 29 | .addSubcommand((sub) => |
22 | 30 | sub |
23 | 31 | .setName("profile") |
| @@ -79,6 +87,82 @@ export const devCommand: Command = { | ||
79 | 87 | return; |
80 | 88 | } |
81 | 89 | |
90 | + if (subcommand === "system") { | |
91 | + const memory = process.memoryUsage(); | |
92 | + const allServers = await db.query.servers.findMany(); | |
93 | + const allRepos = await db.query.repositories.findMany(); | |
94 | + const allMonitors = await db.query.monitors.findMany(); | |
95 | + | |
96 | + const uptimeSec = Math.floor(process.uptime()); | |
97 | + const days = Math.floor(uptimeSec / 86400); | |
98 | + const hours = Math.floor((uptimeSec % 86400) / 3600); | |
99 | + const minutes = Math.floor((uptimeSec % 3600) / 60); | |
100 | + const seconds = uptimeSec % 60; | |
101 | + const uptimeParts: string[] = []; | |
102 | + if (days > 0) uptimeParts.push(`${days}d`); | |
103 | + if (hours > 0) uptimeParts.push(`${hours}h`); | |
104 | + if (minutes > 0) uptimeParts.push(`${minutes}m`); | |
105 | + uptimeParts.push(`${seconds}s`); | |
106 | + const uptimeStr = uptimeParts.join(" "); | |
107 | + | |
108 | + const ping = interaction.client.ws.ping; | |
109 | + const pingStatus = ping < 150 ? "π’" : ping < 300 ? "π‘" : "π΄"; | |
110 | + | |
111 | + const embed = createBaseEmbed("β‘ DevPulse Bot System Telemetry") | |
112 | + .setColor(BrandColors.primary) | |
113 | + .setDescription("Live operational status, database health, and memory metrics:") | |
114 | + .addFields( | |
115 | + { | |
116 | + name: "πΆ Gateway Latency", | |
117 | + value: `${pingStatus} \`${ping >= 0 ? `${ping}ms` : "Connecting..."}\``, | |
118 | + inline: true, | |
119 | + }, | |
120 | + { | |
121 | + name: "β±οΈ Bot Uptime", | |
122 | + value: `\`${uptimeStr}\``, | |
123 | + inline: true, | |
124 | + }, | |
125 | + { | |
126 | + name: "ποΈ Database (libSQL)", | |
127 | + value: "π’ Connected", | |
128 | + inline: true, | |
129 | + }, | |
130 | + { | |
131 | + name: "π§ Memory (RSS)", | |
132 | + value: `\`${formatBytes(memory.rss)}\``, | |
133 | + inline: true, | |
134 | + }, | |
135 | + { | |
136 | + name: "π¦ Heap (Used / Total)", | |
137 | + value: `\`${formatBytes(memory.heapUsed)} / ${formatBytes(memory.heapTotal)}\``, | |
138 | + inline: true, | |
139 | + }, | |
140 | + { | |
141 | + name: "β‘ Runtime", | |
142 | + value: `\`Bun ${typeof Bun !== "undefined" ? (Bun as any).version : process.version}\``, | |
143 | + inline: true, | |
144 | + }, | |
145 | + { | |
146 | + name: "π Discord Servers", | |
147 | + value: `\`${interaction.client.guilds.cache.size}\` Connected (\`${allServers.length}\` Registered)`, | |
148 | + inline: true, | |
149 | + }, | |
150 | + { | |
151 | + name: "π‘ Tracked Repos", | |
152 | + value: `\`${allRepos.length}\` Repositories`, | |
153 | + inline: true, | |
154 | + }, | |
155 | + { | |
156 | + name: "π¦ Active Monitors", | |
157 | + value: `\`${allMonitors.filter((m) => m.isActive).length} / ${allMonitors.length}\` Active`, | |
158 | + inline: true, | |
159 | + }, | |
160 | + ); | |
161 | + | |
162 | + await interaction.editReply({ embeds: [embed] }); | |
163 | + return; | |
164 | + } | |
165 | + | |
82 | 166 | if (subcommand === "dashboard") { |
83 | 167 | const stats = await analyticsService.getPersonalDashboard(targetUser.id, 7); |
84 | 168 | const embed = createPersonalDashboardEmbed(stats); |
85 | 169 | |
βΌapps/bot/src/commands/help.ts
+2-2
| @@ -41,8 +41,8 @@ export const helpCommand: Command = { | ||
41 | 41 | inline: false, |
42 | 42 | }, |
43 | 43 | { |
44 | - name: "π¨βπ» Developer Telemetry", | |
45 | - value: "`/dev dashboard`, `/dev profile`, `/dev link-github`", | |
44 | + name: "π¨βπ» Developer Telemetry & System", | |
45 | + value: "`/dev dashboard`, `/dev profile`, `/dev link-github`, `/dev system`", | |
46 | 46 | inline: false, |
47 | 47 | }, |
48 | 48 | { |
49 | 49 | |
βΌapps/bot/src/index.ts
+26-16
| @@ -34,17 +34,21 @@ export async function startUnifiedApp() { | ||
34 | 34 | // 2. Initialize Discord Client |
35 | 35 | const client = createBotClient(); |
36 | 36 | |
37 | - // 3. Initialize & start Fastify HTTP API Server (exposes /health, /ready, /metrics) | |
38 | - const apiServer = buildServer({ discordClient: client }); | |
39 | - try { | |
40 | - const address = await apiServer.listen({ | |
41 | - port: config.PORT, | |
42 | - host: config.HOST, | |
43 | - }); | |
44 | - logger.info({ address }, `DevPulse HTTP API server listening on ${address}`); | |
45 | - } catch (err) { | |
46 | - logger.error({ err }, "Failed to bind DevPulse HTTP API server"); | |
47 | - process.exit(1); | |
37 | + // 3. Conditionally initialize Fastify HTTP API Server (pure Discord bot mode by default) | |
38 | + let apiServer: any = null; | |
39 | + if (config.ENABLE_HTTP_API) { | |
40 | + apiServer = buildServer({ discordClient: client }); | |
41 | + try { | |
42 | + const address = await apiServer.listen({ | |
43 | + port: config.PORT, | |
44 | + host: config.HOST, | |
45 | + }); | |
46 | + logger.info({ address }, `DevPulse HTTP API server listening on ${address}`); | |
47 | + } catch (err) { | |
48 | + logger.error({ err }, "Failed to bind DevPulse HTTP API server"); | |
49 | + } | |
50 | + } else { | |
51 | + logger.info("Running in pure Discord bot mode (HTTP API disabled, zero open ports needed)"); | |
48 | 52 | } |
49 | 53 | |
50 | 54 | // 4. Graceful shutdown handler |
| @@ -52,10 +56,12 @@ export async function startUnifiedApp() { | ||
52 | 56 | const shutdown = async (signal: string) => { |
53 | 57 | if (isShuttingDown) return; |
54 | 58 | isShuttingDown = true; |
55 | - logger.info({ signal }, "Gracefully shutting down DevPulse unified platform..."); | |
59 | + logger.info({ signal }, "Gracefully shutting down DevPulse platform..."); | |
56 | 60 | scheduler.stop(); |
57 | 61 | try { |
58 | - await apiServer.close(); | |
62 | + if (apiServer) { | |
63 | + await apiServer.close(); | |
64 | + } | |
59 | 65 | client.destroy(); |
60 | 66 | logger.info("DevPulse shutdown completed successfully."); |
61 | 67 | } catch (err) { |
| @@ -75,9 +81,13 @@ export async function startUnifiedApp() { | ||
75 | 81 | config.DISCORD_TOKEN.startsWith("your_discord_bot_token"); |
76 | 82 | |
77 | 83 | if (isDefaultOrPlaceholderToken) { |
78 | - logger.warn( | |
79 | - `β οΈ DevPulse HTTP API is running on port ${config.PORT}, but DISCORD_TOKEN in .env is not configured.`, | |
80 | - ); | |
84 | + if (config.ENABLE_HTTP_API) { | |
85 | + logger.warn( | |
86 | + `β οΈ DevPulse HTTP API is running on port ${config.PORT}, but DISCORD_TOKEN in .env is not configured.`, | |
87 | + ); | |
88 | + } else { | |
89 | + logger.warn("β οΈ DevPulse is ready, but DISCORD_TOKEN in .env is not configured."); | |
90 | + } | |
81 | 91 | logger.warn( |
82 | 92 | "π Please edit /home/swadhin/discordbot/.env and set your DISCORD_TOKEN and DISCORD_CLIENT_ID to connect to Discord.", |
83 | 93 | ); |
84 | 94 | |
βΌpackages/config/src/index.ts
+1-0
| @@ -3,6 +3,7 @@ import { z } from "zod"; | ||
3 | 3 | export const ConfigSchema = z.object({ |
4 | 4 | NODE_ENV: z.enum(["development", "production", "test"]).default("development"), |
5 | 5 | LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"), |
6 | + ENABLE_HTTP_API: z.coerce.boolean().default(false), | |
6 | 7 | PORT: z.coerce.number().int().positive().default(3000), |
7 | 8 | HOST: z.string().default("0.0.0.0"), |
8 | 9 | |
9 | 10 | |