教程:修改 Grafana 的源代码

导航至

本文最初发布于 dev.to,并经许可在此处转载。

一次探索和猜测的故事

所以这篇博客与我通常的教程有点不同…

一点背景:我一直在与 Jacob Marble 合作,测试并“演示”他使用 InfluxDB 3.0 和 OpenTelemetry 生态系统的工作(如果您想了解更多信息,我强烈建议查看这篇 博客)。

在项目中,我们确定需要为 InfluxDB 数据源启用特定的 Grafana 功能,特别是从追踪到日志的功能。Grafana 是一个开源平台,其主要优势之一是能够修改其源代码以适应我们的独特需求。然而,即使对于经验最丰富的开发人员来说,深入研究如此强大的工具的代码库也可能令人望而生畏。

尽管存在复杂性,我们还是迎接了挑战,并一头扎进了 Grafana 的源代码。我们跌跌撞撞,一路学习了很多东西。现在,我们已经成功修改了 Grafana 以满足我们特定的项目需求,我认为是时候与大家分享这些获得的知识了。

本博客的目的不仅是为您提供调整 Grafana 源代码的逐步指南,还在于激发您探索和调整开源项目以满足您的需求。它是关于传授一种方法和一种心态,培养一种好奇心文化,并鼓励更多动手学习和解决问题。

我希望本指南能够启发您为您的项目修改 Grafana 的源代码,从而扩展开源平台可能实现的范围。现在是时候卷起袖子,深入 Grafana 代码的深处了。

问题

因此,我们的问题在于 Grafana 的 Trace 可视化

Trace visualization of Grafana

正如您所见,除了一个禁用的按钮:此 span 的日志,可视化效果在 InfluxDB 中表现相当好。如果我们不使用追踪数据源(在本例中,Jaeger 与 InfluxDB 3.0 作为 gRPC 存储引擎)配置日志数据源,则 Grafana 会自动禁用此按钮。Grafana 通常默认使用 日志浏览器界面 表示日志数据源。常见的日志数据源包括 LokiOpenSearchElasticsearch。因此,让我们前往 Jaeger 数据源并进行配置…

Connections-data source

您可以通过 连接 -> 数据源 导航数据源。我们目前配置了三个数据源:FlightSQL、InfluxDB 和 Jaeger。如果我们打开 Jaeger 配置并导航到 Trace to Logs 部分,我们希望能够选择 InfluxDB 或 FlightSQL 作为我们的数据源。

Trace to logs - Grafana

休斯顿,我们遇到问题了。Grafana 似乎不承认 InfluxDB 是日志数据源。这很合理。InfluxDB 最近才成为日志的可行选项。那么,我们有哪些选择呢?

  1. 我们躺平,接受问题,并希望将来此功能变得足够通用,以支持更多数据源。
  2. 采取行动,自己做出改变。

嗯,到现在您已经知道我们选择了哪个选项。

解决方案

本节总结了我为发现需要进行的更改、如何为自己的数据源实施更改以及最终如何构建自己的 Grafana OSS 自定义版本而采取的步骤。

发现

因此,第一步是了解从哪里开始。Grafana 是一个庞大的开源平台,包含许多组件,因此我需要缩小搜索范围。因此,我做的第一件事是在 Grafana 存储库中搜索生命迹象。

Discovery

正如您所见,我通过使用关键字 trace 进行了这个小发现,这让我找到了 TraceToLogs 目录。这让我找到了 TraceToLogsSettings.tsx 中的这段代码

export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
  const supportedDataSourceTypes = [
    'loki',
    'elasticsearch',
    'grafana-splunk-datasource', // external
    'grafana-opensearch-datasource', // external
    'grafana-falconlogscale-datasource', // external
    'googlecloud-logging-datasource', // external
  ];

这段代码似乎创建了 Trace to Logs 功能支持的数据源的静态列表。我们可以通过列表中的一些常见嫌疑对象(Loki、Elasticsearch 等)来确认这一点。基于这一发现,我们对 Grafana 源代码的第一个修改是将我们的数据源添加到此列表中。

