Skip to content

Commit

Permalink
feat(core): opentelementry support (update of DIYgod#14668) (DIYgod#1…
Browse files Browse the repository at this point in the history
…6087)

* chore: squash commit

* fix: tweak some mentioned issues

---------

Co-authored-by: incubator4 <[email protected]>
  • Loading branch information
2 people authored and wonktondI committed Jul 28, 2024
1 parent dd0bf0b commit f9e2419
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 1 deletion.
2 changes: 2 additions & 0 deletions lib/app.tsx
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 { jsxRenderer } from 'hono/jsx-renderer';
import { trimTrailingSlash } from 'hono/trailing-slash';

Expand All @@ -37,6 +38,7 @@ app.use(
})
);
app.use(mLogger);
app.use(trace);
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 @@ -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;
Expand Down Expand Up @@ -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({
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;
25 changes: 25 additions & 0 deletions lib/middleware/trace.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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({
Expand Down
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 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 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 @@ -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",
Expand Down
Loading

0 comments on commit f9e2419

Please sign in to comment.