---
title: "OTLP 适配器"
description: "通过 OpenTelemetry 协议（OTLP）向 Grafana、Datadog、Honeycomb 以及任何兼容的后端发送日志。支持 gRPC 和 HTTP 传输。"
canonical_url: "https://evlog.zhcndoc.com/integrate/adapters/hybrid/otlp"
---
# OTLP 适配器

> 通过 OpenTelemetry 协议（OTLP）向 Grafana、Datadog、Honeycomb 以及任何兼容的后端发送日志。支持 gRPC 和 HTTP 传输。

OTLP（OpenTelemetry 协议）适配器以标准的 OpenTelemetry 格式发送日志。这适用于任何兼容 OTLP 的后端，包括：

- **Grafana Cloud** (Loki)
- **Datadog**
- **Honeycomb**
- **Jaeger**
- **Splunk**
- **New Relic**
- **自建 OpenTelemetry 收集器**
- **HyperDX**

<prompt :actions="["copy","cursor","claude"]" description="添加 OTLP 排水适配器" icon="i-simple-icons-opentelemetry">

添加 OTLP 排水适配器，通过 OpenTelemetry 协议发送 evlog 广播级事件。

1. 确定我正在使用的框架并遵循其 evlog 集成模式
2. 如果尚未安装 evlog，请安装它
3. 从 'evlog/otlp' 导入 createOTLPDrain
4. 将 createOTLPDrain() 连接到我的框架的排水配置中
5. 设置 OTLP_ENDPOINT 环境变量（收集器 URL）
6. 可选设置 OTLP_HEADERS 用于身份验证
7. 通过触发请求进行测试，并检查您的 OTLP 后端（Grafana、Datadog、Honeycomb 等）

适配器文档：[https://www.evlog.dev/integrate/adapters/hybrid/otlp](https://www.evlog.dev/integrate/adapters/hybrid/otlp)
框架设置：[https://www.evlog.dev/integrate/frameworks/overview](https://www.evlog.dev/integrate/frameworks/overview)

</prompt>

## 安装

OTLP 适配器与 evlog 一起打包提供：

```typescript [src/index.ts]
import { createOTLPDrain } from 'evlog/otlp'
```

## 快速开始

### 1. 设置您的 OTLP 端点

```bash [.env]
OTLP_ENDPOINT=http://localhost:4318
```

### 2. 将排水连接到您的框架

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createOTLPDrain } from 'evlog/otlp'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createOTLPDrain } from 'evlog/otlp'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createOTLPDrain } from 'evlog/otlp'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createOTLPDrain(),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createOTLPDrain } from 'evlog/otlp'

export const { handle, handleError } = createEvlogHooks({ drain: createOTLPDrain() })
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createOTLPDrain } from 'evlog/otlp'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createOTLPDrain } from 'evlog/otlp'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ drain: createOTLPDrain() }),
]
```

```typescript [Hono]src/index.ts
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({ drain: createOTLPDrain() }))
```

```typescript [Express]src/index.ts
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({ drain: createOTLPDrain() }))
```

```typescript [Fastify]src/index.ts
import { createOTLPDrain } from 'evlog/otlp'

await app.register(evlog, { drain: createOTLPDrain() })
```

```typescript [Elysia]src/index.ts
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({ drain: createOTLPDrain() }))
```

```typescript [NestJS]src/app.module.ts
import { createOTLPDrain } from 'evlog/otlp'

EvlogModule.forRoot({ drain: createOTLPDrain() })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createOTLPDrain } from 'evlog/otlp'

const handler = withEvlog(new RPCHandler(router), { drain: createOTLPDrain() })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createOTLPDrain } from 'evlog/otlp'

export default withEvlog(handler, { drain: createOTLPDrain() })
```

```typescript [Standalone]
import { createOTLPDrain } from 'evlog/otlp'

