feat(core): opentelementry support (update of #14668) (#16087)

* chore: squash commit

* fix: tweak some mentioned issues

---------

Co-authored-by: incubator4 <aries0robin@gmail.com>
This commit is contained in:
Nya Candy 2024-07-05 19:22:33 +08:00 committed by GitHub
parent a761fd0857
commit 68c7430fbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 315 additions and 1 deletions

View File

@ -12,6 +12,7 @@ import debug from '@/middleware/debug';
import header from '@/middleware/header';
import antiHotlink from '@/middleware/anti-hotlink';
import parameter from '@/middleware/parameter';
import trace from '@/middleware/trace';
import { jsxRenderer } from 'hono/jsx-renderer';
import { trimTrailingSlash } from 'hono/trailing-slash';
@ -37,6 +38,7 @@ app.use(
})
);
app.use(mLogger);
app.use(trace);
app.use(sentry);
app.use(accessControl);
app.use(debug);

View File

@ -7,6 +7,8 @@ import Error from '@/views/error';
import NotFoundError from './types/not-found';
import { requestMetric } from '@/utils/otel';
export const errorHandler: ErrorHandler = (error, ctx) => {
const requestPath = ctx.req.path;
const matchedRoute = ctx.req.routePath;
@ -61,6 +63,7 @@ export const errorHandler: ErrorHandler = (error, ctx) => {
const message = `${error.name}: ${errorMessage}`;
logger.error(`Error in ${requestPath}: ${message}`);
requestMetric.error({ path: requestPath, method: ctx.req.method, status: ctx.res.status });
return config.isPackage
? ctx.json({

View File

@ -1,3 +1,4 @@
import { requestMetric } from '@/utils/otel';
import { MiddlewareHandler } from 'hono';
import logger from '@/utils/logger';
import { getPath, time } from '@/utils/helpers';
@ -34,7 +35,10 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
await next();
logger.info(`${LogPrefix.Outgoing} ${method} ${path} ${colorStatus(ctx.res.status)} ${time(start)}`);
const status = ctx.res.status;
logger.info(`${LogPrefix.Outgoing} ${method} ${path} ${colorStatus(status)} ${time(start)}`);
requestMetric.success(Date.now() - start, { path, method, status });
};
export default middleware;

25
lib/middleware/trace.ts Normal file
View File

@ -0,0 +1,25 @@
import { MiddlewareHandler } from 'hono';
import { getPath } from '@/utils/helpers';
import { config } from '@/config';
import { tracer } from '@/utils/otel';
const middleware: MiddlewareHandler = async (ctx, next) => {
if (config.debugInfo) {
// Only enable tracing in debug mode
const { method, raw } = ctx.req;
const path = getPath(raw);
const span = tracer.startSpan(`${method} ${path}`, {
kind: 1, // server
attributes: {},
});
span.addEvent('invoking handleRequest');
await next();
span.end();
} else {
// Skip
await next();
}
};
export default middleware;

View File

@ -4,9 +4,11 @@ import { Hono, type Handler } from 'hono';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { serveStatic } from '@hono/node-server/serve-static';
import { config } from '@/config';
import index from '@/routes/index';
import robotstxt from '@/routes/robots.txt';
import metrics from '@/routes/metrics';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -99,6 +101,10 @@ for (const namespace in namespaces) {
app.get('/', index);
app.get('/robots.txt', robotstxt);
if (config.debugInfo) {
// Only enable tracing in debug mode
app.get('/metrics', metrics);
}
app.use(
'/*',
serveStatic({

12
lib/routes/metrics.ts Normal file
View File

@ -0,0 +1,12 @@
import type { Handler } from 'hono';
import { getContext } from '@/utils/otel';
const handler: Handler = (ctx) =>
getContext()
.then((val) => ctx.text(val))
.catch((error) => {
ctx.status(500);
ctx.json({ error });
});
export default handler;

2
lib/utils/otel/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './metric';
export * from './trace';

66
lib/utils/otel/metric.ts Normal file
View File

@ -0,0 +1,66 @@
import { Resource } from '@opentelemetry/resources';
import { PrometheusExporter, PrometheusSerializer } from '@opentelemetry/exporter-prometheus';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { MeterProvider } from '@opentelemetry/sdk-metrics';
import { Attributes } from '@opentelemetry/api';
interface IMetricAttributes extends Attributes {
method: string;
path: string;
status: number;
}
interface IHistogramAttributes extends IMetricAttributes {
unit: string;
}
const METRIC_PREFIX = 'rsshub';
const exporter = new PrometheusExporter({});
const provider = new MeterProvider({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'rsshub',
}),
readers: [exporter],
});
const serializer = new PrometheusSerializer();
const meter = provider.getMeter('rsshub');
const requestTotal = meter.createCounter<IMetricAttributes>(`${METRIC_PREFIX}_request_total`);
const requestErrorTotal = meter.createCounter<IMetricAttributes>(`${METRIC_PREFIX}_request_error_total`);
const requestDurationSecondsBucket = meter.createHistogram<IHistogramAttributes>(`${METRIC_PREFIX}_request_duration_seconds_bucket`, {
advice: {
explicitBucketBoundaries: [0.01, 0.1, 1, 2, 5, 15, 30, 60],
},
});
const request_duration_milliseconds_bucket = meter.createHistogram<IHistogramAttributes>(`${METRIC_PREFIX}_request_duration_milliseconds_bucket`, {
advice: {
explicitBucketBoundaries: [10, 20, 50, 100, 250, 500, 1000, 5000, 15000],
},
});
export const requestMetric = {
success: (value: number, attributes: IMetricAttributes) => {
requestTotal.add(1, attributes);
request_duration_milliseconds_bucket.record(value, { unit: 'millisecond', ...attributes });
requestDurationSecondsBucket.record(value / 1000, { unit: 'second', ...attributes });
},
error: (attributes: IMetricAttributes) => {
requestErrorTotal.add(1, attributes);
},
};
export const getContext = () =>
new Promise<string>((resolve, reject) => {
exporter
.collect()
.then((value) => {
resolve(serializer.serialize(value.resourceMetrics));
})
.finally(() => {
reject('');
});
});

28
lib/utils/otel/trace.ts Normal file
View File

@ -0,0 +1,28 @@
import { Resource } from '@opentelemetry/resources';
import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
const provider = new BasicTracerProvider({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'rsshub',
}),
});
const exporter = new OTLPTraceExporter({
// optional OTEL_EXPORTER_OTLP_ENDPOINT=https://localhost:4318
});
provider.addSpanProcessor(
new BatchSpanProcessor(exporter, {
// The maximum queue size. After the size is reached spans are dropped.
maxQueueSize: 4096,
// The interval between two consecutive exports
scheduledDelayMillis: 30000,
})
);
provider.register();
export const tracer = provider.getTracer('rsshub');
export const mainSpan = tracer.startSpan('main');

View File

@ -53,6 +53,13 @@
"@hono/node-server": "1.12.0",
"@hono/zod-openapi": "0.14.7",
"@notionhq/client": "2.2.15",
"@opentelemetry/api": "1.8.0",
"@opentelemetry/exporter-prometheus": "0.49.1",
"@opentelemetry/exporter-trace-otlp-http": "0.49.1",
"@opentelemetry/resources": "1.22.0",
"@opentelemetry/sdk-metrics": "1.22.0",
"@opentelemetry/sdk-trace-base": "1.22.0",
"@opentelemetry/semantic-conventions": "1.22.0",
"@postlight/parser": "2.2.3",
"@scalar/hono-api-reference": "0.5.94",
"@sentry/node": "7.116.0",

View File

@ -17,6 +17,27 @@ importers:
'@notionhq/client':
specifier: 2.2.15
version: 2.2.15
'@opentelemetry/api':
specifier: 1.8.0
version: 1.8.0
'@opentelemetry/exporter-prometheus':
specifier: 0.49.1
version: 0.49.1(@opentelemetry/api@1.8.0)
'@opentelemetry/exporter-trace-otlp-http':
specifier: 0.49.1
version: 0.49.1(@opentelemetry/api@1.8.0)
'@opentelemetry/resources':
specifier: 1.22.0
version: 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-metrics':
specifier: 1.22.0
version: 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-trace-base':
specifier: 1.22.0
version: 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/semantic-conventions':
specifier: 1.22.0
version: 1.22.0
'@postlight/parser':
specifier: 2.2.3
version: 2.2.3
@ -1426,6 +1447,73 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
'@opentelemetry/api-logs@0.49.1':
resolution: {integrity: sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==}
engines: {node: '>=14'}
'@opentelemetry/api@1.8.0':
resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==}
engines: {node: '>=8.0.0'}
'@opentelemetry/core@1.22.0':
resolution: {integrity: sha512-0VoAlT6x+Xzik1v9goJ3pZ2ppi6+xd3aUfg4brfrLkDBHRIVjMP0eBHrKrhB+NKcDyMAg8fAbGL3Npg/F6AwWA==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.9.0'
'@opentelemetry/exporter-prometheus@0.49.1':
resolution: {integrity: sha512-FgzGl6OH22f+Wb1dh/TnoQSnZE2SCADhHx06nMqxivSqRJ9t3AhUdMsEOFt2IMjZClE705pcsLHk10BCJ79vsA==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/exporter-trace-otlp-http@0.49.1':
resolution: {integrity: sha512-KOLtZfZvIrpGZLVvblKsiVQT7gQUZNKcUUH24Zz6Xbi7LJb9Vt6xtUZFYdR5IIjvt47PIqBKDWUQlU0o1wAsRw==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/otlp-exporter-base@0.49.1':
resolution: {integrity: sha512-z6sHliPqDgJU45kQatAettY9/eVF58qVPaTuejw9YWfSRqid9pXPYeegDCSdyS47KAUgAtm+nC28K3pfF27HWg==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': ^1.0.0
'@opentelemetry/otlp-transformer@0.49.1':
resolution: {integrity: sha512-Z+koA4wp9L9e3jkFacyXTGphSWTbOKjwwXMpb0CxNb0kjTHGUxhYRN8GnkLFsFo5NbZPjP07hwAqeEG/uCratQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.9.0'
'@opentelemetry/resources@1.22.0':
resolution: {integrity: sha512-+vNeIFPH2hfcNL0AJk/ykJXoUCtR1YaDUZM+p3wZNU4Hq98gzq+7b43xbkXjadD9VhWIUQqEwXyY64q6msPj6A==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.9.0'
'@opentelemetry/sdk-logs@0.49.1':
resolution: {integrity: sha512-gCzYWsJE0h+3cuh3/cK+9UwlVFyHvj3PReIOCDOmdeXOp90ZjKRoDOJBc3mvk1LL6wyl1RWIivR8Rg9OToyesw==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.4.0 <1.9.0'
'@opentelemetry/api-logs': '>=0.39.1'
'@opentelemetry/sdk-metrics@1.22.0':
resolution: {integrity: sha512-k6iIx6H3TZ+BVMr2z8M16ri2OxWaljg5h8ihGJxi/KQWcjign6FEaEzuigXt5bK9wVEhqAcWLCfarSftaNWkkg==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.9.0'
'@opentelemetry/sdk-trace-base@1.22.0':
resolution: {integrity: sha512-pfTuSIpCKONC6vkTpv6VmACxD+P1woZf4q0K46nSUvXFvOFqjBYKFaAMkKD3M1mlKUUh0Oajwj35qNjMl80m1Q==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.9.0'
'@opentelemetry/semantic-conventions@1.22.0':
resolution: {integrity: sha512-CAOgFOKLybd02uj/GhCdEeeBjOS0yeoDeo/CA7ASBSmenpZHAKGB3iDm/rv3BQLcabb/OprDEsSQ1y0P8A7Siw==}
engines: {node: '>=14'}
'@otplib/core@12.0.1':
resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==}
@ -7824,6 +7912,77 @@ snapshots:
'@open-draft/until@2.1.0': {}
'@opentelemetry/api-logs@0.49.1':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/api@1.8.0': {}
'@opentelemetry/core@1.22.0(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/semantic-conventions': 1.22.0
'@opentelemetry/exporter-prometheus@0.49.1(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-metrics': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/exporter-trace-otlp-http@0.49.1(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/otlp-exporter-base': 0.49.1(@opentelemetry/api@1.8.0)
'@opentelemetry/otlp-transformer': 0.49.1(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-trace-base': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/otlp-exporter-base@0.49.1(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/otlp-transformer@0.49.1(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/api-logs': 0.49.1
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-logs': 0.49.1(@opentelemetry/api-logs@0.49.1)(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-metrics': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-trace-base': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources@1.22.0(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/semantic-conventions': 1.22.0
'@opentelemetry/sdk-logs@0.49.1(@opentelemetry/api-logs@0.49.1)(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/api-logs': 0.49.1
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/sdk-metrics@1.22.0(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.8.0)
lodash.merge: 4.6.2
'@opentelemetry/sdk-trace-base@1.22.0(@opentelemetry/api@1.8.0)':
dependencies:
'@opentelemetry/api': 1.8.0
'@opentelemetry/core': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.8.0)
'@opentelemetry/semantic-conventions': 1.22.0
'@opentelemetry/semantic-conventions@1.22.0': {}
'@otplib/core@12.0.1': {}
'@otplib/plugin-crypto@12.0.1':