现在,作为编码悲观主义者,我知道这可能不是我们唯一需要做的更改,但这是一个很好的起点。所以,我做了以下事情

  1. 我 fork 了 Grafana 存储库
  2. 克隆了存储库
git clone https://github.com/InfluxCommunity/grafana

在我进行这些修改之前,我想做更多的搜索,看看是否还有其他更改应该进行。TraceToLogsSettings 文件中有一行引起了我的注意

const updateTracesToLogs = useCallback(
    (value: Partial<TraceToLogsOptionsV2>) => {
      // Cannot use updateDatasourcePluginJsonDataOption here as we need to update 2 keys, and they would overwrite each
      // other as updateDatasourcePluginJsonDataOption isn't synchronized
      onOptionsChange({
        ...options,
        jsonData: {
          ...options.jsonData,
          tracesToLogsV2: {
            ...traceToLogs,
            ...value,
          },
          tracesToLogs: undefined,
        },
      });
    },
    [onOptionsChange, options, traceToLogs]
  );

它是 TraceToLogsOptionsV2。当我搜索 Grafana 使用此接口的位置时,我找到了以下条目。

TraceToLogsOptionsV2

看来我们可能还需要在 createSpanLink.tsx 文件中做一些工作。在本节中,我找到了以下代码片段。此时,我的问题是“这段代码到底在做什么?”

case statement

长话短说,case 语句本质上是告诉 trace 可视化检查定义的日志数据源(如果有),并定义与该数据源相关的查询界面。如果在 case 语句中未找到指定的数据源,则 Grafana 只会禁用该按钮。这意味着正如我们所怀疑的那样,仅更改原始文件是不够的。

好的,我们的调查已完成,让我们继续进行代码更改。

修改

我们需要修改两个文件

  1. TraceToLogsSettings.tsx
  2. createSpanLink.tsx

让我们从最容易解决的文件开始,然后从那里开始。

TraceToLogsSettings

此文件相对容易更改。我们所需要做的就是修改受支持的日志输入源的静态列表,如下所示

export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
  const supportedDataSourceTypes = [
    'loki',
    'elasticsearch',
    'grafana-splunk-datasource', // external
    'grafana-opensearch-datasource', // external
    'grafana-falconlogscale-datasource', // external
    'googlecloud-logging-datasource', // external
    'influxdata-flightsql-datasource', // external
    'influxdb', // external
  ];

正如您所见,我添加了两个数据源。我快速构建了 Grafana 项目,以查看这对我们的数据源配置有何影响(我们将在最后讨论如何构建)。

Trace-to-logs-influxdb-v1

太棒了!我们得到了结果。现在,这仍然没有启用 Trace View 中的按钮,但我们已经知道这将需要更多的工作。

现在,让我们继续进行我们修改的核心部分。郑重声明,我不是 TypeScript 开发人员。我所知道的是,该文件包含大量示例,我们可以使用这些示例尝试盲目复制粘贴,并进行一些修改。我最终对两个插件都这样做了,但为了使博客简洁,我们将重点关注 InfluxDB 官方插件。

我的假设是使用 Grafana Loki 界面作为 InfluxDB 界面的基础。首先包括添加数据源类型

import { LokiQuery } from '../../../plugins/datasource/loki/types';
import { InfluxQuery } from '../../../plugins/datasource/influxdb/types';

当 Grafana 拥有数据源的官方插件时,这些很容易找到,因为它嵌入在官方存储库中。对于我们的社区插件,我有两个选择:在文件中定义静态接口或提供更多查询参数。我选择了后者。

下一步是修改 case 语句

