- Clone of github.com/fluxerapp/fluxer (official upstream) - SELF_HOSTING.md: full VM rebuild procedure, architecture overview, service reference, step-by-step setup, troubleshooting, seattle reference - dev/.env.example: all env vars with secrets redacted and generation instructions - dev/livekit.yaml: LiveKit config template with placeholder keys - fluxer-seattle/: existing seattle deployment setup scripts
155 lines
4.4 KiB
TypeScript
155 lines
4.4 KiB
TypeScript
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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<HonoEnv>(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,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|