initLogger({ drain: createOTLPDrain() })
```

</framework-tabs>

## 配置

适配器按优先级从多个来源读取配置（优先级从高到低）：

1. **传递给 createOTLPDrain() 的覆盖项**
2. **运行时配置** `runtimeConfig.otlp`（仅限 Nuxt/Nitro）
3. **环境变量**

### 环境变量

<table>
<thead>
  <tr>
    <th>
      变量
    </th>
    
    <th>
      说明
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        OTLP_ENDPOINT
      </code>
    </td>
    
    <td>
      OTLP HTTP 端点（例如，<code>
        http://localhost:4318
      </code>
      
      ）。标准的 <code>
        OTEL_EXPORTER_OTLP_ENDPOINT
      </code>
      
       也可用。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTLP_HEADERS
      </code>
    </td>
    
    <td>
      以 <code>
        key=value
      </code>
      
       键值对形式提供的请求头，使用逗号分隔。标准的 <code>
        OTEL_EXPORTER_OTLP_HEADERS
      </code>
      
       也可用。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
      </code>
    </td>
    
    <td>
      完整的日志 URL，按原样使用，不会追加 <code>
        /v1/logs
      </code>
      
      。优先于 <code>
        OTEL_EXPORTER_OTLP_ENDPOINT
      </code>
      
      。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTEL_EXPORTER_OTLP_LOGS_HEADERS
      </code>
    </td>
    
    <td>
      日志信号的请求头。合并到 <code>
        OTEL_EXPORTER_OTLP_HEADERS
      </code>
      
       之上，冲突时以前者为准。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTEL_EXPORTER_OTLP_COMPRESSION
      </code>
    </td>
    
    <td>
      <code>
        gzip
      </code>
      
       或 <code>
        none
      </code>
      
      。<code>
        OTEL_EXPORTER_OTLP_LOGS_COMPRESSION
      </code>
      
       优先。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTEL_EXPORTER_OTLP_PROTOCOL
      </code>
    </td>
    
    <td>
      <code>
        http/json
      </code>
      
       或 <code>
        http/protobuf
      </code>
      
      。<code>
        OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
      </code>
      
       优先。不支持 <code>
        grpc
      </code>
      
      。
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTEL_SERVICE_NAME
      </code>
    </td>
    
    <td>
      服务名称覆盖项
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OTEL_RESOURCE_ATTRIBUTES
      </code>
    </td>
    
    <td>
      以逗号分隔的 <code>
        key=value
      </code>
      
       资源属性。<code>
        resourceAttributes
      </code>
      
       以及事件的 <code>
        service
      </code>
      
      、<code>
        environment
      </code>
      
      、<code>
        version
      </code>
      
      、<code>
        region
      </code>
      
       和 <code>
        commitHash
      </code>
      
       优先。
    </td>
  </tr>
</tbody>
</table>

这些变量遵循 [OpenTelemetry 导出器规范](https://opentelemetry.io/docs/specs/otel/protocol/exporter/)，因此已经通过环境变量配置 OTel SDK 的部署无需额外设置 evlog 专用变量。

### 运行时配置（仅限 Nuxt）

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  runtimeConfig: {
    otlp: {
      endpoint: '', // 通过 OTLP_ENDPOINT（或 OTEL_EXPORTER_OTLP_ENDPOINT）设置
    },
  },
})
```

### 覆盖选项

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createOTLPDrain } from 'evlog/otlp'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }))
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createOTLPDrain } from 'evlog/otlp'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }))
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createOTLPDrain } from 'evlog/otlp'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createOTLPDrain } from 'evlog/otlp'

export const { handle, handleError } = createEvlogHooks({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createOTLPDrain } from 'evlog/otlp'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }))
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createOTLPDrain } from 'evlog/otlp'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createOTLPDrain({
        endpoint: 'http://localhost:4318',
        serviceName: 'my-api',
        headers: {
          'Authorization': 'Bearer xxx',
        },
        resourceAttributes: {
          'deployment.environment': 'staging',
        },
      }),
    }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
}))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
}))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createOTLPDrain } from 'evlog/otlp'

await app.register(evlog, {
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
}))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createOTLPDrain } from 'evlog/otlp'

