Sentry(v20.12.1) K8S 云原生架构探索,JavaScript Data Management(问题分组篇)

601 阅读12分钟

系列

  1. Sentry-Go SDK 中文实践指南
  2. 一起来刷 Sentry For Go 官方文档之 Enriching Events
  3. Snuba:Sentry 新的搜索基础设施(基于 ClickHouse 之上)
  4. Sentry 10 K8S 云原生架构探索,Vue App 1 分钟快速接入
  5. Sentry(v20.12.1) K8S 云原生架构探索,玩转前/后端监控与事件日志大数据分析,高性能+高可用+可扩展+可伸缩集群部署
  6. Sentry(v20.12.1) K8S 云原生架构探索,Sentry JavaScript SDK 三种安装加载方式
  7. Sentry(v20.12.1) K8S 云原生架构探索,SENTRY FOR JAVASCRIPT SDK 配置详解
  8. Sentry(v20.12.1) K8S 云原生架构探索, SENTRY FOR JAVASCRIPT 手动捕获事件基本用法
  9. Sentry(v20.12.1) K8S 云原生架构探索,SENTRY FOR JAVASCRIPT Source Maps详解
  10. Sentry(v20.12.1) K8S 云原生架构探索,SENTRY FOR JAVASCRIPT 故障排除
  11. Sentry(v20.12.1) K8S 云原生架构探索,1分钟上手 JavaScript 性能监控
  12. Sentry(v20.12.1) K8S 云原生架构探索,JavaScript 性能监控之管理 Transactions
  13. Sentry(v20.12.1) K8S 云原生架构探索,JavaScript 性能监控之采样 Transactions
  14. Sentry(v20.12.1) K8S 云原生架构探索,JavaScript Enriching Events(丰富事件信息)

SDK Fingerprinting

在受支持的 SDK 中,可以覆盖 Sentry 的默认分组,该分组将 fingerprint 属性作为字符串数组传递。fingerprint 数组的长度不受限制。这类似于 fingerprint rules functionality,它总是可用的,并可以实现类似的结果。

Basic Example

在最基本的情况下,值直接传递:

function makeRequest(method, path, options) {
  return fetch(method, path, options).catch(function(err) {
    Sentry.withScope(function(scope) {
      // group errors together based on their request and response
      scope.setFingerprint([method, path, String(err.statusCode)]);
      Sentry.captureException(err);
    });
  });
}

您可以使用变量替换将动态值填充到通常在服务器上计算出的指纹中。例如,可以添加值 {{default}},以将整个正常生成的分组哈希添加到指纹中。这些值与服务器端指纹识别相同。有关更多信息,请参见 Variables。

Group Errors With Greater Granularity

您的应用程序查询远程过程调用模型(Remote Procedure Call Model, RPC)接口或外部应用程序编程接口(external application Programming interface, API)服务,因此堆栈跟踪通常是相同的(即使传出请求非常不同)。

以下示例将进一步分解 Sentry 将创建的默认组(用 {{default}} 表示),并考虑错误对象的一些属性:

class MyRPCError extends Error {
  constructor(message, functionName, errorCode) {
    super(message);

    // The name of the RPC function that was called (e.g. "getAllBlogArticles")
    this.functionName = functionName;

    // For example a HTTP status code returned by the server.
    this.errorCode = errorCode;
  }
}

Sentry.init({
  ...,
  beforeSend: function(event, hint) {
    const exception = hint.originalException;

    if (exception instanceof MyRPCError) {
      event.fingerprint = [
        '{{ default }}',
        String(exception.functionName),
        String(exception.errorCode)
      ];
    }

    return event;
  }
});

Group Errors More Aggressively

通用错误(例如数据库连接错误)具有许多不同的堆栈跟踪,并且从不在一起。

以下示例将通过从数组中省略 {{ default }} 来完全覆盖 Sentry 的分组:

class DatabaseConnectionError extends Error {}

Sentry.init({
  ...,
  beforeSend: function(event, hint) {
    const exception = hint.originalException;

    if (exception instanceof DatabaseConnectionError) {
      event.fingerprint = ['database-connection-error'];
    }

    return event;
  }
});

Fingerprint Rules

Fingerprint 规则(以前称为服务器端指纹)也配置了类似于 stack trace rules 的配置,但语法略有不同。匹配器(matchers)是相同的,但不是翻转标志(flipping flags),而是分配一个指纹,它完全覆盖默认分组。

