内容整理于官方开发文档
系列
- Docker Compose 部署与故障排除详解
- K8S + Helm 一键微服务部署
- Sentry 开发者贡献指南 - 前端(ReactJS生态)
- Sentry 开发者贡献指南 - 后端服务(Python/Go/Rust/NodeJS)
- Sentry 开发者贡献指南 - 前端 React Hooks 与虫洞状态管理模式
性能监控指南
本文档介绍了 SDK
应如何通过分布式跟踪添加对性能监控的支持。
这应该提供 SDK
需要实现的 API
的概述,而不强制要求内部实现细节。
参考实现:
SDK 配置
通过设置两个新的 SDK
配置选项之一来启用跟踪,tracesSampleRate
和 tracesSampler
。如果未设置,则两者都默认为 undefined
,从而选择如何加入跟踪。
tracesSampleRate
这应该是介于 0.0
和 1.0
(含)之间的 float/double
,表示任何给定 transaction
将被发送到 Sentry
的百分比机会。
因此,0.0
是 0%
的机会(不会发送),而 1.0
是 100%
的机会(都将发送)。
此 rate
同样适用于所有 transaction
;
换句话说,每个 transaction
都应该有相同的随机机会以 sampled = true
结束,等于 tracesSampleRate
。
tracesSampler
这应该是一个 callback
,在 transaction
开始时调用,它将被赋予一个 samplingContext
对象,并且应该返回一个介于 0.0
和 1.0
之间的采样率_对于所讨论的 transaction_。
此采样率的行为方式应与上面的 tracesSampleRate
相同,不同之处在于它仅适用于新创建的 transaction
,因此可以以不同的 rate
对不同的 transaction
进行采样。
返回 0.0
应该强制删除 transaction
(设置为 sampled = false
),返回 1.0
应该强制发送 transaction
(设置 sampled = true
)。
可选地,tracesSampler
callback 也可以返回一个布尔值来强制进行采样决策(false
等同于 0.0
,true
等同于 1.0
)。
如果返回两种不同的数据类型在实现语言中不是一个选项,则可以安全地省略这种可能性。
maxSpans
由于 transaction
payload 在摄取端强制执行最大大小,因此 SDK
应限制附加到事务的 span
数。
这类似于如何限制面包屑
和其他任意大小的列表以防止意外误用。
如果在达到最大值后添加新的 span
,SDK
应删除 span
并理想地使用内部日志记录来帮助调试。
maxSpans
应该作为一个内部的、不可配置的、默认为 1000
的常量来实现。如果在给定的平台中有理由,它可能会变得可配置。
maxSpans
限制还可以帮助避免永远不会完成的 transaction
(在 span
打开时保持 transaction
打开的平台中),防止 OOM
错误,并通常避免降低应用程序性能。
Event
变更
在撰写本文时,transaction
是作为 Event
模型的扩展实现的。
Transaction
的显着特征是 type: "transaction"
。
除此之外,Event
获得了新的字段:spans
、contexts.TraceContext
。
新的 Span
和 Transaction
类
在内存中,span
构建了一个定时操作的概念树(conceptual tree)
。
我们称整个 span tree
为 transaction
。
有时我们使用术语 "transaction"
来指代作为整棵树的 span tree
,有时特指树的 root span
。
通过网络,transaction
被序列化为 JSON
作为增强的 Event
,并作为 envelope
发送。
不同的 envelope
类型用于优化摄取(因此我们可以以不同于其他事件的方式路由 “transaction events”
,主要是 “error events”
)。
在 Sentry UI
中,您可以使用 Discover
查看所有类型的事件,并使用 Issues
和 Performance
部分分别深入研究 errors
和 transactions
。
面向用户的跟踪文档解释了更多产品级别的概念。
Span
类将每个单独的 span
存储在 trace
中。
Transaction
类就像一个 span
,有几个主要区别:
-
Transaction
有name
,span
没有。 - 在
span
上调用finish
方法会记录span
的结束时间戳。对于transaction
,finish
方法另外向Sentry
发送一个事件。
Transaction
类可能继承自 Span
,但这是一个实现细节。
从语义上讲,transaction
既表示 span tree
的 top-level span
,也表示向 Sentry
报告的单位。
-
Span
接口- 创建
Span
时,将startTimestamp
设置为当前时间 -
SpanContext
是Span
的属性集合(可以是一个实现细节)。如果可能,SpanContext
应该是不可变的。 -
Span
应该有一个方法startChild
,它使用当前span
的id
作为新span
的parentSpanId
创建一个新的span
,并将当前span
的sampled
值复制到新span
的sampled
属性 -
startChild
方法应遵守maxSpans
限制,一旦达到限制,SDK
不应为给定的transaction
创建新的子span
。 -
Span
应该有一个名为toSentryTrace
的方法,它返回一个字符串,该字符串可以作为名为sentry-trace
的header
发送。 -
Span
应该有一个名为iterHeaders
(适应平台的命名约定)的方法,它返回一个可迭代的或header
名称和值的映射。这是一个包含return {"sentry-trace": toSentryTrace()}
的薄wrapper
。 请参阅continueFromHeaders
以了解为什么存在这种情况,并且在编写集成(integration)
时应该首选。
- 创建
-
Transaction
接口- 一个
Transaction
内部包含一个子Span
的平面列表(不是树结构) -
Transaction
还有一个setName
方法来设置transaction
的名称 -
Transaction
在创建时收到一个TransactionContext
(新属性与SpanContext
是name
) - 由于
Transaction
继承了Span
,因此它具有所有Span
可用的函数并且可以像Span
一样进行交互 - 一个
transaction
要么被采样(sampled = true
),要么被取消采样(sampled = false
),一个在transaction
的生命周期中被继承或设置一次的决定,并且在任何一种情况下都会传播给所有的children
。不应将未抽样的transaction
发送给Sentry
。 -
TransactionContext
应该有一个叫做fromSentryTrace
的static/ctor
方法,它用从sentry-trace
header值接收的数据预填充一个TransactionContext
-
TransactionContext
应该有一个名为continueFromHeaders(headerMap)
的static/ctor
方法,它现在实际上只是一个围绕fromSentryTrace(headerMap.get("sentry-trace"))
的薄wrapper
。integration/framework-sdk
的作者应该更喜欢fromSentryTrace
,因为它隐藏了核心sdk
中更深层次使用的确切header
名称,并为将来使用其他header
(来自W3C
)留下了机会,而无需更改所有集成。
- 一个
-
Span.finish()
- 只需将
endTimestamp
设置为当前时间(在 payloadtimestamp
中)
- 只需将
-
Transaction.finish()
-
super.finish()
(在 Span 上调用 finish) - 仅当
sampled == true
时才将其发送给Sentry
- 一个
Transaction
需要被包裹在一个Envelope
中并发送到Envelope Endpoint
-
Transport
应该为Transactions
/Events
使用相同的内部队列 -
Transport
应该实现基于类别的速率限制 → -
Transport
应该处理在内部将Transaction
包装在Envelope
中
-
采样
每个 transaction
都有一个 “抽样决策”
,即一个布尔值,指示是否应该将其发送给 Sentry
。
这应该在 transaction
的生命周期内只设置一次,并且应该存储在内部的 sampled
布尔值中。
transaction
可以通过多种方式结束抽样决策(sampling decision)
:
- 根据
tracesSampleRate
中设置的静态采样率
随机采样 - 根据
tracesSampler
返回的动态采样率
进行随机采样 -
tracesSampler
返回的绝对决策(100%
概率或0%
概率) - 如果
transaction
有父级,继承其父级的抽样决策
- 传递给
startTransaction
的绝对决策
当其中一个以上发挥作用的可能性时,应适用以下优先规则:
- 如果将
抽样决策
传递给startTransaction
(startTransaction({name: "my transaction", sampled: true})
),则将使用该决策,而不管其他任何事情。 - 如果定义了
tracesSampler
,则将使用其决策
。它可以选择保留或忽略任何父采样决策
,或使用采样上下文数据
做出自己的决策或为transaction
选择采样率
。 - 如果未定义
tracesSampler
,但存在父采样决策,则将使用父采样决策。 - 如果没有定义
tracesSampler
并且没有父采样决策,则将使用tracesSampleRate
。
Transaction
应仅通过tracesSampleRate
或tracesSampler
进行采样。sampleRate
配置用于error
事件,不应应用于transaction
。
采样上下文
如果定义,tracesSampler
回调应该传递一个 samplingContext
对象,该对象至少应该包括:
- 创建
transaction
的transactionContext
- 一个布尔值
parentSampled
,包含从父级传递过来的采样决策
,如果有的话 - 来自可选的
customSamplingContext
对象的数据在手动调用时传递给startTransaction
根据平台,可能包含其他默认数据。(例如,对于服务器框架,包含与 transaction
正在测量的请求相对应的 request
对象是有意义的。)
传播
transaction
的抽样决策
应传递给其所有子项,包括跨服务边界。这可以在相同服务子项的 startChild
方法中完成,并为不同服务中的子项使用 senry-trace
header。
Header sentry-trace
Header
用于跟踪传播。SDK
使用 header
继续跟踪来自上游服务(传入 HTTP
请求),并将跟踪信息传播到下游服务(传出 HTTP
请求)。
sentry-trace = traceid-spanid-sampled
sampled
是可选的。所以至少,它是预期的:
sentry-trace = traceid-spanid
为了与 W3C traceparent
header(没有版本前缀)
和 Zipkin's b3
headers(考虑 64
位和 128
位的 traceId
有效)提供最小的兼容性,sentry-trace
header 应具有以 32
个十六进制字符编码的 128
位的 traceId
以及以 16
个十六进制字符编码的 64
位 spanId
。
为了避免与 W3C traceparent
header(我们的 header
相似但不相同)混淆,
我们将其简称为 sentry-trace
。header
中没有定义版本。
- https://www.w3.org/TR/trace-context/#traceparent-header
- https://zipkin.io/pages/instrumenting#communicating-trace-information
sampled
值
为简化处理,该值由单个(可选)字符组成。可能的值为:
- No value means defer
0 - Don't sample
1 - Sampled
与 b3
header 不同,sentry-trace
header 不应该只包含一个采样决策
,没有 traceid
或 spanid
值。有很好的理由 无论采样决策如何,始终包含 traceid
和 spanid
,这样做也简化了实现。
除了在 Sentry
的情况下使用 *defer 的通常原因外,
还有一个原因是下游系统使用 Sentry
捕获 error
事件。可以在那时做出决定,对跟踪进行采样,以便为报告的崩溃提供跟踪数据。
sentry-trace = sampled
这实际上对于代理将其设置为 0
并选择退出跟踪很有用。
Static API 变更
Sentry.startTransaction
函数应该接受两个参数 - 传递给 Transaction
构造函数的 transactionContext
和一个包含要传递给 tracesSampler
(如果已定义)的数据的可选的 customSamplingContext
对象。
它创建一个绑定到当前 hub
的 Transaction
并返回实例。
用户与实例交互以创建子 span
,因此,必须自己跟踪它。
Hub
变更
-
引入一个名为
traceHeaders
的方法- 此函数返回
header
(string)sentry-trace
- 该值应该是当前在
Scope
上的Span
的trace header
字符串
- 此函数返回
-
引入一个名为
startTransaction
的方法- 采用与
Sentry.startTransaction
相同的两个参数 - 创建一个新的
Transaction
实例 - 应按照本文档
'Sampling'
部分中更详细的描述实施抽样
- 采用与
-
修改名为
captureEvent
或captureTransaction
的方法- 不要为
transaction
设置lastEventId
- 不要为
Scope
变更
Scope
持有对当前 Span
或 Transaction
的引用。
-
Scope
引入setSpan
- 这可以在内部使用,来传递
Span
/Transaction
,以便集成可以将子项附加到它 - 在
Scope
(旧版)上设置transaction
属性应该覆盖存储在Scope
中的Transaction
的名称,如果有的话。
这样,即使用户无法直接访问Transaction
的实例,我们也可以让用户选择更改transaction
名称。
- 这可以在内部使用,来传递
与 beforeSend
和事件处理器的交互
beforeSend
回调是我们认为最重要的特殊 Event Processor
。适当的事件处理器通常被认为是内部的。
Transaction
应该不通过 beforeSend
。但是,它们仍然由事件处理器
处理。
这是在将 transaction
作为 event
的当前实现处理的一些灵活性与为 transaction
和 span
的不同生命周期 hook
留出空间之间的折衷。
动机:
-
面向未来:如果用户依赖
beforeSend
进行transaction
,
这将使最终在不破坏用户代码的情况下实现单个span
摄取变得复杂。
在撰写本文时,transaction
作为event
发送,但这被视为实现细节。 -
API 兼容性:用户拥有他们现有的
beforeSend
实现,只需要处理错误事件。
我们将transaction
作为一种新型event
引入。
当用户升级到新的SDK
版本并开始使用跟踪时,他们的beforeSend
将开始看到他们的代码不打算处理的新类型。
在transaction
之前,他们根本不必关心不同的事件类型。
有几种可能的后果:破坏用户应用程序;默默地和无意地放弃transaction
;transaction
事件以令人惊讶的方式修改。 -
就可用性而言,
beforeSend
不适合删除transaction
,就像删除error
一样。Error
是一个时间点事件。当error
发生时,用户在beforeSend
中有完整的上下文,
并且可以在它进入Sentry
之前修改/丢弃
事件。对于交易,transaction
是不同的。
创建transaction
,然后将它们打开一段时间,同时创建child span
并将其附加到它。
同时传出的HTTP
请求包括当前transaction
与其他服务的采样决策
。
在span
和transaction
完成后,将transaction
放入类似beforeSend
的钩子中会在跟踪中留下来自其他服务的孤立transaction
。
同样,在此后期将采样决策
修改为"yes"
也会产生不一致的痕迹。
跟踪上下文(实验性)
为了对跟踪进行采样,我们需要沿着调用链传递 trace id
以及做出采样决策
所需的信息,即所谓的 跟踪上下文(trace context)
。
协议
Trace
信息作为编码的 tracestate
header 在 SDK
之间传递,SDK
预计会拦截和传播这些 header
。
对于向 sentry
提交的事件,trace context
作为嵌入在 Envelope header
中的 JSON
对象发送,key
为 trace
。
跟踪上下文
无论采用何种传输机制,trace context
都是具有以下字段的 JSON
对象:
-
trace_id
(string, required) -UUID V4
编码为不带破折号的十六进制序列(例如771a43a4192642f0b136d5159a501700
),它是一个由32
个十六进制数字组成的序列。这必须与提交的transaction item
的trace id
匹配。 -
public_key
(string, required) - 来自SDK
使用的DSN
的Public key
。它允许Sentry
通过基于起始项目解析相同的规则集来对跨多个项目的跟踪进行采样。 -
release
(string, optional) - 客户端选项中指定的版本名称,通常是:package@x.y.z+build
。 这应该与transaction event payload
的release
属性匹配* -
environment
- 客户端选项中指定的environment
名称,例如staging
。 这应该与transaction event payload
的environment
属性匹配* -
user
(object, optional) - 包含以下字段的scope
的user context
的子集:-
id
(string, optional) - 用户上下文的id
属性。 -
segment
(string, optional) - 用户数据包中的segment
属性值(如果存在)。将来,该字段可能会被提升为用户上下文的适当属性。
-
-
transaction
(string, optional) - 在scope
上设置的transaction
名称。这应该与transaction event payload
的transaction
属性匹配*
例子:
{
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3",
"release": "myapp@1.1.2",
"environment": "production",
"user": {
"id": "7efa4978da177713df088f846f8c484d",
"segment": "vip"
},
"transaction": "/api/0/project_details"
}
Envelope Headers(信封头)
当通过 Envelope
向 Sentry
发送 transaction
事件时,必须在 trace
字段下的 envelope header
中设置 trace
信息。
这是一个包含 trace context
的最小 envelope header
的示例(尽管 header
不包含换行符,但在下面的示例中添加了换行符以提高可读性):
{
"event_id": "12c2d058d58442709aa2eca08bf20986",
"trace": {
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3"
// other trace attributes
}
}
Tracestate Headers(跟踪状态头)
将跟踪上下文
传播到其他 SDK
时,Sentry
使用 W3C tracestate
header。有关如何将这些 header
传播到其他 SDK
的更多信息,请参阅 "Trace Propagation"
。
Tracestate
header 包含几个特定于供应商的不透明数据。根据 HTTP
规范,这些多个 header
值可以通过两种方式给出,通常由 HTTP
库和开箱即用的框架支持:
- 用逗号连接:
tracestate: sentry=<data>,other=<data>
- 重复:
tracestate: sentry=<data> tracestate: other=<data>
要创建 tracestate header
的内容:
- 将完整的
trace context
序列化为JSON
,包括trace_id
。 - 如果字符串在平台上的表示方式不同,则将生成的
JSON
字符串编码为UTF-8
。 - 使用
base64
对UTF-8
字符串进行编码。 - 去除尾随填充字符 (
=
),因为这是一个保留字符。 - 在前面加上
"sentry="
,导致"sentry=<base64>"
。 - 如上所述加入
header
。
通过去除尾随填充,默认的
base64
解析器可能会检测到不完整的payload
。选择允许丢失=
或允许截断payload
的解析模式。
例如:
{
"trace_id": "771a43a4192642f0b136d5159a501700",
"public_key": "49d0f7386ad645858ae85020e393bef3",
"release": "1.1.22",
"environment": "dev",
"user": {
"segment": "vip",
"id": "7efa4978da177713df088f846f8c484d"
}
}
将编码为
ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ
并导致 header
tracestate: sentry=ewogIC...IH0KfQ,other=[omitted]
(注意 header
末尾的第三方条目;新的或修改的条目总是添加到左侧,因此我们将 sentry=
值放在那里。另请注意,尽管此处为了清晰起见省略了编码值, 在真正的 header 中,将使用完整的值。)
实施指南
支持此 header
的 SDK
必须:
- 创建新的
trace context
时使用scope
信息 - 为包含
transaction
的envelope
添加带有trace context
的envelope header
- 将
tracestate
HTTP header 添加到传出的 HTTP 请求以进行传播 - 在适用的情况下拦截对
tracestate
HTTP header 的传入HTTP
请求,并将它们应用到local trace context
背景
这是性能指南
涵盖的 trace ID
传播的扩展。
根据统一 API 跟踪规范
,Sentry SDK
通过集成向传出请求添加 HTTP header sentry-trace
。
最重要的是,此 header
包含 trace ID
,它必须与 transaction event
的 trace id
以及下面的 trace context
的 trace id
匹配。
trace context
应在 W3C traceparent header 中定义的附加 tracestate
header 中传播。
请注意,我们必须保持与 W3C
规范的兼容性,而不是专有的 sentry-trace
header。
除了 Sentry SDK
放置的内容之外,tracestate
header 还包含供应商特定的不透明数据。
Client 选项
虽然 trace context
正在开发中,但它们应该在内部 trace_sampling
布尔值 client
选项后面进行门控。该选项默认为 false
,不应在 Sentry
文档中记录。
根据平台命名指南,该选项应该适当地区分大小写:
-
trace_sampling
(snake case) -
traceSampling
(camel case) -
TraceSampling
(pascal case) -
setTraceSampling
(Java-style setters)
添加 Envelope Header
在以下任何一种情况下,SDK
应将 envelope header
添加到传出 envelope
中:
-
envelope
包含transaction event
。 -
scope
有一个transaction
绑定。
具体来说,这意味着即使没有 transaction
的 envelope
也可以包含 trace
envelope header,
从而允许 Sentry
最终对属于 transaction
的 attachment
进行采样。
当 envelope
包含 transaction
且 scope
有绑定 transaction
时,SDK
应使用 envelope
的 transaction
来创建 trace
envelope header。
冻结上下文
为了确保 trace
中所有 transaction
的 trace context
完全一致,一旦通过网络发送 trace context
,就不能更改 trace context
,即使 scope
或 options
之后发生更改。
也就是说,一旦计算出 trace context
就不再更新。即使应用程序调用 setRelease
,旧版本仍保留在 context
中。
为了弥补对 setTransaction
和 setUser
等函数的延迟调用,
可以认为 trace context
处于两种状态:NEW 和 SENT。
最初,context
处于 NEW 状态并且可以修改。第一次发送后,它将变为 SENT 并且不能再更改。
我们建议 trace context
应该在第一次需要时即时计算:
- 创建
Envelope
- 传播到传出的
HTTP
请求
Trace context
必须保留,直到用户开始新的 trace
,此时 SDK
必须计算新的 trace context
。
建议
SDK
记录在trace context
冻结时会导致trace context
更改的属性修改,例如user.id
,以简化常见动态采样
陷阱的调试。
传入上下文
与拦截来自入站 HTTP
请求的 trace ID
相同,SDK
应读取 tracestate
header 并假设 Sentry 跟踪上下文(如果指定)。这样的上下文立即冻结在 SENT 状态,不应再允许修改。
平台细节
在 JavaScript 中编码
如前所述,我们需要使用 UTF-8
字符串对 JSON trace context
进行编码。JavaScript
内部使用 UTF16
,因此我们需要做一些工作来进行转换。
Base64 encoding and decoding in JavaScript(以及 Using Javascript's atob to decode base64 doesn't properly decode utf-8 strings)介绍了基本思想。
- https://attacomsian.com/blog/javascript-base64-encode-decode
- https://*.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings
简而言之,这是将 context
转换为可以保存在 tracestate
中的 base64
字符串的函数。最后我们采用了一个更简单的实现,但想法是一样的:
// Compact form
function objToB64(obj) {
const utf16Json = JSON.stringify(obj);
const b64 = btoa(
encodeURIComponent(utf16Json).replace(
/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode("0x" + p1);
}
)
);
const len = b64.length;
if (b64[len - 2] === "=") {
return b64.substr(0, len - 2);
} else if (b64[len - 1] === "=") {
return b64.substr(0, len - 1);
}
return b64;
}
// Commented
function objToB64(obj) {
// object to JSON string
const utf16Json = JSON.stringify(obj);
// still utf16 string but with non ASCI escaped as UTF-8 numbers)
const encodedUtf8 = encodeURIComponent(utf16Json);
// replace the escaped code points with utf16
// in the first 256 code points (the most wierd part)
const b64 = btoa(
endcodedUtf8.replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) {
return String.fromCharCode("0x" + p1);
})
);
// drop the '=' or '==' padding from base64
const len = b64.length;
if (b64[len - 2] === "=") {
return b64.substr(0, len - 2);
} else if (b64[len - 1] === "=") {
return b64.substr(0, len - 1);
}
return b64;
}
// const test = {"x":"a-