commit
8ee52ec554
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/otel': minor
|
||||
---
|
||||
|
||||
Initial OpenTelemetry support with Hono
|
|
@ -0,0 +1,25 @@
|
|||
name: ci-otel
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/otel/**'
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- 'packages/otel/**'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/otel
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
- run: yarn test
|
|
@ -42,6 +42,7 @@
|
|||
"build:tsyringe": "yarn workspace @hono/tsyringe build",
|
||||
"build:cloudflare-access": "yarn workspace @hono/cloudflare-access build",
|
||||
"build:standard-validator": "yarn workspace @hono/standard-validator build",
|
||||
"build:otel": "yarn workspace @hono/otel build",
|
||||
"build": "run-p 'build:*'",
|
||||
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
# OpenTelemetry middleware for Hono
|
||||
|
||||
This package provides a [Hono](https://hono.dev/) middleware that instruments your application with [OpenTelemetry](https://opentelemetry.io/).
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { otel } from '@hono/otel'
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node'
|
||||
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
traceExporter: new ConsoleSpanExporter(),
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', otel())
|
||||
app.get('/', (c) => c.text('foo'))
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
## Usage on Cloudflare Workers
|
||||
|
||||
Since @opentelemetry/sdk-node is not supported on [Cloudflare Workers](https://workers.cloudflare.com/), you need to use [@microlabs/otel-cf-workers](https://github.com/evanderkoogh/otel-cf-workers) instead.
|
||||
|
||||
The following example shows how to use @microlabs/otel-cf-workers with [Honeycomb](https://www.honeycomb.io/):
|
||||
|
||||
```ts
|
||||
import { otel } from '@hono/otel'
|
||||
import { instrument, ResolveConfigFn } from '@microlabs/otel-cf-workers'
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', otel())
|
||||
app.get('/', (c) => c.text('foo'))
|
||||
|
||||
const config: ResolveConfigFn = (env: Env, _trigger) => {
|
||||
return {
|
||||
exporter: {
|
||||
url: 'https://api.honeycomb.io/v1/traces',
|
||||
headers: { 'x-honeycomb-team': env.HONEYCOMB_API_KEY },
|
||||
},
|
||||
service: { name: 'greetings' },
|
||||
}
|
||||
}
|
||||
|
||||
export default instrument(app, config)
|
||||
```
|
||||
|
||||
## Limitation
|
||||
|
||||
Since this instrumentation is based on Hono's middleware system, it instruments the entire request-response lifecycle. This means that it doesn't provide fine-grained instrumentation for individual middleware.
|
||||
|
||||
## Author
|
||||
|
||||
Hong Minhee <https://hongminhee.org/>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "@hono/otel",
|
||||
"version": "0.0.0",
|
||||
"description": "OpenTelemetry middleware for Hono",
|
||||
"type": "module",
|
||||
"module": "dist/index.js",
|
||||
"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": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
||||
"hono": "^4.4.12",
|
||||
"tsup": "^8.1.0",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { SpanKind, SpanStatusCode } from '@opentelemetry/api'
|
||||
import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
||||
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
|
||||
import {
|
||||
ATTR_HTTP_REQUEST_METHOD,
|
||||
ATTR_HTTP_RESPONSE_HEADER,
|
||||
ATTR_HTTP_RESPONSE_STATUS_CODE,
|
||||
ATTR_URL_FULL,
|
||||
ATTR_HTTP_ROUTE,
|
||||
} from '@opentelemetry/semantic-conventions'
|
||||
import { Hono } from 'hono'
|
||||
import { otel } from '../src'
|
||||
|
||||
describe('OpenTelemetry middleware', () => {
|
||||
const app = new Hono()
|
||||
|
||||
const memoryExporter = new InMemorySpanExporter()
|
||||
const spanProcessor = new SimpleSpanProcessor(memoryExporter)
|
||||
const tracerProvider = new NodeTracerProvider({
|
||||
spanProcessors: [spanProcessor],
|
||||
})
|
||||
|
||||
app.use(otel({ tracerProvider }))
|
||||
app.get('/foo', (c) => c.text('foo'))
|
||||
app.post('/error', (_) => {
|
||||
throw new Error('error message')
|
||||
})
|
||||
|
||||
it('Should make a span', async () => {
|
||||
memoryExporter.reset()
|
||||
const response = await app.request('http://localhost/foo')
|
||||
const spans = memoryExporter.getFinishedSpans()
|
||||
expect(spans.length).toBe(1)
|
||||
const [span] = spans
|
||||
expect(span.name).toBe('GET /foo')
|
||||
expect(span.kind).toBe(SpanKind.SERVER)
|
||||
expect(span.status.code).toBe(SpanStatusCode.UNSET)
|
||||
expect(span.status.message).toBeUndefined()
|
||||
expect(span.attributes[ATTR_HTTP_REQUEST_METHOD]).toBe('GET')
|
||||
expect(span.attributes[ATTR_URL_FULL]).toBe('http://localhost/foo')
|
||||
expect(span.attributes[ATTR_HTTP_ROUTE]).toBe('/foo')
|
||||
expect(span.attributes[ATTR_HTTP_RESPONSE_STATUS_CODE]).toBe(200)
|
||||
for (const [name, value] of response.headers.entries()) {
|
||||
expect(span.attributes[ATTR_HTTP_RESPONSE_HEADER(name)]).toBe(value)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should make a span with error', async () => {
|
||||
memoryExporter.reset()
|
||||
await app.request('http://localhost/error', { method: 'POST' })
|
||||
const spans = memoryExporter.getFinishedSpans()
|
||||
expect(spans.length).toBe(1)
|
||||
const [span] = spans
|
||||
expect(span.name).toBe('POST /error')
|
||||
expect(span.kind).toBe(SpanKind.SERVER)
|
||||
expect(span.status.code).toBe(SpanStatusCode.ERROR)
|
||||
expect(span.status.message).toBe('Error: error message')
|
||||
expect(span.attributes[ATTR_HTTP_REQUEST_METHOD]).toBe('POST')
|
||||
expect(span.attributes[ATTR_URL_FULL]).toBe('http://localhost/error')
|
||||
expect(span.attributes[ATTR_HTTP_ROUTE]).toBe('/error')
|
||||
})
|
||||
|
||||
it('Should update the active span', async () => {
|
||||
memoryExporter.reset()
|
||||
await tracerProvider.getTracer('test').startActiveSpan('existing span', async () => {
|
||||
await app.request('http://localhost/foo')
|
||||
})
|
||||
const spans = memoryExporter.getFinishedSpans()
|
||||
expect(spans.length).toBe(1)
|
||||
const [span] = spans
|
||||
expect(span.name).toBe('GET /foo')
|
||||
expect(span.attributes[ATTR_HTTP_ROUTE]).toBe('/foo')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,75 @@
|
|||
import { SpanKind, SpanStatusCode, type TracerProvider, trace } from '@opentelemetry/api'
|
||||
import {
|
||||
ATTR_HTTP_REQUEST_HEADER,
|
||||
ATTR_HTTP_REQUEST_METHOD,
|
||||
ATTR_HTTP_RESPONSE_HEADER,
|
||||
ATTR_HTTP_RESPONSE_STATUS_CODE,
|
||||
ATTR_URL_FULL,
|
||||
ATTR_HTTP_ROUTE,
|
||||
} from '@opentelemetry/semantic-conventions'
|
||||
import type { Env, Input } from 'hono'
|
||||
import { createMiddleware } from 'hono/factory'
|
||||
import metadata from '../package.json' with { type: 'json'}
|
||||
|
||||
const PACKAGE_NAME = metadata.name
|
||||
const PACKAGE_VERSION = metadata.version
|
||||
|
||||
export type OtelOptions = {
|
||||
augmentSpan?: false;
|
||||
tracerProvider?: TracerProvider
|
||||
} | {
|
||||
augmentSpan: true;
|
||||
}
|
||||
|
||||
export const otel = <E extends Env = any, P extends string = any, I extends Input = {}>(
|
||||
options: OtelOptions = {}
|
||||
) => {
|
||||
if (options.augmentSpan) {
|
||||
return createMiddleware<E, P, I>(async (c, next) => {
|
||||
const result = await next()
|
||||
const span = trace.getActiveSpan()
|
||||
if (span != null) {
|
||||
const route = c.req.matchedRoutes[c.req.matchedRoutes.length - 1]
|
||||
span.setAttribute(ATTR_HTTP_ROUTE, route.path)
|
||||
span.updateName(`${c.req.method} ${route.path}`)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider()
|
||||
const tracer = tracerProvider.getTracer(PACKAGE_NAME, PACKAGE_VERSION)
|
||||
return createMiddleware<E, P, I>(async (c, next) => {
|
||||
const route = c.req.matchedRoutes[c.req.matchedRoutes.length - 1]
|
||||
await tracer.startActiveSpan(
|
||||
`${c.req.method} ${route.path}`,
|
||||
{
|
||||
kind: SpanKind.SERVER,
|
||||
attributes: {
|
||||
[ATTR_HTTP_REQUEST_METHOD]: c.req.method,
|
||||
[ATTR_URL_FULL]: c.req.url,
|
||||
[ATTR_HTTP_ROUTE]: route.path,
|
||||
},
|
||||
},
|
||||
async (span) => {
|
||||
for (const [name, value] of Object.entries(c.req.header())) {
|
||||
span.setAttribute(ATTR_HTTP_REQUEST_HEADER(name), value)
|
||||
}
|
||||
try {
|
||||
await next()
|
||||
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, c.res.status)
|
||||
for (const [name, value] of c.res.headers.entries()) {
|
||||
span.setAttribute(ATTR_HTTP_RESPONSE_HEADER(name), value)
|
||||
}
|
||||
if (c.error) {
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: String(c.error) })
|
||||
}
|
||||
} catch (e) {
|
||||
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) })
|
||||
throw e
|
||||
} finally {
|
||||
span.end()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
},
|
||||
})
|
108
yarn.lock
108
yarn.lock
|
@ -2677,6 +2677,22 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/otel@workspace:packages/otel":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/otel@workspace:packages/otel"
|
||||
dependencies:
|
||||
"@opentelemetry/api": "npm:^1.9.0"
|
||||
"@opentelemetry/sdk-trace-base": "npm:^1.30.0"
|
||||
"@opentelemetry/sdk-trace-node": "npm:^1.30.0"
|
||||
"@opentelemetry/semantic-conventions": "npm:^1.28.0"
|
||||
hono: "npm:^4.4.12"
|
||||
tsup: "npm:^8.1.0"
|
||||
vitest: "npm:^1.6.0"
|
||||
peerDependencies:
|
||||
hono: "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@hono/prometheus@workspace:packages/prometheus":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@hono/prometheus@workspace:packages/prometheus"
|
||||
|
@ -3465,13 +3481,103 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/api@npm:~1.9.0":
|
||||
"@opentelemetry/api@npm:^1.9.0, @opentelemetry/api@npm:~1.9.0":
|
||||
version: 1.9.0
|
||||
resolution: "@opentelemetry/api@npm:1.9.0"
|
||||
checksum: 9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/context-async-hooks@npm:1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/context-async-hooks@npm:1.30.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 46fef8f3af37227c16cf4e3d9264bfc7cfbe7357cb4266fa10ef32aa3256da6782110bea997d7a6b6815afb540da0a937fb5ecbaaed248c0234f8872bf25e8df
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/core@npm:1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/core@npm:1.30.0"
|
||||
dependencies:
|
||||
"@opentelemetry/semantic-conventions": "npm:1.28.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 52d17b5ddb06ab4241b977ff89b81f69f140edb5c2a78b2188d95fa7bdfdd1aa2dcafb1e2830ab77d557876682ab8f08727ba8f165ea3c39fbb6bf3b86ef33c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/propagator-b3@npm:1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/propagator-b3@npm:1.30.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:1.30.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 2378d9527247982ad09c08f51b90364913640a72519df3b65fbd694a666f4e13ce035b3a42d3651f5d707e85b3f48b7837e4aa50fbbfe3fcb8f6af47e0af5c34
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/propagator-jaeger@npm:1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/propagator-jaeger@npm:1.30.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:1.30.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: a2cd68d3ca08ba84b62427d363f7054a8d51922805376987d67bbf7d61513cde9665a4f5df262f46ed2affae0557d3bc13b0ec3aa68f84088f092f007849f781
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/resources@npm:1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/resources@npm:1.30.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:1.30.0"
|
||||
"@opentelemetry/semantic-conventions": "npm:1.28.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 2b298193de85f8d7d05f9d71e5ea63189668f99248486246a4cfdc8667a5face205d650ef1ee6204a9f9c16d0b0e7704bb89a5d47537279c8e3378231ed35d1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-trace-base@npm:1.30.0, @opentelemetry/sdk-trace-base@npm:^1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/sdk-trace-base@npm:1.30.0"
|
||||
dependencies:
|
||||
"@opentelemetry/core": "npm:1.30.0"
|
||||
"@opentelemetry/resources": "npm:1.30.0"
|
||||
"@opentelemetry/semantic-conventions": "npm:1.28.0"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 3d8dcb0ec4e70405593421ea4df8b9a5e7faceea16cb900f30747eaeaa1f96059d40312ff2171208bb627deab6a6f32024681128cfba45af2671c6cfba528af1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/sdk-trace-node@npm:^1.30.0":
|
||||
version: 1.30.0
|
||||
resolution: "@opentelemetry/sdk-trace-node@npm:1.30.0"
|
||||
dependencies:
|
||||
"@opentelemetry/context-async-hooks": "npm:1.30.0"
|
||||
"@opentelemetry/core": "npm:1.30.0"
|
||||
"@opentelemetry/propagator-b3": "npm:1.30.0"
|
||||
"@opentelemetry/propagator-jaeger": "npm:1.30.0"
|
||||
"@opentelemetry/sdk-trace-base": "npm:1.30.0"
|
||||
semver: "npm:^7.5.2"
|
||||
peerDependencies:
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
checksum: 284b314c8c5b6da6e7e2b6c6814d6ef7cdfeeb3bce211bc1c38dc1e4b092f811727040265a75b5f6b67c287429cbd23661210b253429370918cb111bef1b57ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/semantic-conventions@npm:1.28.0, @opentelemetry/semantic-conventions@npm:^1.28.0":
|
||||
version: 1.28.0
|
||||
resolution: "@opentelemetry/semantic-conventions@npm:1.28.0"
|
||||
checksum: deb8a0f744198071e70fea27143cf7c9f7ecb7e4d7b619488c917834ea09b31543c1c2bcea4ec5f3cf68797f0ef3549609c14e859013d9376400ac1499c2b9cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@opentelemetry/semantic-conventions@npm:~1.26.0":
|
||||
version: 1.26.0
|
||||
resolution: "@opentelemetry/semantic-conventions@npm:1.26.0"
|
||||
|
|
Loading…
Reference in New Issue