这些规则可以在 Settings > Projects > [project] > Issue Grouping > Fingerprint Rules 中按项目设置。Group Settings 有输入字段,您可以在其中写入自定义指纹规则。该语法遵循 Discover queries 中的语法。如果要否定匹配,可以在表达式前面加上感叹号(!)。

所有值都匹配,并且在堆栈跟踪的情况下,将考虑所有帧。 如果所有匹配项都匹配,则应用指纹。

Fingerprinting Config

# You can use comments to explain the rules.  Rules themselves follow the
# following syntax:
matcher:expression -> list of values
# The list of values can be hardcoded or substituted values.

下面是一个实际的示例,它将特定类型的异常组合在一起:

error.type:DatabaseUnavailable -> system-down
error.type:ConnectionError -> system-down

error.value:"connection error: *" -> connection-error, {{ transaction }}

Matchers

匹配器通常使用通配符语法。可以使用以下匹配器:

error.type

  • alias: type
  • 与异常类型(异常名称)匹配。匹配以区分大小写的形式进行。
error.type:ZeroDivisionError -> zero-division
error.type:ConnectionError -> connection-error

error.value

  • alias: value
  • 与异常值匹配。错误或异常通常具有易于理解的描述(值)。该匹配器允许不区分大小写的匹配。
error.value:"connection error (code: *)" -> connection-error
error.value:"could not connect (*)" -> connection-error

message

  • 匹配日志消息。它还会自动检查其他异常值,因为它们很难分开。匹配不区分大小写。
message:"system encountered a fatal problem: *" -> fatal-log

logger

  • 匹配 logger 的名称,这对于将 logger 的所有消息组合在一起非常有用。此匹配区分大小写。
logger:"com.myapp.mypackage.*" -> mypackage-logger

level

  • 在日志级别匹配。匹配不区分大小写。
logger:"com.myapp.FooLogger" level:"error" -> mylogger-error

tags.tag_name

  • 与标签 tag_name 的值匹配。这对于过滤某些类型的事件很有用。例如,您可以分离由特定服务器引起的事件:
tags.server_name:"canary-*.mycompany.internal" -> canary-events

stack.abs_path

  • alias: path
  • 在事件的路径上匹配,并且不区分大小写。它使用路径遍历语义,这意味着 * 不匹配斜杠,而 ** 匹配。请注意,此匹配器在 abs_pathfilename 上都匹配,因为 SDK 关于如何支持这些值可能会非常不一致。如果 glob 匹配这些值中的任何一个,则视为匹配。
stack.abs_path:"**/my-utils/*.js" -> my-utils, {{ error.type }}

stack.module

  • alias: module
  • stack.abs_path 类似,但是匹配模块名称。匹配区分大小写,并且会应用常规的规则(* 也匹配斜杠)。
stack.module:"*/my-utils/*" -> my-utils, {{ error.type }}

stack.function

  • alias: function
  • 检查堆栈跟踪中的任何函数是否与全局匹配。匹配区分大小写:
stack.function:"my_assertion_failed" -> my-assertion-failed

stack.package

  • alias: package
  • 与 frame 的 "package" 匹配。这通常是包含 frame 的 debug symbol/object file 的名称。如果有任何 frame 与该目标文件匹配,那么它将匹配。
stack.package:"**/libcurl.dylib" -> libcurl
stack.package:"**/libcurl.so" -> libcurl

family

  • 用于 "scope" 匹配器。存在以下族:用于任何类型的 JavaScript 事件的 javascript,用于任何类型的 Native 事件的 native。任何其他平台称为 other
family:native !stack.module:"myproject::*" -> not-from-my-project

app

  • 检查 frame 是否在应用程序内。与其他匹配器结合使用时特别有用。可能的值为 yesno
app:yes stack.function:"assert" -> assert

Combining Matchers

当多个匹配器组合在一起时,它们都需要匹配。在 frame 上运行的匹配器必须全部应用于同一 frame;否则,它们不被视为匹配项。

例如,如果在函数名称和模块名称上都匹配,则仅当 frame 同时在函数名称和模块名称上匹配时,才存在匹配项。 一个frame 仅与函数名称匹配是不够的,即使另一个 frame 本身会与模块名称匹配也是如此。

# this matches if a frame exists with a specific function and module name
# and also a specific error type is thrown
error.type:ConnectionError stack.function:"connect" stack.module:"bot" -> bot-error