EvlogModule.forRoot({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createOTLPDrain } from 'evlog/otlp'

const handler = withEvlog(new RPCHandler(router), {
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createOTLPDrain } from 'evlog/otlp'

export default withEvlog(handler, {
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createOTLPDrain } from 'evlog/otlp'

initLogger({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    serviceName: 'my-api',
    headers: {
      'Authorization': 'Bearer xxx',
    },
    resourceAttributes: {
      'deployment.environment': 'staging',
    },
  }),
})
```

</framework-tabs>

### 完整配置参考

<table>
<thead>
  <tr>
    <th>
      选项
    </th>
    
    <th>
      类型
    </th>
    
    <th>
      默认值
    </th>
    
    <th>
      描述
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        endpoint
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      -
    </td>
    
    <td>
      OTLP HTTP 端点（必填）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        serviceName
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      来自事件
    </td>
    
    <td>
      覆盖 <code>
        service.name
      </code>
      
       资源属性
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        headers
      </code>
    </td>
    
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      -
    </td>
    
    <td>
      用于身份验证的自定义 HTTP 请求头
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        resourceAttributes
      </code>
    </td>
    
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      -
    </td>
    
    <td>
      其他 OTLP 资源属性
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        recordShape
      </code>
    </td>
    
    <td>
      <code>
        'json' | 'compact'
      </code>
    </td>
    
    <td>
      <code>
        'json'
      </code>
    </td>
    
    <td>
      记录承载事件的方式（<a href="#record-shape">
        详情
      </a>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        compression
      </code>
    </td>
    
    <td>
      <code>
        'gzip' | 'none'
      </code>
    </td>
    
    <td>
      <code>
        'none'
      </code>
    </td>
    
    <td>
      使用 Gzip 压缩请求正文，并发送 <code>
        Content-Encoding: gzip
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        protocol
      </code>
    </td>
    
    <td>
      <code>
        'http/json' | 'http/protobuf'
      </code>
    </td>
    
    <td>
      <code>
        'http/json'
      </code>
    </td>
    
    <td>
      请求正文编码（<a href="#protobuf">
        详情
      </a>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        semanticConventions
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      在 evlog 字段旁添加 OpenTelemetry 属性名称（<a href="#semantic-conventions">
        详情
      </a>
      
      ）
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timeout
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        5000
      </code>
    </td>
    
    <td>
      请求超时时间（毫秒）
    </td>
  </tr>
</tbody>
</table>

## 部署

OTLP 是一种协议，而不是产品。相同的适配器既可以与您自行运行的收集器通信，也可以与托管网关通信。只有端点和请求头会发生变化。

### 自托管

运行一个 OpenTelemetry Collector，并让 evlog 指向它。无需其他配置：

```yaml [otel-collector.yaml]
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [debug]
```

```bash [终端]
docker run --rm -p 4318:4318 \
  -v $(pwd)/otel-collector.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector:latest
```

```bash [.env]
OTLP_ENDPOINT=http://localhost:4318
```

从这里开始，收集器会将数据分发到您想要的任何位置：Loki、ClickHouse、Elasticsearch、托管后端，或同时分发到多个位置。这种间接层正是选择 OTLP 而不是直接适配器的原因。

### 托管网关

相同的适配器，加上需要凭据的端点：

<code-group>

```bash [Grafana Cloud]
OTLP_ENDPOINT=https://otlp-gateway-prod-us-central-0.grafana.net/otlp
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20base64-encoded-credentials
```

```bash [Datadog]
OTLP_ENDPOINT=https://http-intake.logs.datadoghq.com
OTLP_HEADERS=DD-API-KEY=your-api-key
```

```bash [Honeycomb]
OTLP_ENDPOINT=https://api.honeycomb.io
OTLP_HEADERS=x-honeycomb-team=your-api-key
```

</code-group>

<callout color="info" icon="i-lucide-info">

Grafana Cloud 使用经过 URL 编码的请求头，因此 `%20` 表示空格。适配器会自动解码这种格式。

</callout>

## OTLP 日志格式

evlog 将广泛事件映射到 OTLP 日志格式：

<table>
<thead>
  <tr>
    <th>
      evlog 字段
    </th>
    
    <th>
      OTLP 字段
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        level
      </code>
    </td>
    
    <td>
      <code>
        severityNumber
      </code>
      
       / <code>
        severityText
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timestamp
      </code>
    </td>
    
    <td>
      <code>
        timeUnixNano
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        service
      </code>
    </td>
    
    <td>
      资源属性 <code>
        service.name
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        environment
      </code>
    </td>
    
    <td>
      资源属性 <code>
        deployment.environment
      </code>
      
      ；启用 <code>
        semanticConventions
      </code>
      
       时还包括 <code>
        deployment.environment.name
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        version
      </code>
    </td>
    
    <td>
      资源属性 <code>
        service.version
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        region
      </code>
    </td>
    
    <td>
      资源属性 <code>
        cloud.region
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        traceId
      </code>
    </td>
    
    <td>
      <code>
        traceId
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        spanId
      </code>
    </td>
    
    <td>
      <code>
        spanId
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      所有其他字段
    </td>
    
    <td>
      日志属性
    </td>
  </tr>
</tbody>
</table>

`spanId` 是生成该事件的请求所属的 span。[TraceContext 丰富化器](https://evlog.zhcndoc.com/use-cases/enrichers#tracecontextenricher) 会将传入 `traceparent` 中调用方的 span 记录为 `parentSpanId`，并将其作为日志属性发送。因此，请使用追踪器的活动 span 设置 `spanId`，以便将记录关联到服务器 span。

属性值会保留其类型：整数作为 `intValue` 发送，其他有限数字作为 `doubleValue` 发送，布尔值作为 `boolValue` 发送，元素共享一种基本类型的数组作为 `arrayValue` 发送。在 `json` 形状中，嵌套的普通对象作为 `kvlistValue` 发送。其他值会序列化为 `stringValue`。无效 W3C 十六进制格式或全为零的 `traceId` 或 `spanId` 会保留为普通属性。

宽事件中的每个字段都会作为 OTLP 记录字段、资源属性或日志属性发送。`null` 和 `undefined` 会被省略，而不会传输。两种形状都会在每一层删除它们，因此嵌套的 `{ user: { id: null } }` 在 `compact` 中没有 `user.id` 属性，在 `json` 的键值列表中也没有 `id` 条目。

### 记录形状

`recordShape` 控制记录承载事件的方式。默认值为 `json`。

<code-group>

```json [json (默认)]
{
  "body": { "stringValue": "{\"timestamp\":\"…\",\"method\":\"POST\",\"user\":{\"id\":\"usr_123\"}}" },
  "attributes": [
    { "key": "method", "value": { "stringValue": "POST" } },
    { "key": "user", "value": { "kvlistValue": { "values": [
      { "key": "id", "value": { "stringValue": "usr_123" } },
      { "key": "plan", "value": { "stringValue": "premium" } }
    ] } } }
  ]
}
```

```json [compact]
{
  "body": { "stringValue": "POST /api/checkout (500)" },
  "attributes": [
    { "key": "method", "value": { "stringValue": "POST" } },
    { "key": "user.id", "value": { "stringValue": "usr_123" } },
    { "key": "user.plan", "value": { "stringValue": "premium" } }
  ]
}
```

</code-group>

当你的后端按摄取量收费或对属性建立分面时，值得切换到 `compact`：

- **正文是一行摘要**：`POST /api/checkout (500)`，如果无法生成则回退到服务名称，而不是在属性旁边重复整个事件。将消息聚类成模板的后端只有在正文稳定时才能做到这一点。
- **嵌套字段会变成点号分隔的属性**，因此每个叶字段都是自己的分面：```typescript [server/api/checkout.post.ts]
const drain = createOTLPDrain({ recordShape: 'compact' })

log.set({ user: { id: 'usr_123', plan: 'premium' } })
// → user.id, user.plan
```

只有普通对象会被遍历。数组会作为单个属性发送：当其元素共享一种基本类型时使用 `arrayValue`，否则使用 JSON 字符串。对数组建立索引，例如 `ai.tools.0.name`，会将列表变成一组无界的不同属性键，而大多数后端都会对此收费，并且没有任何后端可以将其制成图表。其他非普通对象也是如此，例如 `Date`。空对象会保留为单个 `{}` 属性，而不是消失。

<callout color="info" icon="i-lucide-info">

下一个 major 版本中，`compact` 将成为默认值。如果你现在正在设置项目，请尽早切换。之后再迁移意味着需要重写基于 `json` 形状构建的查询。

</callout>

### 语义约定

启用 `semanticConventions: true` 后，每条记录还会为 evlog 已知字段携带 [OpenTelemetry 语义约定](https://opentelemetry.io/docs/specs/semconv/)中的名称，因此内置 HTTP、错误和 GenAI 视图的后端可以识别它们。evlog 名称会保留，因此现有查询仍可继续使用。

<table>
<thead>
  <tr>
    <th>
      evlog 字段
    </th>
    
    <th>
      属性
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        method
      </code>
    </td>
    
    <td>
      <code>
        http.request.method
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        path
      </code>
    </td>
    
    <td>
      <code>
        url.path
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status
      </code>
    </td>
    
    <td>
      <code>
        http.response.status_code
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        userAgent.raw
      </code>
    </td>
    
    <td>
      <code>
        user_agent.original
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        error.name
      </code>
      
       / <code>
        error.message
      </code>
      
       / <code>
        error.stack
      </code>
    </td>
    
    <td>
      <code>
        exception.type
      </code>
      
       / <code>
        exception.message
      </code>
      
       / <code>
        exception.stacktrace
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.model
      </code>
      
       / <code>
        ai.provider
      </code>
      
       / <code>
        ai.responseId
      </code>
    </td>
    
    <td>
      <code>
        gen_ai.request.model
      </code>
      
       / <code>
        gen_ai.provider.name
      </code>
      
       / <code>
        gen_ai.response.id
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.inputTokens
      </code>
      
       / <code>
        ai.outputTokens
      </code>
    </td>
    
    <td>
      <code>
        gen_ai.usage.input_tokens
      </code>
      
       / <code>
        gen_ai.usage.output_tokens
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.cacheReadTokens
      </code>
      
       / <code>
        ai.cacheWriteTokens
      </code>
    </td>
    
    <td>
      <code>
        gen_ai.usage.cache_read.input_tokens
      </code>
      
       / <code>
        gen_ai.usage.cache_creation.input_tokens
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ai.finishReason
      </code>
    </td>
    
    <td>
      <code>
        gen_ai.response.finish_reasons
      </code>
    </td>
  </tr>
</tbody>
</table>

只有字段类型符合约定要求时才会进行映射，并且不会覆盖事件已经使用该名称设置的属性。资源还会获得 `deployment.environment.name`。该选项将在下一个 major 版本中成为默认值。

### Protobuf

使用 `protocol: 'http/protobuf'` 时，适配器会将同一请求作为二进制 protobuf 发送，并设置 `Content-Type: application/x-protobuf`。当收集器和网关只接受 protobuf，或需要缩小负载时，可以使用它。编码器随 evlog 提供且没有依赖，只有选择该协议时才会加载，因此 JSON 用户的 `evlog/otlp` 大小不变。

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-drain.ts
import { createOTLPDrain } from 'evlog/otlp'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }))
})
```

```typescript [Nitro]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createOTLPDrain } from 'evlog/otlp'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }))
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createOTLPDrain } from 'evlog/otlp'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createOTLPDrain } from 'evlog/otlp'

export const { handle, handleError } = createEvlogHooks({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

```typescript [TanStack Start]server/plugins/evlog-drain.ts
import { definePlugin } from 'nitro'
import { createOTLPDrain } from 'evlog/otlp'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }))
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createOTLPDrain } from 'evlog/otlp'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      drain: createOTLPDrain({
        endpoint: 'http://localhost:4318',
        protocol: 'http/protobuf',
        compression: 'gzip',
      }),
    }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
}))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
}))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createOTLPDrain } from 'evlog/otlp'

