/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import type {ErrorContext, ErrorType, HonoEnv} from '@fluxer/media_proxy/src/types/HonoEnv'; import type {MetricsInterface} from '@fluxer/media_proxy/src/types/Metrics'; import {createMiddleware} from 'hono/factory'; function getRouteFromPath(path: string): string | null { if (path === '/_health' || path === '/internal/telemetry') return null; if (path.startsWith('/avatars/')) return 'avatars'; if (path.startsWith('/icons/')) return 'icons'; if (path.startsWith('/banners/')) return 'banners'; if (path.startsWith('/emojis/')) return 'emojis'; if (path.startsWith('/stickers/')) return 'stickers'; if (path.startsWith('/attachments/')) return 'attachments'; if (path.startsWith('/external/')) return 'external'; if (path.startsWith('/guilds/')) return 'guild_assets'; return 'other'; } function getErrorTypeFromStatus(status: number): ErrorType { switch (status) { case 400: return 'bad_request'; case 401: return 'unauthorized'; case 403: return 'forbidden'; case 404: return 'not_found'; case 408: return 'timeout'; case 413: return 'payload_too_large'; default: if (status >= 500 && status < 600) { return 'upstream_5xx'; } return 'other'; } } export function createMetricsMiddleware(metrics: MetricsInterface) { return createMiddleware(async (ctx, next) => { const start = Date.now(); let errorType: ErrorType | undefined; let errorSource: string | undefined; try { await next(); } catch (error) { if (error instanceof Error) { const message = error.message.toLowerCase(); if (message.includes('timeout') || message.includes('timed out') || message.includes('etimedout')) { errorType = 'timeout'; errorSource = 'network'; } else if ( message.includes('econnrefused') || message.includes('econnreset') || message.includes('enotfound') ) { errorType = 'upstream_5xx'; errorSource = 'network'; } } throw error; } finally { const duration = Date.now() - start; const route = getRouteFromPath(ctx.req.path); if (route !== null) { const status = ctx.res.status; const baseDimensions = { 'http.request.method': ctx.req.method, 'url.path': route, 'http.response.status_code': String(status), }; metrics.histogram({ name: 'http.server.request.duration', dimensions: baseDimensions, valueMs: duration, }); metrics.counter({ name: 'http.server.request.count', dimensions: baseDimensions, value: 1, }); metrics.histogram({ name: 'media_proxy.latency', dimensions: {route}, valueMs: duration, }); metrics.counter({ name: 'media_proxy.request', dimensions: {route, status: String(status)}, }); if (status >= 400) { const errorContext = ctx.get('metricsErrorContext') as ErrorContext | undefined; const finalErrorType = errorContext?.errorType ?? errorType ?? getErrorTypeFromStatus(status); const finalErrorSource = errorContext?.errorSource ?? errorSource ?? 'handler'; metrics.counter({ name: 'media_proxy.failure', dimensions: { route, status: String(status), error_type: finalErrorType, error_source: finalErrorSource, }, }); } else { metrics.counter({ name: 'media_proxy.success', dimensions: {route, status: String(status)}, }); } const contentLength = ctx.res.headers.get('content-length'); if (contentLength) { const bytes = Number.parseInt(contentLength, 10); if (!Number.isNaN(bytes)) { metrics.counter({ name: 'media_proxy.bytes', dimensions: {route}, value: bytes, }); } } } } }); }