Variables

在 fingerprint 的右边,你可以使用常数值和变量。 变量会被自动替换,并具有与匹配器相同的名称,但它们的填充方式可能不同。

变量用双花括号括起来({{variable_name}})。

{{ default }}

  • 这将填充正常分组操作将产生的默认 fingerprint。如果您想用其他方法细分一个已经存在的组,这将非常有用:
stack.function:"query_database" -> {{ default }}, {{ transaction }}

{{ transaction }}

  • 这会将 transaction 名称填写到 transaction 中。它将强制为每个 transaction 创建一个组:
error.type:"ApiError" -> api-error, {{ transaction }}

{{ error.type }}

  • alias: {{ type }}
  • 这将填写发生的错误的名称。 使用 ·chained exceptions· 时,它将是最近抛出的错误。 这将强制每个 ·transaction· 创建一个组:
stack.function:"evaluate_script" -> script-evaluation, {{ error.type }}

{{ stack.function }}

  • alias: {{ function }}
  • 这将填充 "crashing frame," 的功能名称,也称为应用程序代码的最上层 frame。
error.type:"ScriptError" -> script-evaluation, {{ stack.function }}

{{ stack.module }}

  • alias: {{ module }}
  • 这将填写"crashing frame,"的模块名称,也称为应用程序代码的最上层 frame。
error.type:"ScriptError" -> script-evaluation, {{ stack.module }}

{{ stack.package }}

  • alias: {{ package }}
  • 这将填写 "crashing frame," 的 package 名称(object file),也称为应用程序代码的最上层 frame。
stack.function:"assert" -> assertion, {{ stack.package }}

{{ logger }}

  • 这将填写引起事件的 logger 的名称。
message:"critical connection error*" -> connection-error, {{ logger }}

{{ level }}

  • 这将填充用于创建事件的日志级别的名称。
message:"connection error*" -> connection-error, {{ logger }}, {{ level }}

{{ tags.tag_name }}

  • 这会将 tag 的值填充到 fingerprint 中,例如,可以使用 fingerprint 按 server name 或类似名称拆分事件。
message:"connection error*" -> connection-error, {{ tags.server_name }}

Stack Trace Rules

如果使用堆栈跟踪进行分组,则堆栈跟踪规则(以前称为分组增强)会影响输入该算法的数据。可以在 Settings > Projects > [project] > Issue Grouping > Stack Trace Rules 下,针对每个项目配置这些规则。

每行都是一条规则;当所有表达式匹配时,一个或多个匹配表达式后跟一个或多个要执行的动作。所有规则在堆栈跟踪中的所有帧上从上到下执行。

堆栈跟踪规则的语法类似于:

matcher-name:expression other-matcher:expression ... action1 action2 ...

该语法遵循 Discover queries 中的语法。如果要否定匹配,可以在表达式前面加上感叹号(!)。

下面是一个实际的例子:

# mark all code in node modules not to be in app
stack.abs_path:**/node_modules/**         -app

# remove all generated javascript code from all grouping
stack.abs_path:**/generated/**.js         -group

Matchers

可以在一行中定义多个匹配器。下面的匹配器是可用的:

family

  • 匹配通用 platform family,目前包括 javascript, native 和 other。逗号分隔规则将它们应用于多个平台。
family:javascript stack.abs_path:**/generated/**  -group

stack.abs_path

  • alias: path
  • 该匹配器对堆栈跟踪中路径上的 Unix glob 行为不区分大小写。路径分隔符被标准化为 /。作为特殊规则,如果文件名是相对的,则它仍在 **/ 上匹配。
