feat: Adding hono/prometheus middleware (#340)
* adding hono/prometheus middleware * adding tests for the metrics endpoint * adding changeset * adding workflow and a build script to main package.json * change to middleware factory instead * extending the readme with some examples * updating required hono version, adding installation section to readme * updating hono version in devdependenciespull/358/head
parent
64988bc7e8
commit
5c165793fc
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@hono/prometheus': major
|
||||||
|
---
|
||||||
|
|
||||||
|
Releasing first version
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: ci-prometheus
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/prometheus/**'
|
||||||
|
pull_request:
|
||||||
|
branches: ['*']
|
||||||
|
paths:
|
||||||
|
- 'packages/prometheus/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./packages/prometheus
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
- run: yarn install --frozen-lockfile
|
||||||
|
- run: yarn build
|
||||||
|
- run: yarn test
|
|
@ -28,6 +28,7 @@
|
||||||
"build:react-renderer": "yarn workspace @hono/react-renderer build",
|
"build:react-renderer": "yarn workspace @hono/react-renderer build",
|
||||||
"build:auth-js": "yarn workspace @hono/auth-js build",
|
"build:auth-js": "yarn workspace @hono/auth-js build",
|
||||||
"build:bun-transpiler": "yarn workspace @hono/bun-transpiler build",
|
"build:bun-transpiler": "yarn workspace @hono/bun-transpiler build",
|
||||||
|
"build:prometheus": "yarn workspace @hono/prometheus build",
|
||||||
"build": "run-p 'build:*'",
|
"build": "run-p 'build:*'",
|
||||||
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
||||||
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
# Prometheus middleware for Hono
|
||||||
|
|
||||||
|
This middleware adds basic [RED metrics](https://www.weave.works/blog/the-red-method-key-metrics-for-microservices-architecture/) to your Hono application, and exposes them on the `/metrics` endpoint for Prometheus to scrape.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This package depends on `prom-client`, so you need to install that as well:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -S @hono/prometheus prom-client
|
||||||
|
# or
|
||||||
|
yarn add @hono/prometheus prom-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { prometheus } from '@hono/prometheus'
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
const { printMetrics, registerMetrics } = prometheus()
|
||||||
|
|
||||||
|
app.use('*', registerMetrics)
|
||||||
|
app.get('/metrics', printMetrics)
|
||||||
|
app.get('/', (c) => c.text('foo'))
|
||||||
|
|
||||||
|
export default app
|
||||||
|
```
|
||||||
|
|
||||||
|
Making a GET request to `/metrics` returns the string representation of the metrics:
|
||||||
|
|
||||||
|
```
|
||||||
|
# HELP http_request_duration_seconds Duration of HTTP requests in seconds
|
||||||
|
# TYPE http_request_duration_seconds histogram
|
||||||
|
http_request_duration_seconds_bucket{le="0.005",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.01",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.025",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.05",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.075",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.1",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.25",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.5",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.75",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="1",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="2.5",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="5",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="7.5",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="10",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_sum{method="GET",route="/",status="200",ok="true"} 0.000251125
|
||||||
|
http_request_duration_seconds_count{method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_request_duration_seconds_bucket{le="0.005",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.01",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.025",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.05",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.075",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.1",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.25",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.5",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="0.75",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="1",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="2.5",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="5",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="7.5",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="10",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_bucket{le="+Inf",method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
http_request_duration_seconds_sum{method="GET",route="/user/:id",status="200",ok="true"} 0.000391333
|
||||||
|
http_request_duration_seconds_count{method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
|
||||||
|
# HELP http_requests_total Total number of HTTP requests
|
||||||
|
# TYPE http_requests_total counter
|
||||||
|
http_requests_total{method="GET",route="/",status="200",ok="true"} 2
|
||||||
|
http_requests_total{method="GET",route="/user/:id",status="200",ok="true"} 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
An options object can be passed in the `prometheus()` middleware factory to configure the metrics:
|
||||||
|
|
||||||
|
### `prefix`
|
||||||
|
|
||||||
|
Type: *string*
|
||||||
|
|
||||||
|
Prefix all metrics with this string.
|
||||||
|
|
||||||
|
### `registry`
|
||||||
|
|
||||||
|
Type: *[Registry](https://www.npmjs.com/package/prom-client)*
|
||||||
|
|
||||||
|
A prom-client Registry instance to store the metrics. If not provided, a new one will be created.
|
||||||
|
|
||||||
|
Useful when you want to register some custom metrics while exposing them on the same `/metrics` endpoint that this middleware creates. In this case, you can create a Registry instance, register your custom metrics on that, and pass that into this option.
|
||||||
|
|
||||||
|
### `collectDefaultMetrics`
|
||||||
|
|
||||||
|
Type: *boolean | [CollectDefaultMetricsOptions](https://www.npmjs.com/package/prom-client#default-metrics)*
|
||||||
|
|
||||||
|
There are some default metrics recommended by prom-client, like event loop delay, garbage collection statistics etc.
|
||||||
|
|
||||||
|
To enable these metrics, set this option to `true`. To configure the default metrics, pass an object with the [configuration options](https://www.npmjs.com/package/prom-client#default-metrics).
|
||||||
|
|
||||||
|
### `metricOptions`
|
||||||
|
|
||||||
|
Type: *object (see below)*
|
||||||
|
|
||||||
|
Modify the standard metrics (*requestDuration* and *requestsTotal*) with any of the [Counter](https://www.npmjs.com/package/prom-client#counter) / [Histogram](https://www.npmjs.com/package/prom-client#histogram) metric options, including:
|
||||||
|
|
||||||
|
#### `disabled`
|
||||||
|
|
||||||
|
Type: *boolean*
|
||||||
|
|
||||||
|
Disables the metric.
|
||||||
|
|
||||||
|
#### `customLabels`
|
||||||
|
|
||||||
|
Type: *Record<string, (context) => string>*
|
||||||
|
|
||||||
|
A record where the keys are the labels to add to the metrics, and the values are functions that receive the Hono context and return the value for that label. This is useful when adding labels to the metrics that are specific to your application or your needs. These functions are executed after all the other middlewares finished.
|
||||||
|
|
||||||
|
The following example adds a label to the *requestsTotal* metric with the `contentType` name where the value is the content type of the response:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
app.use('*', prometheus({
|
||||||
|
metricOptions: {
|
||||||
|
requestsTotal: {
|
||||||
|
customLabels: {
|
||||||
|
content_type: (c) => c.res.headers.get('content-type'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Adding custom metrics
|
||||||
|
|
||||||
|
If you want to expose custom metrics on the `/metrics` endpoint, you can create a [Registry](https://www.npmjs.com/package/prom-client#registry) instance and pass it to the `prometheus()` factory function using the `registry` property:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { prometheus } from '@hono/prometheus'
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { Counter, Registry } from 'prom-client'
|
||||||
|
|
||||||
|
const registry = new Registry()
|
||||||
|
const customCounter = new Counter({
|
||||||
|
name: 'custom_counter',
|
||||||
|
help: 'A custom counter',
|
||||||
|
registers: [registry],
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
const { printMetrics, registerMetrics } = prometheus({
|
||||||
|
registry,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('*', registerMetrics)
|
||||||
|
app.get('/metrics', printMetrics)
|
||||||
|
app.get('/', (c) => c.text('foo'))
|
||||||
|
|
||||||
|
export default app
|
||||||
|
|
||||||
|
// Somewhere in your application you can increment the custom counter:
|
||||||
|
customCounter.inc()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
David Dios <https://github.com/dios-david>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "@hono/prometheus",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Prometheus metrics middleware for Hono",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.mjs",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest --run",
|
||||||
|
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
||||||
|
"publint": "publint",
|
||||||
|
"release": "yarn build && yarn test && yarn publint && yarn publish"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.mts",
|
||||||
|
"import": "./dist/index.mjs",
|
||||||
|
"require": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://registry.npmjs.org",
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/honojs/middleware.git"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/honojs/middleware",
|
||||||
|
"peerDependencies": {
|
||||||
|
"hono": "^3.12.0",
|
||||||
|
"prom-client": "^15.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"hono": "^3.12.0",
|
||||||
|
"prom-client": "^15.0.0",
|
||||||
|
"tsup": "^8.0.1",
|
||||||
|
"vitest": "^1.0.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import type { Histogram} from 'prom-client'
|
||||||
|
import { Registry } from 'prom-client'
|
||||||
|
import { prometheus } from './index'
|
||||||
|
|
||||||
|
describe('Prometheus middleware', () => {
|
||||||
|
const app = new Hono()
|
||||||
|
const registry = new Registry()
|
||||||
|
|
||||||
|
app.use('*', prometheus({
|
||||||
|
registry,
|
||||||
|
}).registerMetrics)
|
||||||
|
|
||||||
|
app.get('/', (c) => c.text('hello'))
|
||||||
|
app.get('/user/:id', (c) => c.text(c.req.param('id')))
|
||||||
|
|
||||||
|
beforeEach(() => registry.resetMetrics())
|
||||||
|
|
||||||
|
describe('configuration', () => {
|
||||||
|
it('prefix - adds the provided prefix to the metric names', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
const registry = new Registry()
|
||||||
|
|
||||||
|
app.use('*', prometheus({
|
||||||
|
registry,
|
||||||
|
prefix: 'myprefix_',
|
||||||
|
}).registerMetrics)
|
||||||
|
|
||||||
|
expect(await registry.metrics()).toMatchInlineSnapshot(`
|
||||||
|
"# HELP myprefix_http_request_duration_seconds Duration of HTTP requests in seconds
|
||||||
|
# TYPE myprefix_http_request_duration_seconds histogram
|
||||||
|
|
||||||
|
# HELP myprefix_http_requests_total Total number of HTTP requests
|
||||||
|
# TYPE myprefix_http_requests_total counter
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('customLabels - adds custom labels to metrics', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
const registry = new Registry()
|
||||||
|
|
||||||
|
app.use('*', prometheus({
|
||||||
|
registry,
|
||||||
|
metricOptions: {
|
||||||
|
requestsTotal: {
|
||||||
|
customLabels: {
|
||||||
|
id: (c) => c.req.query('id') ?? 'unknown',
|
||||||
|
contentType: (c) => c.res.headers.get('content-type') ?? 'unknown',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}).registerMetrics)
|
||||||
|
|
||||||
|
app.get('/', (c) => c.text('hello'))
|
||||||
|
|
||||||
|
await app.request('http://localhost/?id=123')
|
||||||
|
|
||||||
|
expect(await registry.getSingleMetricAsString('http_requests_total')).toMatchInlineSnapshot(`
|
||||||
|
"# HELP http_requests_total Total number of HTTP requests
|
||||||
|
# TYPE http_requests_total counter
|
||||||
|
http_requests_total{method="GET",route="/",status="200",ok="true",id="123",contentType="text/plain;charset=UTF-8"} 1"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('metrics', () => {
|
||||||
|
describe('http_requests_total', () => {
|
||||||
|
it('increments the http_requests_total metric with the correct labels on successful responses', async () => {
|
||||||
|
await app.request('http://localhost/')
|
||||||
|
|
||||||
|
const { values } = await registry.getSingleMetric('http_requests_total')!.get()!
|
||||||
|
|
||||||
|
expect(values).toEqual([
|
||||||
|
{
|
||||||
|
labels: {
|
||||||
|
method: 'GET',
|
||||||
|
route: '/',
|
||||||
|
status: '200',
|
||||||
|
ok: 'true',
|
||||||
|
},
|
||||||
|
value: 1,
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('increments the http_requests_total metric with the correct labels on errors', async () => {
|
||||||
|
await app.request('http://localhost/notfound')
|
||||||
|
|
||||||
|
const { values } = await registry.getSingleMetric('http_requests_total')!.get()!
|
||||||
|
|
||||||
|
expect(values).toEqual([
|
||||||
|
{
|
||||||
|
labels: {
|
||||||
|
method: 'GET',
|
||||||
|
route: '/*',
|
||||||
|
status: '404',
|
||||||
|
ok: 'false',
|
||||||
|
},
|
||||||
|
value: 1,
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('http_requests_duration', () => {
|
||||||
|
it('updates the http_requests_duration metric with the correct labels on successful responses', async () => {
|
||||||
|
await app.request('http://localhost/')
|
||||||
|
|
||||||
|
const { values } = await (registry.getSingleMetric('http_request_duration_seconds') as Histogram)!.get()!
|
||||||
|
|
||||||
|
const countMetric = values.find(
|
||||||
|
(v) => v.metricName === 'http_request_duration_seconds_count' &&
|
||||||
|
v.labels.method === 'GET' &&
|
||||||
|
v.labels.route === '/' &&
|
||||||
|
v.labels.status === '200'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(countMetric?.value).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates the http_requests_duration metric with the correct labels on errors', async () => {
|
||||||
|
await app.request('http://localhost/notfound')
|
||||||
|
|
||||||
|
const { values } = await (registry.getSingleMetric('http_request_duration_seconds') as Histogram)!.get()!
|
||||||
|
|
||||||
|
const countMetric = values.find(
|
||||||
|
(v) => v.metricName === 'http_request_duration_seconds_count' &&
|
||||||
|
v.labels.method === 'GET' &&
|
||||||
|
v.labels.route === '/*' &&
|
||||||
|
v.labels.status === '404'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(countMetric?.value).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('metrics endpoint', () => {
|
||||||
|
it('returns the metrics in the prometheus string format on the /metrics endpoint', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
const registry = new Registry()
|
||||||
|
|
||||||
|
const { printMetrics, registerMetrics } = prometheus({
|
||||||
|
registry,
|
||||||
|
metricOptions: {
|
||||||
|
requestDuration: {
|
||||||
|
disabled: true, // Disable duration metrics to make the test result more predictable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('*', registerMetrics)
|
||||||
|
app.get('/', (c) => c.text('hello'))
|
||||||
|
app.get('/metrics', printMetrics)
|
||||||
|
|
||||||
|
await app.request('http://localhost/')
|
||||||
|
|
||||||
|
const response = await app.request('http://localhost/metrics')
|
||||||
|
|
||||||
|
expect(await response.text()).toMatchInlineSnapshot(`
|
||||||
|
"# HELP http_requests_total Total number of HTTP requests
|
||||||
|
# TYPE http_requests_total counter
|
||||||
|
http_requests_total{method="GET",route="/",status="200",ok="true"} 1
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,77 @@
|
||||||
|
import type { Context } from 'hono'
|
||||||
|
import { createMiddleware } from 'hono/factory'
|
||||||
|
import type { DefaultMetricsCollectorConfiguration, RegistryContentType } from 'prom-client'
|
||||||
|
import { Registry, collectDefaultMetrics as promCollectDefaultMetrics } from 'prom-client'
|
||||||
|
import { type MetricOptions, type CustomMetricsOptions, createStandardMetrics } from './standardMetrics'
|
||||||
|
|
||||||
|
interface PrometheusOptions {
|
||||||
|
registry?: Registry;
|
||||||
|
collectDefaultMetrics?: boolean | DefaultMetricsCollectorConfiguration<RegistryContentType>;
|
||||||
|
prefix?: string;
|
||||||
|
metricOptions?: Omit<CustomMetricsOptions, 'prefix' | 'register'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluateCustomLabels = (
|
||||||
|
customLabels: MetricOptions['customLabels'],
|
||||||
|
context: Context,
|
||||||
|
) => {
|
||||||
|
const labels: Record<string, string> = {}
|
||||||
|
|
||||||
|
for (const [key, fn] of Object.entries(customLabels ?? {})) {
|
||||||
|
labels[key] = fn(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prometheus = (options?: PrometheusOptions) => {
|
||||||
|
const {
|
||||||
|
registry = new Registry(),
|
||||||
|
collectDefaultMetrics = false,
|
||||||
|
prefix = '',
|
||||||
|
metricOptions,
|
||||||
|
} = options ?? {}
|
||||||
|
|
||||||
|
if (collectDefaultMetrics) {
|
||||||
|
promCollectDefaultMetrics({
|
||||||
|
prefix,
|
||||||
|
register: registry,
|
||||||
|
...(typeof collectDefaultMetrics === 'object' && collectDefaultMetrics)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = createStandardMetrics({
|
||||||
|
prefix,
|
||||||
|
registry,
|
||||||
|
customOptions: metricOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
printMetrics: async (c: Context) => c.text(await registry.metrics()),
|
||||||
|
registerMetrics: createMiddleware(async (c, next) => {
|
||||||
|
const timer = metrics.requestDuration?.startTimer()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await next()
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
const commonLabels = {
|
||||||
|
method: c.req.method,
|
||||||
|
route: c.req.routePath,
|
||||||
|
status: c.res.status.toString(),
|
||||||
|
ok: String(c.res.ok),
|
||||||
|
}
|
||||||
|
|
||||||
|
timer?.({
|
||||||
|
...commonLabels,
|
||||||
|
...evaluateCustomLabels(metricOptions?.requestDuration?.customLabels, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
metrics.requestsTotal?.inc({
|
||||||
|
...commonLabels,
|
||||||
|
...evaluateCustomLabels(metricOptions?.requestsTotal?.customLabels, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
import type { Context } from 'hono'
|
||||||
|
import type { CounterConfiguration, HistogramConfiguration, Metric } from 'prom-client'
|
||||||
|
import { Counter, Histogram, type Registry } from 'prom-client'
|
||||||
|
|
||||||
|
export type MetricOptions = {
|
||||||
|
disabled?: boolean;
|
||||||
|
customLabels?: Record<string, (c: Context) => string>;
|
||||||
|
} & (
|
||||||
|
({ type: 'counter' } & CounterConfiguration<string>) |
|
||||||
|
({ type: 'histogram' } & HistogramConfiguration<string>)
|
||||||
|
)
|
||||||
|
|
||||||
|
const standardMetrics = {
|
||||||
|
requestDuration: {
|
||||||
|
type: 'histogram',
|
||||||
|
name: 'http_request_duration_seconds',
|
||||||
|
help: 'Duration of HTTP requests in seconds',
|
||||||
|
labelNames: ['method', 'status', 'ok', 'route'],
|
||||||
|
// OpenTelemetry recommendation for histogram buckets of http request duration:
|
||||||
|
// https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration
|
||||||
|
buckets: [ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10 ],
|
||||||
|
},
|
||||||
|
requestsTotal: {
|
||||||
|
type: 'counter',
|
||||||
|
name: 'http_requests_total',
|
||||||
|
help: 'Total number of HTTP requests',
|
||||||
|
labelNames: ['method', 'status', 'ok', 'route'],
|
||||||
|
},
|
||||||
|
} satisfies Record<string, MetricOptions>
|
||||||
|
|
||||||
|
export type MetricName = keyof typeof standardMetrics
|
||||||
|
|
||||||
|
export type CustomMetricsOptions = {
|
||||||
|
[Name in MetricName]?: Partial<Omit<MetricOptions, 'type' | 'collect' | 'labelNames'>>
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatedMetrics = {
|
||||||
|
[Name in MetricName]: (typeof standardMetrics)[Name]['type'] extends 'counter' ? Counter<string> : Histogram<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMetricConstructor = (type: MetricOptions['type']) => ({
|
||||||
|
counter: Counter,
|
||||||
|
histogram: Histogram,
|
||||||
|
})[type]
|
||||||
|
|
||||||
|
export const createStandardMetrics = ({
|
||||||
|
registry,
|
||||||
|
prefix = '',
|
||||||
|
customOptions,
|
||||||
|
} : {
|
||||||
|
registry: Registry;
|
||||||
|
prefix?: string;
|
||||||
|
customOptions?: CustomMetricsOptions;
|
||||||
|
}) => {
|
||||||
|
const createdMetrics: Record<string, Metric> = {}
|
||||||
|
|
||||||
|
for (const [metric, options] of Object.entries(standardMetrics)) {
|
||||||
|
const opts: MetricOptions = {
|
||||||
|
...options,
|
||||||
|
...customOptions?.[metric as MetricName],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.disabled) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricConstructor = getMetricConstructor(opts.type)
|
||||||
|
|
||||||
|
createdMetrics[metric] = new MetricConstructor({
|
||||||
|
...(opts as object),
|
||||||
|
name: `${prefix}${opts.name}`,
|
||||||
|
help: opts.help,
|
||||||
|
registers: [...opts.registers ?? [], registry],
|
||||||
|
labelNames: [...opts.labelNames ?? [], ...Object.keys(opts.customLabels ?? {})],
|
||||||
|
...(opts.type === 'histogram' && opts.buckets && {
|
||||||
|
buckets: opts.buckets,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdMetrics as CreatedMetrics
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts"
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
})
|
49
yarn.lock
49
yarn.lock
|
@ -1536,6 +1536,20 @@ __metadata:
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@hono/prometheus@workspace:packages/prometheus":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@hono/prometheus@workspace:packages/prometheus"
|
||||||
|
dependencies:
|
||||||
|
hono: "npm:^3.12.0"
|
||||||
|
prom-client: "npm:^15.0.0"
|
||||||
|
tsup: "npm:^8.0.1"
|
||||||
|
vitest: "npm:^1.0.4"
|
||||||
|
peerDependencies:
|
||||||
|
hono: ^3.12.0
|
||||||
|
prom-client: ^15.0.0
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@hono/qwik-city@workspace:packages/qwik-city":
|
"@hono/qwik-city@workspace:packages/qwik-city":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@hono/qwik-city@workspace:packages/qwik-city"
|
resolution: "@hono/qwik-city@workspace:packages/qwik-city"
|
||||||
|
@ -2802,7 +2816,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@opentelemetry/api@npm:^1.6.0":
|
"@opentelemetry/api@npm:^1.4.0, @opentelemetry/api@npm:^1.6.0":
|
||||||
version: 1.7.0
|
version: 1.7.0
|
||||||
resolution: "@opentelemetry/api@npm:1.7.0"
|
resolution: "@opentelemetry/api@npm:1.7.0"
|
||||||
checksum: b5468115d1cec45dd2b86b39210fdc03620a93b9f07c3d20b14081f75e2f7c9b37ceceeb60d5f35c6d4f9819ae07eee0b4874e53e7362376db21db1e00f483f8
|
checksum: b5468115d1cec45dd2b86b39210fdc03620a93b9f07c3d20b14081f75e2f7c9b37ceceeb60d5f35c6d4f9819ae07eee0b4874e53e7362376db21db1e00f483f8
|
||||||
|
@ -4881,6 +4895,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"bintrees@npm:1.0.2":
|
||||||
|
version: 1.0.2
|
||||||
|
resolution: "bintrees@npm:1.0.2"
|
||||||
|
checksum: 132944b20c93c1a8f97bf8aa25980a76c6eb4291b7f2df2dbcd01cb5b417c287d3ee0847c7260c9f05f3d5a4233aaa03dec95114e97f308abe9cc3f72bed4a44
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"bl@npm:^4.0.3, bl@npm:^4.1.0":
|
"bl@npm:^4.0.3, bl@npm:^4.1.0":
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
resolution: "bl@npm:4.1.0"
|
resolution: "bl@npm:4.1.0"
|
||||||
|
@ -8851,6 +8872,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"hono@npm:^3.12.0":
|
||||||
|
version: 3.12.6
|
||||||
|
resolution: "hono@npm:3.12.6"
|
||||||
|
checksum: 74475dc0519f064a6c25b4d588a65ad06b9e3217c914e1d60aad9f3fc7516786dbda44e5bab527a6e6b8a10fdf30c37d25c2d20bab03681325d65edd6909a913
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"hosted-git-info@npm:^2.1.4":
|
"hosted-git-info@npm:^2.1.4":
|
||||||
version: 2.8.9
|
version: 2.8.9
|
||||||
resolution: "hosted-git-info@npm:2.8.9"
|
resolution: "hosted-git-info@npm:2.8.9"
|
||||||
|
@ -14180,6 +14208,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"prom-client@npm:^15.0.0":
|
||||||
|
version: 15.1.0
|
||||||
|
resolution: "prom-client@npm:15.1.0"
|
||||||
|
dependencies:
|
||||||
|
"@opentelemetry/api": "npm:^1.4.0"
|
||||||
|
tdigest: "npm:^0.1.1"
|
||||||
|
checksum: c10781adbf49225298e44da5396a51a0bd4d0cddc3c7e237ba50e888e12ead26a8f98261f362a442f1bbcdaddd6e7302d5675b37beac67ea9b6f82e4d39fb3cc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"promise-breaker@npm:^6.0.0":
|
"promise-breaker@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "promise-breaker@npm:6.0.0"
|
resolution: "promise-breaker@npm:6.0.0"
|
||||||
|
@ -16246,6 +16284,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tdigest@npm:^0.1.1":
|
||||||
|
version: 0.1.2
|
||||||
|
resolution: "tdigest@npm:0.1.2"
|
||||||
|
dependencies:
|
||||||
|
bintrees: "npm:1.0.2"
|
||||||
|
checksum: 10187b8144b112fcdfd3a5e4e9068efa42c990b1e30cd0d4f35ee8f58f16d1b41bc587e668fa7a6f6ca31308961cbd06cd5d4a4ae1dc388335902ae04f7d57df
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"term-size@npm:^2.1.0":
|
"term-size@npm:^2.1.0":
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
resolution: "term-size@npm:2.2.1"
|
resolution: "term-size@npm:2.2.1"
|
||||||
|
|
Loading…
Reference in New Issue