await app.register(evlog, {
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createOTLPDrain } from 'evlog/otlp'

app.use(evlog({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
}))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createOTLPDrain } from 'evlog/otlp'

EvlogModule.forRoot({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createOTLPDrain } from 'evlog/otlp'

const handler = withEvlog(new RPCHandler(router), {
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createOTLPDrain } from 'evlog/otlp'

export default withEvlog(handler, {
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createOTLPDrain } from 'evlog/otlp'

initLogger({
  drain: createOTLPDrain({
    endpoint: 'http://localhost:4318',
    protocol: 'http/protobuf',
    compression: 'gzip',
  }),
})
```

</framework-tabs>

如果需要自行编码请求，`evlog/otlp/protobuf` 会导出 `encodeOTLPLogsRequest()`。

### 严重性映射

<table>
<thead>
  <tr>
    <th>
      evlog 级别
    </th>
    
    <th>
      OTLP 严重性编号
    </th>
    
    <th>
      OTLP 严重性文本
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        trace
      </code>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      TRACE
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        debug
      </code>
    </td>
    
    <td>
      5
    </td>
    
    <td>
      DEBUG
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        info
      </code>
    </td>
    
    <td>
      9
    </td>
    
    <td>
      INFO
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        warn
      </code>
    </td>
    
    <td>
      13
    </td>
    
    <td>
      WARN
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        error
      </code>
    </td>
    
    <td>
      17
    </td>
    
    <td>
      ERROR
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fatal
      </code>
    </td>
    
    <td>
      21
    </td>
    
    <td>
      FATAL
    </td>
  </tr>
</tbody>
</table>

## 故障排除

### 缺少端点错误

```text [控制台]
[evlog/otlp] 缺少端点。请设置 OTLP_ENDPOINT 或 OTEL_EXPORTER_OTLP_ENDPOINT
```

请确保设置了端点环境变量并且服务器已重新启动。

### 401 未授权

您的身份验证标头可能缺失或无效。请检查：

1. `OTEL_EXPORTER_OTLP_HEADERS` 格式是否正确
2. 凭据是否有效且未过期
3. 端点 URL 是否正确

### 404 未找到

适配器发送到 `/v1/logs`。请确保你的端点：

- 支持 OTLP HTTP（而非 gRPC）。对于仅支持 protobuf 的接收器，请设置 `protocol: 'http/protobuf'`
- 是不包含 `/v1/logs` 后缀的基础 URL

### 日志未显示

1. 检查服务器控制台是否有 `[evlog/otlp]` 错误消息
2. 首先使用本地收集器测试以验证格式
3. 检查后端的摄取延迟（某些后端有 1-2 分钟的延迟）。

## 直接 API 用法

对于高级用例：

```typescript [server/utils/otlp.ts]
import { sendToOTLP, sendBatchToOTLP, toOTLPLogRecord } from 'evlog/otlp'

// 发送单个事件
await sendToOTLP(event, {
  endpoint: 'http://localhost:4318',
})

// 发送多个事件
await sendBatchToOTLP(events, {
  endpoint: 'http://localhost:4318',
})

// 将事件转换为 OTLP 格式（用于检查）
const otlpRecord = toOTLPLogRecord(event)
```

## 下一步

- [Axiom 适配器](https://evlog.zhcndoc.com/integrate/adapters/cloud/axiom) - 将日志发送到 Axiom
- [PostHog 适配器](https://evlog.zhcndoc.com/integrate/adapters/cloud/posthog) - 将日志发送到 PostHog
- [自定义适配器](https://evlog.zhcndoc.com/extend/custom-drains) - 构建你自己的适配器
- [最佳实践](https://evlog.zhcndoc.com/reference/best-practices) - 安全和生产环境提示。

---

- [OpenTelemetry 文档](https://opentelemetry.io/docs/)
- [自定义适配器](https://evlog.zhcndoc.com/extend/custom-drains)


## Sitemap

See the full [sitemap](https://evlog.zhcndoc.com/sitemap.md) for all pages.