# match on all files under `project` with a `.c` extension
stack.abs_path:**/project/**.c` +app

# matches on vendor/foo without sub folders
stack.abs_path:**/vendor/foo/*.c` -app

# matches on `foo.c` as well as `foo/bar.c`.
stack.abs_path:**/*.gen.c` -group

stack.module

  • alias: module
  • Module 与 path 类似,但与 module 匹配。它不用于 Native,但用于 JavaScript、Python 和类似平台。匹配是区分大小写的,常规通配符是可用的。请注意,modules 不是 packages,这可能会让 Native 环境感到困惑。

stack.function

  • alias: function
  • 匹配堆栈跟踪中的函数,并且使用常规通配符区分大小写。
stack.function:myproject_* +app
stack.function:malloc      -group

stack.package

  • alias: package
  • 在堆栈跟踪中匹配 package。package 是包含 functionmodule 的容器。这是一个 .jar ,一个 .dylib 或类似的。匹配规则与 path 相同。例如,这通常是一个绝对路径。
stack.package:**/libcurl.dylib -group

app

  • stack trace frame 的应用内标记的当前状态匹配。yes 表示该 frame 是应用程序内 frame,no 表示不是。

Actions

有两种类型的 actions:标记(flag)和变量(variables)设置。

flag 标识在所有匹配器都匹配并使用以下前缀时采取的动作:

  • + 设置 flag
  • - 取消设置 flag
  • ^ 适用于匹配帧之上的帧(走向崩溃)。
  • v 适用于匹配帧下面的帧(远离崩溃)。 例如,-group ^-group 从组中移除匹配帧和它上面的所有帧。
  • app:在应用程序内标记或取消标记 frame
  • group:从分组中添加或删除 frame

variables:可以设置变量(variable=value)。当前只有一种:

  • max-frames:设置要分组的总帧数。默认值为 0,表示“所有帧”。如果设置为 3,则仅考虑前三个帧。

如果一行以 hash(#) 作为前缀,则它是一个注释并被忽略。

stack.abs_path:**/node_modules/** -group
stack.abs_path:**/app/utils/requestError.jsx -group
stack.abs_path:**src/getsentry/src/getsentry/** +app

family:native max-frames=3

stack.function:fetchSavedSearches v-group
stack.abs_path:**/app/views/**.jsx stack.function:fetchData ^-group

family:native stack.function:SpawnThread v-app -app
family:native stack.function:_NSRaiseError ^-group -app
family:native stack.function:std::* -app
family:native stack.function:core::* -app

Recommendations

这些建议将大大改善您的即用型分组体验。

Mark in-app Frames

为了主动改善您的体验,请帮助 Sentry 确定堆栈跟踪中的哪些帧是“应用程序内”(属于您自己的应用程序),哪些不是。SDK 定义了默认规则,但是在许多情况下,也可以在服务器上进行改进。特别是对于需要服务器端处理的语言(例如,Native C,C++ 或 JavaScript),最好在服务器上覆盖它。

stack.function:myapplication::* +app

你也可以通过标记其他帧 “not in-app” 来达到同样的效果。然而,如果是这种情况,你应该确保首先将所有帧设置为 “in-app”,以覆盖默认值:

app:no                   +app
stack.function:std::*    -app
stack.function:boost::*  -app

你需要强制所有帧首先在应用程序内,因为客户端 SDK 或早期处理可能已经设置了一些默认值。

Cut Stack Traces

在许多情况下,您要删除堆栈跟踪的顶部或底部。例如,许多代码库使用通用函数来生成错误。在这种情况下,错误机制将显示为堆栈跟踪的一部分。

例如,如果你使用 Rust,你可能想要删除一些与 panic 处理相关的 frames:

stack.function:std::panicking::begin_panic       ^-app -app ^-group
stack.function:core::panicking::begin_panic      ^-app -app ^-group

在这里,我们告诉系统从 begin-panic 到崩溃位置的所有 frame 都不是应用程序的一部分(包括 panic frame 本身)。在所有情况下,以上所有 frame 均与分组无关。

同样,您也可以删除堆栈跟踪的 base。如果您有不同的主循环来驱动应用程序,则此功能特别有用:

例如,如果你使用 Rust,你可能想要删除一些与 panic 处理相关的 frames:

stack.function:std::panicking::begin_panic       ^-app -app ^-group
stack.function:core::panicking::begin_panic      ^-app -app ^-group

这并不适用于所有的项目,但它可以很好地工作于许多崩溃的大型应用程序。默认的策略是考虑与分组相关的大多数堆栈跟踪。这意味着导致崩溃的每个不同的堆栈跟踪都将导致创建不同的组。如果你不想这样,你可以通过限制应该考虑的帧数来强制设置更大的组。

例如,如果堆栈跟踪中的任何帧都指向一个公共的外部库,你可以告诉系统只考虑 top N 帧:

# always only consider the top 1 frame for all native events
family:native max-frames=1

# if the bug is in proprietarymodule.so, only consider top 2 frames
family:native stack.package:**/proprietarymodule.so  max-frames=2

# these are functions we want to consider much more of the stack trace for
family:native stack.function:KnownBadFunction1  max-frames=5
family:native stack.function:KnownBadFunction2  max-frames=5