云端

PostHog 适配器

通过 OTLP 将广泛事件发送到 PostHog Logs,用于结构化日志查询、调试和监控。

PostHog 是一个开源的产品分析平台。evlog 的 PostHog 适配器通过标准 OTLP 格式将你的广泛事件发送到 PostHog Logs,为你提供一个带有过滤、搜索和尾部模式的专用日志查看器,并可利用现有的 PostHog API 密钥。

添加 PostHog 排水适配器

安装

PostHog 适配器已与 evlog 捆绑在一起:

src/index.ts
import { createPostHogDrain } from 'evlog/posthog'

快速开始

1. 获取 PostHog 项目 API 密钥

  1. 登录你的 PostHog 控制面板
  2. 进入 设置 > 项目 > 项目 API 密钥
  3. 复制密钥(以 phc_ 开头)

2. 设置环境变量

.env
POSTHOG_API_KEY=phc_your-project-api-key

3. 将排水管道连接到你的框架

// server/plugins/evlog-drain.ts
import { createPostHogDrain } from 'evlog/posthog'

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

就这样!现在你的广泛事件将以完整的 OTLP 结构(包括严重级别、跟踪上下文和结构化属性)出现在 PostHog 日志中。

配置

适配器从多个来源读取配置(优先级从高到低):

  1. 通过 createPostHogDrain() 传入的覆盖项
  2. 位于 runtimeConfig.posthog 的运行时配置(仅限 Nuxt/Nitro)
  3. 环境变量POSTHOG_*

环境变量

变量说明
POSTHOG_API_KEY项目 API 密钥(以 phc_ 开头)
POSTHOG_HOSTPostHog 主机 URL(适用于 EU 或自托管)

运行时配置(仅限 Nuxt)

通过 nuxt.config.ts 进行配置,以实现类型安全的配置:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    posthog: {
      apiKey: '', // 通过 POSTHOG_API_KEY 设置
      host: '', // 通过 POSTHOG_HOST 设置
    },
  },
})

覆盖选项

直接传递选项以覆盖任何配置:

server/plugins/evlog-drain.ts
const drain = createPostHogDrain({
  host: 'https://eu.i.posthog.com',
  timeout: 10000,
})

完整配置参考

选项类型默认值说明
apiKeystring-项目 API 密钥(必填)
hoststringhttps://us.i.posthog.comPostHog 主机 URL
distinctIdstring-每个事件使用的静态用户标识符
distinctIdFieldstringuserId保存用户标识符的事件字段(点号路径)
sessionIdFieldstringsessionId保存 PostHog 会话 ID 的事件字段(点号路径)
recordShape'json' | 'compact''json'日志记录形状,参见下文
timeoutnumber5000请求超时时间,单位为毫秒
retriesnumber2临时故障时的重试次数

日志与 PostHog 之间发生了什么

在底层,createPostHogDrain() 使用 PostHog 特定的默认值包装 OTLP 适配器的 sendBatchToOTLP()

  • 端点{host}/i/v1/logs(PostHog 的 OTLP 日志摄取端点)
  • 身份验证Authorization: Bearer {apiKey} 请求头
  • 格式:标准 OTLP ExportLogsServiceRequest,包含严重级别、跟踪上下文和结构化属性
  • 身份标识posthogDistinctIdsessionId 属性,PostHog 通过它们将日志与项目中的其他数据关联起来

选择记录形状

PostHog 将日志属性视为分面:你可以按这些属性进行过滤、细分和设置警报。使用默认的 json 形状时,嵌套字段会作为一个序列化属性到达(ai = {"calls":2,"costUsd":0.0012}),PostHog 可以显示它,但无法据此绘制图表。

compact 会将这些字段扁平化为 ai.callsai.costUsd,并使用一行摘要替代正文,从而避免重复整个事件:

server/plugins/evlog-drain.ts
const drain = createPostHogDrain({ recordShape: 'compact' })

这是 PostHog 推荐的形状。它还会减少发送的数据量:Logs 按摄取的数据量计费,而默认形状会将每个字段传输两次:一次位于正文中,一次位于属性中。

下一个主版本中,compact 将成为默认值。现在就在新项目中设置它;之后切换意味着需要重写基于 json 形状构建的已保存视图和警报。

