Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: opentelementry support #14668

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 logger from '@/utils/logger';

Expand All @@ -27,6 +28,7 @@ const app = new Hono();
app.use(compress());

app.use(mLogger);
app.use(trace);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If DEBUG_INFO is set to false or any strings other than true, the debug info from RSSHub will be hidden and only available if the same string is provided as query string debug.

/metrics does not respect this behaviour currently.

The same also apply to ACCESS_KEY.

if (requestPath === '/' || requestPath === '/robots.txt' || requestPath === '/metrics') {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basicly, metrics should be export with a external local port. In that case rsshub may handle another port with 127.0.0.1:9090 for example.
I'm not sure if an external port should be created, but it might be a better solution than modify if else for access-control.
And also should be with env to control local server enable or not.

app.use(sentry);
app.use(accessControl);
app.use(debug);
Expand Down
3 changes: 3 additions & 0 deletions lib/errors/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import RequestInProgressError from './request-in-progress';
import RejectError from './reject';
import NotFoundError from './not-found';

import { requestMetric } from '@/utils/otel';

export const errorHandler: ErrorHandler = (error, ctx) => {
const requestPath = ctx.req.path;
const matchedRoute = ctx.req.routePath;
Expand Down Expand Up @@ -61,6 +63,7 @@ export const errorHandler: ErrorHandler = (error, ctx) => {
}

logger.error(`Error in ${requestPath}: ${message}`);
requestMetric.error({ path: requestPath, method: ctx.req.method, status: ctx.res.status });

return config.isPackage ? ctx.json({
error: {
Expand Down
2 changes: 2 additions & 0 deletions lib/init-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as path from 'node:path';
import index from '@/routes/index';
import robotstxt from '@/routes/robots.txt';
import test from '@/routes/test/router';
import metrics from '@/routes/metrics';

import { getCurrentPath } from '@/utils/helpers';
const __dirname = getCurrentPath(import.meta.url);
Expand Down Expand Up @@ -54,6 +55,7 @@ export default function (app: Hono) {
// routes without rss data
app.get('/', index);
app.get('/robots.txt', robotstxt);
app.get('/metrics', metrics);

app.use(
'/*',
Expand Down
2 changes: 1 addition & 1 deletion lib/middleware/access-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
const accessKey = ctx.req.query('key');
const accessCode = ctx.req.query('code');

if (requestPath === '/' || requestPath === '/robots.txt') {
if (requestPath === '/' || requestPath === '/robots.txt' || requestPath === '/metrics') {
await next();
} else {
if (config.accessKey && !(config.accessKey === accessKey || accessCode === md5(requestPath + config.accessKey))) {
Expand Down
6 changes: 5 additions & 1 deletion lib/middleware/logger.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { requestMetric } from '@/utils/otel';
import { MiddlewareHandler } from 'hono';
import logger from '@/utils/logger';
import { getPath, time } from '@/utils/helpers';
Expand Down Expand Up @@ -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;
18 changes: 18 additions & 0 deletions lib/middleware/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MiddlewareHandler } from 'hono';
import { getPath } from '@/utils/helpers';
import { tracer } from '@/utils/otel';

const middleware: MiddlewareHandler = async (ctx, next) => {
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();
};

export default middleware;
12 changes: 12 additions & 0 deletions lib/routes/metrics.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions lib/utils/otel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './metric';
export * from './trace';
66 changes: 66 additions & 0 deletions lib/utils/otel/metric.ts
Original file line number Diff line number Diff line change
@@ -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 request_total = meter.createCounter<IMetricAttributes>(`${metric_prefix}_request_total`);
const request_error_total = meter.createCounter<IMetricAttributes>(`${metric_prefix}_request_error_total`);
const request_duration_seconds_bucket = 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) => {
request_total.add(1, attributes);
request_duration_milliseconds_bucket.record(value, { unit: 'millisecond', ...attributes });
request_duration_seconds_bucket.record(value / 1000, { unit: 'second', ...attributes });
},
error: (attributes: IMetricAttributes) => {
request_error_total.add(1, attributes);
},
};

export const getContext = () =>
new Promise<string>((resolve, reject) => {
exporter
.collect()
.then((value) => {
resolve(serializer.serialize(value.resourceMetrics));
})
.finally(() => {
reject('');
});
});

Check warning on line 66 in lib/utils/otel/metric.ts

View check run for this annotation

Codecov / codecov/patch

lib/utils/otel/metric.ts#L57-L66

Added lines #L57 - L66 were not covered by tests
28 changes: 28 additions & 0 deletions lib/utils/otel/trace.ts
Original file line number Diff line number Diff line change
@@ -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');
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@
"dependencies": {
"@hono/node-server": "1.8.2",
"@notionhq/client": "2.2.14",
"@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",
Comment on lines +54 to +60
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@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",
"@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",

Please pin the dependency version.

"@postlight/parser": "2.2.3",
"@sentry/node": "7.105.0",
"@tonyrl/rand-user-agent": "2.0.53",
Expand Down
Loading