Skip to content

TypeScript SDK: HTTP Middleware

The Aires TypeScript SDK provides patterns for automatic HTTP instrumentation. These middleware functions capture method, path, status code, duration, and request/response sizes for every HTTP request.

Elysia is the recommended HTTP framework for Bun. Create an Aires plugin:

import { aires } from "@aires/sdk"
import { Elysia } from "elysia"
import { randomUUID } from "crypto"

const airesPlugin = new Elysia({ name: "aires" })
  .derive(({ request }) => {
    const traceId = request.headers.get("x-trace-id") ?? randomUUID()
    const parentSpanId = request.headers.get("x-parent-span-id") ?? undefined
    const spanId = randomUUID()
    const start = performance.now()

    return {
      traceId,
      spanId,
      parentSpanId,
      requestStart: start,
    }
  })
  .onAfterResponse(({ request, traceId, spanId, parentSpanId, requestStart, set }) => {
    const durationMs = Math.round(performance.now() - requestStart)
    const url = new URL(request.url)
    const status = set.status ?? 200

    aires.info(`${request.method} ${url.pathname}`, {
      traceId,
      spanId,
      parentSpanId,
      category: "http",
      http: {
        method: request.method,
        path: url.pathname,
        status: typeof status === "number" ? status : 200,
        durationMs,
      },
      attr: {
        "http.user_agent": request.headers.get("user-agent") ?? "",
        "http.content_length": request.headers.get("content-length") ?? "0",
      },
      tags: status >= 400 ? ["error"] : [],
    })

    // Record latency metric
    aires.metric("http.request.duration_ms", durationMs, {
      attr: {
        method: request.method,
        path: url.pathname,
        status: String(status),
      },
    })
  })
  .onError(({ error, request, traceId, spanId }) => {
    const url = new URL(request.url)

    aires.error(`${request.method} ${url.pathname} failed`, {
      traceId,
      spanId,
      category: "http",
      error: {
        type: error.constructor.name,
        message: error.message,
        stack: error.stack ?? "",
        handled: false,
      },
    })
  })

// Usage
const app = new Elysia()
  .use(airesPlugin)
  .get("/api/health", () => ({ status: "ok" }))
  .post("/api/tasks", ({ body, traceId }) => {
    // traceId is available in all handlers
    aires.info("creating task", { traceId, category: "task" })
    return { id: "task-123" }
  })
  .listen(3000)

For Express (Node.js) applications:

import { aires } from "@aires/sdk"
import express from "express"
import { randomUUID } from "crypto"

function airesMiddleware() {
  return (req: express.Request, res: express.Response, next: express.NextFunction) => {
    const traceId = (req.headers["x-trace-id"] as string) ?? randomUUID()
    const parentSpanId = req.headers["x-parent-span-id"] as string | undefined
    const spanId = randomUUID()
    const start = performance.now()

    // Attach trace context to the request for downstream handlers
    req.traceId = traceId
    req.spanId = spanId

    // Capture when the response finishes
    res.on("finish", () => {
      const durationMs = Math.round(performance.now() - start)

      aires.info(`${req.method} ${req.path}`, {
        traceId,
        spanId,
        parentSpanId,
        category: "http",
        http: {
          method: req.method,
          path: req.path,
          status: res.statusCode,
          durationMs,
        },
        attr: {
          "http.user_agent": req.headers["user-agent"] ?? "",
          "http.remote_addr": req.ip ?? "",
        },
      })

      aires.metric("http.request.duration_ms", durationMs, {
        attr: {
          method: req.method,
          path: req.path,
          status: String(res.statusCode),
        },
      })
    })

    next()
  }
}

// Extend Express Request type
declare global {
  namespace Express {
    interface Request {
      traceId: string
      spanId: string
    }
  }
}

// Usage
const app = express()
app.use(airesMiddleware())

app.get("/api/health", (req, res) => {
  res.json({ status: "ok" })
})

app.post("/api/tasks", (req, res) => {
  aires.info("creating task", {
    traceId: req.traceId,
    category: "task",
  })
  res.status(201).json({ id: "task-123" })
})

app.listen(3000)

For bare Bun.serve without a framework:

import { aires } from "@aires/sdk"
import { randomUUID } from "crypto"

function withAires(
  handler: (req: Request, traceContext: { traceId: string; spanId: string }) => Response | Promise<Response>
) {
  return async (req: Request): Promise<Response> => {
    const traceId = req.headers.get("x-trace-id") ?? randomUUID()
    const spanId = randomUUID()
    const start = performance.now()
    const url = new URL(req.url)

    let status = 200
    let response: Response

    try {
      response = await handler(req, { traceId, spanId })
      status = response.status
    } catch (err: any) {
      status = 500
      aires.error(`${req.method} ${url.pathname} unhandled error`, {
        traceId,
        spanId,
        category: "http",
        error: {
          type: err.constructor.name,
          message: err.message,
          stack: err.stack ?? "",
          handled: false,
        },
      })
      response = new Response("Internal Server Error", { status: 500 })
    }

    const durationMs = Math.round(performance.now() - start)

    aires.info(`${req.method} ${url.pathname}`, {
      traceId,
      spanId,
      category: "http",
      http: {
        method: req.method,
        path: url.pathname,
        status,
        durationMs,
      },
    })

    aires.metric("http.request.duration_ms", durationMs, {
      attr: {
        method: req.method,
        path: url.pathname,
        status: String(status),
      },
    })

    return response
  }
}

// Usage
Bun.serve({
  port: 3000,
  fetch: withAires(async (req, { traceId, spanId }) => {
    const url = new URL(req.url)

    if (url.pathname === "/api/health") {
      return Response.json({ status: "ok" })
    }

    if (url.pathname === "/api/tasks" && req.method === "POST") {
      aires.info("creating task", { traceId, spanId, category: "task" })
      return Response.json({ id: "task-123" }, { status: 201 })
    }

    return new Response("Not Found", { status: 404 })
  }),
})

All middleware patterns capture:

FieldSourceClickHouse Column
HTTP methodreq.methodhttp_method
URL pathreq.url / req.pathhttp_path
Status coderes.statusCode / response.statushttp_status_code
Durationperformance.now() deltahttp_duration_ms
User agentUser-Agent headerattributes['http.user_agent']
Remote addressreq.ip / connection infoattributes['http.remote_addr']
SELECT
    http_path,
    http_method,
    count() AS requests,
    countIf(http_status_code >= 400) AS errors,
    round(errors / requests * 100, 2) AS error_rate
FROM events
WHERE category = 'http'
  AND timestamp > now() - INTERVAL 1 HOUR
GROUP BY http_path, http_method
ORDER BY requests DESC;
SELECT
    http_path,
    quantile(0.5)(http_duration_ms) AS p50,
    quantile(0.95)(http_duration_ms) AS p95,
    quantile(0.99)(http_duration_ms) AS p99,
    max(http_duration_ms) AS max_ms
FROM events
WHERE category = 'http'
  AND timestamp > now() - INTERVAL 1 HOUR
GROUP BY http_path
ORDER BY p95 DESC;
SELECT
    timestamp,
    http_method,
    http_path,
    http_status_code,
    http_duration_ms,
    trace_id
FROM events
WHERE category = 'http'
  AND http_duration_ms > 1000
  AND timestamp > now() - INTERVAL 1 HOUR
ORDER BY http_duration_ms DESC
LIMIT 20;