将日志关联到用户和会话回放

携带用户标识符的日志会显示在该用户的 PostHog 个人资料中的 日志 选项卡下,无需通过猜测服务名称来查找特定用户访问过的内容。如果同时携带会话 ID,日志还会关联到该用户的会话回放。

PostHog 从日志属性中读取这两个值:使用 posthogDistinctId 表示用户,使用 sessionId 表示回放。适配器会从你的广泛事件中填充它们,因此只要事件携带这些值,就可以直接使用:

server/api/checkout.post.ts
const log = useLogger(event)

log.set({
  userId: user.id, // → posthogDistinctId,关联到该用户
  sessionId: body.sessionId, // → sessionId,关联到会话回放
})

会话 ID 来自前端。使用 posthog.get_session_id() 读取它,并将其随请求一起发送:

app/checkout.ts
import posthog from 'posthog-js'

await $fetch('/api/checkout', {
  method: 'POST',
  body: { ...payload, sessionId: posthog.get_session_id() },
})

指定你自己的字段

如果用户身份位于事件中的其他位置,可以让适配器指向该字段。两个选项都接受点号路径:

server/plugins/evlog-drain.ts
const drain = createPostHogDrain({
  distinctIdField: 'user.id',
  sessionIdField: 'session.id',
})

对于 eve agent,调用方主体就是 eve 用于路由的身份:

agent/hooks/evlog.ts
const drain = createPostHogDrain({ distinctIdField: 'eve.caller.principalId' })

静态 distinctId 会完全覆盖字段查找。对于作为单一身份运行而非代表用户运行的后端,可以使用它。

PostHog 会将属性值与它所知晓的某个用户的每个 distinct_id 进行匹配,因此该用户的任一标识符都可以使用。属性键可以在每个项目的 设置 > 日志 中配置。将其保留为默认值 posthogDistinctId,即可直接使用。

区域

PostHog 提供美国和欧盟云托管。设置 host 以匹配你的区域:

区域主机
美国(默认)https://us.i.posthog.com
欧盟https://eu.i.posthog.com
自托管你的实例 URL
.env
# 欧盟区域
POSTHOG_API_KEY=phc_xxx
POSTHOG_HOST=https://eu.i.posthog.com

在 PostHog 中查询日志

一旦日志开始流动,请在 PostHog 的 日志 选项卡中查询它们:

  1. 进入 日志,并按服务、严重级别或任何结构化属性进行过滤
  2. 使用搜索栏查找特定日志条目
  3. 点击日志条目以查看所有结构化属性

将自定义事件与日志一起发送

如果你更喜欢将日志作为 PostHog 自定义事件发送(例如用于产品分析、群组或漏斗),请使用带有 mode: 'events'createPostHogDrain()

server/plugins/evlog-drain.ts
import { createPostHogDrain } from 'evlog/posthog'

const drain = createPostHogDrain({
  mode: 'events',
  eventName: 'server_request',
  distinctId: 'my-backend-service',
})

然后像默认日志排水管道一样,将 drain 传递给你的框架(参见上面的快速开始)。

自定义事件会计入你的 PostHog 事件配额。PostHog 日志(默认的 createPostHogDrain())成本要低得多。
旧版:createPostHogEventsDrain() 已弃用,并会重定向到 createPostHogDrain({ mode: 'events' })。它将在下一个主要版本中移除。

配置要发送的事件

