教程:修改 Grafana 的源代码

导航至

本文最初发布在 dev.to,现经授权在此重发。

探索与猜测的故事

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

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

在项目过程中,我们确定了需要启用特定的 Grafana 功能以支持 InfluxDB 数据源,尤其是跟踪到日志的功能。Grafana 是一个开源平台,其最大的优点之一就是能够修改其源代码以适应我们的独特需求。然而,深入研究这样一个强大工具的代码库可能会让人感到不知所措,即使是经验丰富的开发者也不例外。

尽管复杂,但我们接受了挑战,一头扎进了 Grafana 的源代码。我们跌跌撞撞,一路学习,现在我们已经成功修改了 Grafana 以满足我们的特定项目需求,我认为现在是时候将我们所学到的知识分享给大家了。

本博客的目的不仅仅是提供修改 Grafana 源代码的逐步指南,还希望能够激发您探索和适应开源项目以满足您的需求。这关乎传授方法和心态,培养好奇心文化,并鼓励更多动手学习和解决问题的实践。

我希望这篇指南能够激发您修改 Grafana 源代码以满足您项目的需要,从而拓宽开源平台可能性的视野。是时候卷起袖子,深入 Grafana 的代码深处了。

问题

我们的问题出在 Grafana 的 跟踪可视化 上。

Trace visualization of Grafana

如您所见,该可视化与 InfluxDB 的表现相当好,除了一个禁用的按钮: 此跟踪的日志。如果我们不配置与跟踪数据源(在此案例中,Jaeger 与 InfluxDB 3.0 作为 gRPC 存储引擎)的日志数据源,那么 Grafana 会自动禁用此按钮。Grafana 通常默认使用 日志探索界面 来表示日志数据源。常见的日志数据源包括 LokiOpenSearchElasticsearch。所以让我们转到 Jaeger 数据源并配置它……

Connections-data source

您可以通过“连接 -> 数据源”来导航数据源。我们目前有三个配置好的数据源:FlightSQL、InfluxDB和Jaeger。如果我们打开Jaeger配置并导航到“跟踪到日志”部分,我们希望能够选择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
  ];

这段代码似乎创建了一个静态列表,其中包含由跟踪到日志功能支持的数据源。我们可以通过列表中的某些常见嫌疑人(Loki、Elasticsearch等)来确认这一点。基于这个发现,我们对Grafana源代码的第一个修改就是将我们的数据源添加到这个列表中。

现在,作为一个编码悲观主义者,我知道我们可能需要进行的更改不止这些,但这是一个好的开始。所以我做了以下事情

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

在我做出那些修改之前,我想再做一些搜索,看看是否应该做出其他更改。在< strong>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]
  );

它是< strong>TraceToLogsOptionsV2。当我搜索Grafana使用此接口的地方时,我找到了以下条目。

TraceToLogsOptionsV2

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

case statement

简而言之,case语句本质上是告诉跟踪可视化检查定义的日志数据源(如果有),并定义一个与该数据源相关的查询界面。如果指定的数据源没有在这个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

嘿,结果出来了!现在,这还没有在跟踪视图中启用按钮,但我们已经知道这需要更多的工作。

现在,让我们继续进行我们修改的核心部分。记录在案,我并不是 TypeScript 开发者。我所知道的是,这个文件有一大堆我们可以用来尝试盲目的复制粘贴并做些修改的示例。我最终为这两个插件都做了这件事,但是为了使博客更简短,我们将重点关注 InfluxDB 官方插件。

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

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

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

下一步是修改情况语句

// 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);
      }

如您所见,我添加了两个新的情况:influxdata-flightsql-datasourceinfluxdb。然后,我从 Loki 中复制了两个函数调用:getFormattedTagsgetQueryFor。我确定我可以不修改 getFormattedTags,因为它在大多数情况下看起来是相同的。然而,我仍然需要定义自己的 getQueryFor 函数。

让我们来看看在 influxdb 情况语句中调用的新 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

经过我们的修改,现在“此跨度日志”按钮已启用。我们还有一个这个小巧的日志按钮,它出现在每个跨度旁边。坦白说,当前蓝色的“此跨度日志”按钮只能在Grafana资源管理器标签页中工作,但新的“日志”链接可以在我们的仪表板上工作。

为了快速说明差异,用户可以构建自定义的Grafana仪表板,并可以包含1个或多个数据源,以及各种不同的可视化。另一方面,数据探索器提供了一个界面,用于执行类似上述截图中的钻取和调查活动。尽管如此,与我们需要进行的改动相比,这并不是一个大问题。

因此,我们已经到达了深入研究修改Grafana源代码复杂性的终点。在本教程的过程中,我希望您不仅对如何根据具体需求自定义Grafana有了实际的理解,而且也对开源平台的一般灵活性和潜力有了认识。

记住,在开源领域,我们可以根据需要调整、调整和重新想象,没有限制。我希望这份指南能帮助您在深入自己的项目中,并让您离掌握Grafana这个强大的工具更近一步。对我来说,我的旅程仍在继续,我现在计划为这个开源构建添加示例支持。如果您想亲自尝试,可以在这里找到OpenTelemetry示例[https://github.com/InfluxCommunity/opentelemetry-demo]。