mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(threads): thread and forum API, admin, schemas and docs (#3251)
This commit is contained in:
1 parent
5f4295e399
commit
3f4160b138
495 files changed
+39232
-1215
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
import {isLoopbackIpAddress} from '@fluxer/ip_utils/src/IpAddress';
|
||||
import type {HttpBindings} from '@hono/node-server';
|
||||
import type {Handler, MiddlewareHandler} from 'hono';
|
||||
import type {Context, Handler, MiddlewareHandler} from 'hono';
|
||||
|
||||
const SKIP_PATHS = new Set(['/_health', '/_healthz', '/_metrics']);
|
||||
|
||||
@@ -24,11 +24,11 @@ function statusClass(status: number): string {
|
||||
return '5xx';
|
||||
}
|
||||
|
||||
class Counter {
|
||||
export class Counter {
|
||||
private labels = new Map<string, number>();
|
||||
|
||||
inc(labelKey: string = ''): void {
|
||||
this.labels.set(labelKey, (this.labels.get(labelKey) ?? 0) + 1);
|
||||
inc(labelKey: string = '', amount = 1): void {
|
||||
this.labels.set(labelKey, (this.labels.get(labelKey) ?? 0) + amount);
|
||||
}
|
||||
|
||||
render(name: string, help: string): string {
|
||||
@@ -50,7 +50,7 @@ class Counter {
|
||||
}
|
||||
}
|
||||
|
||||
class Histogram {
|
||||
export class Histogram {
|
||||
private readonly buckets: Array<number>;
|
||||
private readonly counts: Array<number>;
|
||||
private sum = 0;
|
||||
@@ -85,7 +85,7 @@ class Histogram {
|
||||
}
|
||||
}
|
||||
|
||||
class Gauge {
|
||||
export class Gauge {
|
||||
private valueFn: () => number;
|
||||
|
||||
constructor(valueFn: () => number) {
|
||||
@@ -119,6 +119,62 @@ function renderRegisteredSections(): Array<string> {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
interface RegisteredMetric {
|
||||
help: string;
|
||||
metric: Counter | Histogram | Gauge;
|
||||
}
|
||||
|
||||
const registeredMetrics = new Map<string, RegisteredMetric>();
|
||||
|
||||
function register<T extends Counter | Histogram | Gauge>(
|
||||
name: string,
|
||||
help: string,
|
||||
create: () => T,
|
||||
kind: new (...args: Array<never>) => T,
|
||||
): T {
|
||||
const existing = registeredMetrics.get(name);
|
||||
if (existing !== undefined) {
|
||||
if (!(existing.metric instanceof kind)) {
|
||||
throw new Error(`metric ${name} is already registered with another type`);
|
||||
}
|
||||
return existing.metric;
|
||||
}
|
||||
const metric = create();
|
||||
registeredMetrics.set(name, {help, metric});
|
||||
return metric;
|
||||
}
|
||||
|
||||
export function registerCounter(name: string, help: string): Counter {
|
||||
return register(name, help, () => new Counter(), Counter);
|
||||
}
|
||||
|
||||
export function registerHistogram(name: string, help: string, buckets: Array<number> = DEFAULT_BUCKETS): Histogram {
|
||||
return register(name, help, () => new Histogram(buckets), Histogram);
|
||||
}
|
||||
|
||||
export function registerGauge(name: string, help: string, valueFn: () => number): Gauge {
|
||||
return register(name, help, () => new Gauge(valueFn), Gauge);
|
||||
}
|
||||
|
||||
function renderRegisteredMetrics(): Array<string> {
|
||||
return [...registeredMetrics].map(([name, {help, metric}]) => metric.render(name, help));
|
||||
}
|
||||
|
||||
const METRICS_CONTENT_TYPE = 'text/plain; version=0.0.4; charset=utf-8';
|
||||
|
||||
function renderMetricsResponse(c: Context, sections: Array<string>): Response {
|
||||
if (!isLoopbackPeer(c.env)) {
|
||||
return c.text('FORBIDDEN', 403, {'Content-Type': 'text/plain'});
|
||||
}
|
||||
return c.text(`${[...sections, ...renderRegisteredMetrics()].join('\n\n')}\n`, 200, {
|
||||
'Content-Type': METRICS_CONTENT_TYPE,
|
||||
});
|
||||
}
|
||||
|
||||
export function createRegisteredMetricsHandler(): Handler {
|
||||
return (c) => renderMetricsResponse(c, []);
|
||||
}
|
||||
|
||||
interface MetricsState {
|
||||
requestsTotal: Counter;
|
||||
errorsTotal: Counter;
|
||||
@@ -165,21 +221,14 @@ export function createMetricsMiddleware(serviceName: string): MetricsResult {
|
||||
}
|
||||
};
|
||||
|
||||
const metricsHandler: Handler = (c) => {
|
||||
if (!isLoopbackPeer(c.env)) {
|
||||
return c.text('FORBIDDEN', 403, {'Content-Type': 'text/plain'});
|
||||
}
|
||||
const sections = [
|
||||
const metricsHandler: Handler = (c) =>
|
||||
renderMetricsResponse(c, [
|
||||
requestsTotal.render(`${prefix}_http_requests_total`, 'Total HTTP requests'),
|
||||
requestDuration.render(`${prefix}_http_request_duration_seconds`, 'HTTP request duration in seconds'),
|
||||
errorsTotal.render(`${prefix}_http_errors_total`, 'Total HTTP 5xx errors'),
|
||||
uptime.render(`${prefix}_uptime_seconds`, 'Process uptime in seconds'),
|
||||
...renderRegisteredSections(),
|
||||
];
|
||||
return c.text(`${sections.join('\n\n')}\n`, 200, {
|
||||
'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
|
||||
});
|
||||
};
|
||||
]);
|
||||
|
||||
return {middleware, metricsHandler, state};
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {createMetricsMiddleware, registerMetricsSection} from '@fluxer/hono/src/middleware/Metrics';
|
||||
import {
|
||||
createMetricsMiddleware,
|
||||
createRegisteredMetricsHandler,
|
||||
registerCounter,
|
||||
registerGauge,
|
||||
registerHistogram,
|
||||
registerMetricsSection,
|
||||
} from '@fluxer/hono/src/middleware/Metrics';
|
||||
import {Hono} from 'hono';
|
||||
import {describe, expect, test} from 'vitest';
|
||||
|
||||
@@ -265,4 +272,44 @@ describe('Metrics Middleware', () => {
|
||||
expect(await (await requestMetrics(app)).text()).not.toContain('fluxer_test_extra');
|
||||
});
|
||||
});
|
||||
|
||||
describe('registered metrics', () => {
|
||||
test('renders registered counters, gauges and histograms after the http metrics', async () => {
|
||||
const counter = registerCounter('fluxer_test_registered_total', 'Registered counter');
|
||||
counter.inc('kind="a"');
|
||||
counter.inc('kind="a"', 2);
|
||||
registerGauge('fluxer_test_registered_gauge', 'Registered gauge', () => 7);
|
||||
registerHistogram('fluxer_test_registered_seconds', 'Registered histogram', [1, 5]).observe(2);
|
||||
const {app} = createTestApp();
|
||||
const body = await (await requestMetrics(app)).text();
|
||||
expect(body).toContain('# TYPE fluxer_test_registered_total counter');
|
||||
expect(body).toContain('fluxer_test_registered_total{kind="a"} 3');
|
||||
expect(body).toContain('fluxer_test_registered_gauge 7');
|
||||
expect(body).toContain('fluxer_test_registered_seconds_bucket{le="5"} 1');
|
||||
expect(body.indexOf('fluxer_test_uptime_seconds')).toBeLessThan(body.indexOf('fluxer_test_registered_total'));
|
||||
});
|
||||
|
||||
test('returns the same metric when registered twice', () => {
|
||||
const first = registerCounter('fluxer_test_registered_twice_total', 'Registered twice');
|
||||
expect(registerCounter('fluxer_test_registered_twice_total', 'Registered twice')).toBe(first);
|
||||
});
|
||||
|
||||
test('refuses to register one name as two types', () => {
|
||||
registerCounter('fluxer_test_registered_clash', 'Clash');
|
||||
expect(() => registerGauge('fluxer_test_registered_clash', 'Clash', () => 0)).toThrow();
|
||||
});
|
||||
|
||||
test('serves only registered metrics from the standalone handler', async () => {
|
||||
registerCounter('fluxer_test_standalone_total', 'Standalone counter').inc();
|
||||
const app = new Hono();
|
||||
app.get('/_metrics', createRegisteredMetricsHandler());
|
||||
const res = await requestMetrics(app);
|
||||
const body = await res.text();
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('Content-Type')).toBe('text/plain; version=0.0.4; charset=utf-8');
|
||||
expect(body).toContain('fluxer_test_standalone_total 1');
|
||||
expect(body).not.toContain('_http_requests_total');
|
||||
expect((await requestMetrics(app, '8.8.8.8')).status).toBe(403);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user