选项类型默认值说明
apiKeystring-项目 API 密钥(必填)
hoststringhttps://us.i.posthog.comPostHog 主机 URL
eventNamestringevlog_wide_eventPostHog 事件名称
distinctIdstring-所有事件使用的静态 distinct_id
distinctIdFieldstringuserId保存 distinct_id 的事件字段(点号路径)
recordShape'json' | 'compact''json'事件属性形状:compact 会扁平化嵌套字段(参见下文
timeoutnumber5000请求超时时间,单位为毫秒

事件格式

evlog 将广泛事件映射到 PostHog 事件:

evlog 字段PostHog 字段
config.distinctIduserIdservicedistinct_id(回退链)
timestamptimestamp
levelproperties.level
serviceproperties.service
environmentproperties.environment
所有其他字段properties.*

使用 recordShape: 'compact' 时,其他字段会以与日志模式相同的方式扁平化为点号键:嵌套的 ai 对象会作为 properties.ai.costUsdproperties.ai.calls 到达,而不是作为一个不透明的 properties.ai 值。PostHog 可以对这些单独的属性进行过滤和细分;默认的 json 形状会将嵌套对象保留为单个序列化属性。有关其中的权衡,以及它如何影响已保存的视图和警报,请参见选择记录形状

distinct ID 的解析方式

distinct_id 遵循回退链:

  1. config.distinctIdcreatePostHogDrain({ mode: 'events' }) 中的显式覆盖项
  2. event.userId — 或 distinctIdField 指向的字段(当其值为字符串或数字时)
  3. event.service — 最终回退值,以匿名事件发送

已识别事件和匿名事件

解析为真实用户的事件属于已识别事件:PostHog 会为其创建用户个人资料并附加用户属性。

当没有解析出标识符时,事件会作为匿名事件发送($process_person_profile: false),而不是将每个请求都归入一个以服务命名的“用户”。PostHog 对匿名事件的收费更低,并且不会将其纳入用户个人资料。

server/plugins/evlog-drain.ts
// 已识别:携带 `userId` 的事件会创建并更新用户个人资料
const drain = createPostHogDrain({ mode: 'events' })

// 将其识别为单一后端身份,无论请求来自何处
const service = createPostHogDrain({ mode: 'events', distinctId: 'checkout-worker' })
匿名事件的成本最高可比已识别事件低 4 倍。仅在确实需要进行逐用户分析的事件上设置身份。

何时使用日志,何时使用事件

createPostHogDrain()createPostHogDrain({ mode: 'events' })
格式OTLP 日志(/i/v1/logsPostHog 事件(/batch/
PostHog UI日志查看器事件探索器
成本较低(专用的日志管道)较高(计入事件)
最佳用途调试、日志搜索、监控产品分析、群组、漏斗

你可以同时使用两种排水管道以获得最佳效果:

server/plugins/evlog-drain.ts
import { createPostHogDrain } from 'evlog/posthog'

const logs = createPostHogDrain()
const events = createPostHogDrain({ mode: 'events' })

const drain = async (ctx) => {
  await Promise.allSettled([logs(ctx), events(ctx)])
}

故障排除

缺少 API 密钥错误

Console
[evlog/posthog] 缺少 apiKey。设置 POSTHOG_API_KEY 环境变量或传递给 createPostHogDrain()

确保环境变量已设置,并且在添加后重新启动了服务器。

事件未出现

PostHog 异步处理事件。事件可能需要短暂延迟(通常不到 1 分钟)才能在仪表板中显示。

  1. 检查服务器控制台是否有 [evlog/posthog] 错误信息
  2. 验证 API 密钥是否正确并以 phc_ 开头
  3. 确认 host 与你的 PostHog 区域(美国或欧盟)匹配

区域错误

如果你在 PostHog EU 上但使用了默认的美国主机,事件交付将失败,并且适配器会在服务器控制台记录错误(例如 [evlog/posthog])。设置正确的主机:

.env
POSTHOG_HOST=https://eu.i.posthog.com

不使用框架调用排水管道

对于高级用例,你可以使用更低级别的函数:

server/utils/posthog.ts
import { sendToPostHog, sendBatchToPostHog } from 'evlog/posthog'

// 发送单个事件到 PostHog 日志(OTLP)
await sendToPostHog(event, {
  apiKey: 'phc_xxx',
})

// 批量发送多个事件
await sendBatchToPostHog(events, {
  apiKey: 'phc_xxx',
})

对于自定义事件,请使用专用的事件函数:

server/utils/posthog.ts
import { sendToPostHogEvents, sendBatchToPostHogEvents, toPostHogEvent } from 'evlog/posthog'

// 发送单个自定义事件
await sendToPostHogEvents(event, {
  apiKey: 'phc_xxx',
})

// 批量发送多个自定义事件
await sendBatchToPostHogEvents(events, {
  apiKey: 'phc_xxx',
})

// 转换为 PostHog 格式(用于检查)
const posthogEvent = toPostHogEvent(event, { apiKey: 'phc_xxx' })

下一步