// TODO: This should eventually move into specific data sources and added to the data frame as we no longer use the
    //  deprecated blob format and we can map the link easily in data frame.
    if (logsDataSourceSettings && traceToLogsOptions) {
      const customQuery = traceToLogsOptions.customQuery ? traceToLogsOptions.query : undefined;
      const tagsToUse =
        traceToLogsOptions.tags && traceToLogsOptions.tags.length > 0 ? traceToLogsOptions.tags : defaultKeys;
      switch (logsDataSourceSettings?.type) {
        case 'loki':
          tags = getFormattedTags(span, tagsToUse);
          query = getQueryForLoki(span, traceToLogsOptions, tags, customQuery);
          break;
        case 'grafana-splunk-datasource':
          tags = getFormattedTags(span, tagsToUse, { joinBy: ' ' });
          query = getQueryForSplunk(span, traceToLogsOptions, tags, customQuery);
          break;
        case 'influxdata-flightsql-datasource':
            tags = getFormattedTags(span, tagsToUse, { joinBy: ' OR ' });
            query = getQueryFlightSQL(span, traceToLogsOptions, tags, customQuery);
          break;
        case 'influxdb':
            tags = getFormattedTags(span, tagsToUse, { joinBy: ' OR ' });
            query = getQueryForInfluxQL(span, traceToLogsOptions, tags, customQuery);
          break;
        case 'elasticsearch':
        case 'grafana-opensearch-datasource':
          tags = getFormattedTags(span, tagsToUse, { labelValueSign: ':', joinBy: ' AND ' });
          query = getQueryForElasticsearchOrOpensearch(span, traceToLogsOptions, tags, customQuery);
          break;
        case 'grafana-falconlogscale-datasource':
          tags = getFormattedTags(span, tagsToUse, { joinBy: ' OR ' });
          query = getQueryForFalconLogScale(span, traceToLogsOptions, tags, customQuery);
          break;
        case 'googlecloud-logging-datasource':
          tags = getFormattedTags(span, tagsToUse, { joinBy: ' AND ' });
          query = getQueryForGoogleCloudLogging(span, traceToLogsOptions, tags, customQuery);
      }

正如您所见,我添加了两个新 case:influxdata-flightsql-datasourceinfluxdb。然后,我从 Loki 复制了 case 中的两个函数调用:getFormattedTagsgetQueryFor。我确定我可以单独保留 getFormattedTags,因为它似乎在大多数情况下都是相同的。但是,我仍然需要定义自己的 getQueryFor 函数。

让我们看一下在 influxdb case 语句中调用的新 getQueryForInfluxQL 函数

function getQueryForInfluxQL(
  span: TraceSpan,
  options: TraceToLogsOptionsV2,
  tags: string,
  customQuery?: string
): InfluxQuery | undefined {
  const { filterByTraceID, filterBySpanID } = options;

  if (customQuery) {
    return {
      refId: '',
      rawQuery: true,
      query: customQuery,
      resultFormat: 'logs',
    };
  }

  let query = 'SELECT time, "severity_text", body, attributes FROM logs WHERE time >=${__from}ms AND time <=${__to}ms';

  if (filterByTraceID && span.traceID && filterBySpanID && span.spanID) {
            query = 'SELECT time, "severity_text", body, attributes FROM logs WHERE "trace_id"=\'${__span.traceId}\' AND "span_id"=\'${__span.spanId}\' AND time >=${__from}ms AND time <=${__to}ms';
    } else if (filterByTraceID && span.traceID) {
            query = 'SELECT time, "severity_text", body, attributes FROM logs WHERE "trace_id"=\'${__span.traceId}\' AND time >=${__from}ms AND time <=${__to}ms';
    } else if (filterBySpanID && span.spanID) {
            query = 'SELECT time, "severity_text", body, attributes FROM logs WHERE "span_id"=\'${__span.spanId}\' AND time >=${__from}ms AND time <=${__to}ms';
  }

  return {
    refId: '',
    rawQuery: true,
    query: query,
    resultFormat: 'logs',
  };
}

这里有很多内容,但让我重点介绍重要部分。首先,我从 Loki 函数的精确副本开始。然后,我进行了以下更改

  1. 我将返回接口从 LokiQuery | undefined 更改为 InfluxQuery | undefined。这是我们之前导入的数据源类型。
  2. 接下来,我专注于返回有效负载。在 InfluxQuery 类型文件中进行了一些挖掘之后,我想出了这个
    return {
        refId: '',
        rawQuery: true,
        query: query,
        resultFormat: 'logs',
      };
    InfluxDB 数据源有一个 resultFormat 参数,允许我定义结果格式(通常是指标)。这也告诉我数据源期望原始查询而不是表达式。
  3. 最后,我定义了用户单击按钮时将运行的查询。这些查询取决于用户在数据源设置中切换的过滤器功能(按 traceID、spanID 或两者过滤)。我修改了 Loki 函数中定义的 if 语句,并构造了静态 InfluxQL 查询。从那里,我使用了在其他数据源中找到的 Grafana 占位符变量来使查询动态化。这是一个例子
    if (filterByTraceID && span.traceID && filterBySpanID && span.spanID) {
                query = 'SELECT time, "severity_text", body, attributes FROM logs WHERE "trace_id"=\'${__span.traceId}\' AND "span_id"=\'${__span.spanId}\' AND time >=${__from}ms AND time <=${__to}ms';
    完全公开,我花了好一会儿才弄清楚 >=${__from}ms<=${__to}ms。这最终成为一个暴力构建和错误案例。

构建

呼!我们已经过了困难的部分。现在开始构建过程。我在 Docker 方面有很多年的经验,所以这部分对我来说没有压力,但我认为对于新的 Docker 用户来说可能会令人生畏。幸运的是,Grafana 为此任务提供了一些易于遵循的 文档。为了解释清楚,这些是步骤

  1. 运行以下构建命令(这可能需要一段时间,如果使用 macOS 或 Windows,请确保您的 docker VM 有足够的内存)
    make build-docker-full
  2. 构建过程会生成一个名为:grafana/grafana-oss:dev 的 Docker 镜像。我们可以直接使用此镜像,但作为一种形式,我喜欢重新标记镜像并将其推送到我的 Docker 注册表。
    docker tag grafana/grafana-oss:dev jaymand13/grafana-oss:dev2
    docker push jaymand13/grafana-oss:dev2
    这样,我在暴力强制更改时就可以设置检查点。

我们有了!一个完全烘焙的 Grafana 开发镜像,可以尝试我们的更改。

结果和结论

因此,在调查、进行更改并构建新的 Grafana 容器之后,让我们看一下我们的结果

Logs for this span

通过我们的更改,此 span 的日志 按钮现在处于活动状态。我们还在每个 span 旁边看到了这个简洁的小日志按钮。坦白说:蓝色的 此 span 的日志 按钮目前仅在 Grafana Explorer 选项卡中有效,但新的 日志 链接在我们的仪表板中有效。

为了快速解释差异,用户构建自定义 Grafana 仪表板,并且可以包含 1 个或多个数据源以及各种不同的可视化效果。另一方面,数据浏览器提供了用于向下钻取和调查活动的界面,就像您在上面的屏幕截图中看到的那样。尽管如此,与我们为达到此目的所需的更改相比,这并不是一个大问题。

至此,我们已经结束了对修改 Grafana 源代码的复杂性的深入研究。在本教程中,我希望您不仅对如何为您的特定需求自定义 Grafana 获得了实际的了解,而且还对开源平台的灵活性和潜力有了更高的认识。

请记住,在开源领域,我们可以根据自己的需要进行调整、修改和重新构想的程度没有限制。我希望本指南能为您深入研究自己的项目提供良好的服务,并使您更接近掌握 Grafana 这个强大的工具。对我而言,我的旅程还在继续,因为我现在计划为这个 OSS 版本添加示例支持。如果您想自己尝试一下,可以在 此处 找到 OpenTelemetry 示例。