Openstatus www.openstatus.dev
at main 226 lines 7.3 kB view raw
1import { writeFileSync } from "node:fs"; 2import { resolve } from "node:path"; 3import { db, eq } from "@openstatus/db"; 4import { monitor, selectMonitorSchema } from "@openstatus/db/src/schema"; 5import { OSTinybird } from "@openstatus/tinybird"; 6 7// WARNING: make sure to enable the Tinybird client in the env you are running this script in 8 9// Configuration 10const MONITOR_ID = "7002"; 11const PERIOD = "7d" as const; 12const INTERVAL = 60; 13const TYPE = "http" as const; 14const OUTPUT_FILE = "blog-post-metrics.json"; 15const PERCENTILE = "p50"; // p50, p75, p90, p95, p99 16 17async function main() { 18 // Get Tinybird API key from environment 19 const tinybirdApiKey = process.env.TINY_BIRD_API_KEY; 20 if (!tinybirdApiKey) { 21 throw new Error("TINY_BIRD_API_KEY environment variable is required"); 22 } 23 24 const tb = new OSTinybird(tinybirdApiKey); 25 26 console.log(`Fetching data for monitor ID: ${MONITOR_ID}`); 27 28 // 1. Fetch monitor from database with private locations 29 const monitorDataRaw = await db.query.monitor.findFirst({ 30 where: eq(monitor.id, Number.parseInt(MONITOR_ID)), 31 with: { 32 privateLocationToMonitors: { 33 with: { 34 privateLocation: true, 35 }, 36 }, 37 }, 38 }); 39 40 if (!monitorDataRaw) { 41 throw new Error(`Monitor with ID ${MONITOR_ID} not found`); 42 } 43 44 // Parse the monitor data using the schema to convert regions string to array 45 const monitorData = selectMonitorSchema.parse(monitorDataRaw); 46 47 // Get private location names 48 const privateLocationNames = 49 monitorDataRaw.privateLocationToMonitors 50 ?.map((pl) => pl.privateLocation?.name) 51 .filter((name): name is string => Boolean(name)) || []; 52 53 // Combine regular regions with private locations 54 const allRegions = [...monitorData.regions, ...privateLocationNames]; 55 56 console.log(`\nMonitor Details:`); 57 console.log(` ID: ${MONITOR_ID}`); 58 console.log(` Name: ${monitorData.name || "Unnamed"}`); 59 console.log(` Type: ${monitorData.jobType}`); 60 console.log(` Active: ${monitorData.active}`); 61 console.log(` Created: ${monitorData.createdAt}`); 62 console.log(` Regular regions: ${monitorData.regions.join(", ")}`); 63 console.log( 64 ` Private locations: ${privateLocationNames.join(", ") || "None"}` 65 ); 66 console.log(` Total regions: ${allRegions.length}`); 67 console.log(`\nQuery Parameters:`); 68 console.log(` Period: ${PERIOD}`); 69 console.log(` Interval: ${INTERVAL} minutes`); 70 71 // Use the monitor's actual type, or fall back to the configured TYPE 72 const monitorType = (monitorData.jobType || TYPE) as "http" | "tcp"; 73 74 // 2. Fetch metricsRegions (timeline data with region, timestamp, and quantiles) 75 const metricsRegionsResult = 76 monitorType === "http" 77 ? PERIOD === "7d" 78 ? await tb.httpMetricsRegionsWeekly({ 79 monitorId: MONITOR_ID, 80 interval: INTERVAL, 81 }) 82 : await tb.httpMetricsRegionsDaily({ 83 monitorId: MONITOR_ID, 84 interval: INTERVAL, 85 }) 86 : PERIOD === "7d" 87 ? await tb.tcpMetricsByIntervalWeekly({ 88 monitorId: MONITOR_ID, 89 interval: INTERVAL, 90 }) 91 : await tb.tcpMetricsByIntervalDaily({ 92 monitorId: MONITOR_ID, 93 interval: INTERVAL, 94 }); 95 96 console.log( 97 `\nFetched ${metricsRegionsResult.data.length} metrics regions data points` 98 ); 99 if (metricsRegionsResult.data.length > 0) { 100 console.log( 101 ` First data point:`, 102 JSON.stringify(metricsRegionsResult.data[0], null, 2) 103 ); 104 console.log( 105 ` Last data point:`, 106 JSON.stringify( 107 metricsRegionsResult.data[metricsRegionsResult.data.length - 1], 108 null, 109 2 110 ) 111 ); 112 } else { 113 console.log(` ⚠️ No data returned. This could mean:`); 114 console.log(` - The monitor hasn't collected any data yet`); 115 console.log(` - The monitor is inactive or was just created`); 116 console.log( 117 ` - There's no data in the selected time period (${PERIOD})` 118 ); 119 console.log( 120 `\n 💡 Tip: Try querying without the interval parameter or using PERIOD="1d"` 121 ); 122 123 // Try without interval to see if that helps 124 console.log(`\n Trying without interval parameter...`); 125 const retryResult = 126 monitorType === "http" 127 ? PERIOD === "7d" 128 ? await tb.httpMetricsRegionsWeekly({ 129 monitorId: MONITOR_ID, 130 }) 131 : await tb.httpMetricsRegionsDaily({ 132 monitorId: MONITOR_ID, 133 }) 134 : PERIOD === "7d" 135 ? await tb.tcpMetricsByIntervalWeekly({ 136 monitorId: MONITOR_ID, 137 }) 138 : await tb.tcpMetricsByIntervalDaily({ 139 monitorId: MONITOR_ID, 140 }); 141 console.log(` Retry returned ${retryResult.data.length} data points`); 142 if (retryResult.data.length > 0) { 143 console.log(` ✅ Success! The interval parameter might be the issue.`); 144 console.log( 145 ` First data point:`, 146 JSON.stringify(retryResult.data[0], null, 2) 147 ); 148 } 149 } 150 151 // 3. Fetch metricsByRegion (summary data by region) 152 const metricsByRegionProcedure = 153 monitorType === "http" 154 ? PERIOD === "7d" 155 ? tb.httpMetricsByRegionWeekly 156 : tb.httpMetricsByRegionDaily 157 : PERIOD === "7d" 158 ? tb.tcpMetricsByRegionWeekly 159 : tb.tcpMetricsByRegionDaily; 160 161 const metricsByRegionsResult = await metricsByRegionProcedure({ 162 monitorId: MONITOR_ID, 163 }); 164 165 console.log( 166 `\nFetched ${metricsByRegionsResult.data.length} metrics by region data points` 167 ); 168 if (metricsByRegionsResult.data.length > 0) { 169 console.log( 170 ` Sample:`, 171 JSON.stringify(metricsByRegionsResult.data.slice(0, 3), null, 2) 172 ); 173 } 174 175 // 4. Transform metricsRegions data to match expected format 176 // Group by timestamp and pivot regions as columns 177 const timelineMap = new Map<number, Record<string, number | string>>(); 178 179 for (const row of metricsRegionsResult.data) { 180 const timestamp = row.timestamp; 181 const region = row.region; 182 const latency = row[`${PERCENTILE}Latency`] ?? 0; 183 184 if (!timelineMap.has(timestamp)) { 185 timelineMap.set(timestamp, { 186 timestamp: new Date(timestamp).toISOString(), 187 }); 188 } 189 190 const entry = timelineMap.get(timestamp)!; 191 entry[region] = latency; 192 } 193 194 // Convert map to sorted array 195 const timelineData = Array.from(timelineMap.values()).sort((a, b) => { 196 const timeA = new Date(a.timestamp as string).getTime(); 197 const timeB = new Date(b.timestamp as string).getTime(); 198 return timeA - timeB; 199 }); 200 201 // 5. Build final output structure 202 const output = { 203 regions: allRegions, 204 data: { 205 regions: allRegions, 206 data: timelineData, 207 }, 208 metricsByRegions: metricsByRegionsResult.data, 209 }; 210 211 // 6. Write to file 212 const outputPath = resolve(process.cwd(), OUTPUT_FILE); 213 writeFileSync(outputPath, JSON.stringify(output, null, 2)); 214 215 console.log(`\n✅ Data exported successfully to: ${outputPath}`); 216 console.log(`Total timeline entries: ${timelineData.length}`); 217 console.log( 218 `Total regions (including private locations): ${allRegions.length}` 219 ); 220} 221 222// Run the script 223main().catch((error) => { 224 console.error("Error:", error); 225 process.exit(1); 226});