title: “如何制作一个DevLake插件?” sidebar_position: 2 description: > 如何制作一个DevLake插件?

如果你喜欢的DevOps工具还没有被DevLake支持,不要担心。实现一个DevLake插件并不困难。在这篇文章中,我们将了解DevLake插件的基础知识,并一起从头开始建立一个插件的例子。

什么是插件?

DevLake插件是用Go的plugin包构建的共享库,在运行时与DevLake核心挂钩。

一个插件可以通过三种方式扩展DevLake的能力。

  1. 与新的数据源集成
  2. 转化/丰富现有数据
  3. 将DevLake数据导出到其他数据系统

插件是如何工作的?

一个插件主要包括可以由DevLake核心执行的子任务的集合。对于数据源插件,一个子任务可能是从数据源中收集一个实体(例如,来自Jira的问题)。除了子任务,还有一些钩子,插件可以实现自定义其初始化、迁移等。最重要的接口列表见下文。

  1. PluginMeta 包含一个插件最少应该实现的接口,只有两个函数;
    • Description() 返回插件的描述
    • RootPkgPath() 返回插件的包路径。
  2. PluginInit 实现自定义的初始化方法;
  3. PluginTask 实现自定义准备数据,其在子任务之前执行;
  4. PluginApi 实现插件自定义的API;
  5. Migratable 返回插件自定义的数据库迁移的脚本。
  6. PluginModel 实现允许其他插件通过 GetTablesInfo() 的方法来获取当前插件的全部数据库表的 model 信息。(若需domain layer的 model 信息,可访问DomainLayerSchema

下图是一个插件执行的流程:

flowchart TD
    subgraph S4[Step4 Extractor 运行流程]
    direction LR
    D4[DevLake]
    D4 -- "Step4.1 创建\n ApiExtractor 并执行" --> E["ExtractXXXMeta.\nEntryPoint"];
    E <-- "Step4.2 读取raw table" --> E2["RawDataSubTaskArgs\n.Table"];
    E -- "Step4.3 解析 RawData" --> ApiExtractor.Extract
    ApiExtractor.Extract -- "返回 gorm 模型" --> E
    end
    subgraph S3[Step3 Collector 运行流程]
    direction LR
    D3[DevLake]
    D3 -- "Step3.1 创建\n ApiCollector 并执行" --> C["CollectXXXMeta.\nEntryPoint"];
    C <-- "Step3.2 创建raw table" --> C2["RawDataSubTaskArgs\n.RAW_BBB_TABLE"];
    C <-- "Step3.3 构造请求query" --> ApiCollectorArgs.\nQuery/UrlTemplate;
    C <-. "Step3.4 通过 ApiClient \n请求并返回HTTP" --> A1["HTTP APIs"];
    C <-- "Step3.5 解析\n并返回请求结果" --> ResponseParser;
    end
    subgraph S2[Step2 DevLake 的自定义插件]
    direction LR
    D2[DevLake]
    D2 <-- "Step2.1 在\`Init\` \n初始化插件" --> plugin.Init;
    D2 <-- "Step2.2 (Optional) 调用\n与返回 migration 脚本" --> plugin.MigrationScripts;
    D2 <-- "Step2.3 (Optional) \n初始化并返回taskCtx" --> plugin.PrepareTaskData;
    D2 <-- "Step2.4 返回\n 需要执行的子函数" --> plugin.SubTaskContext;
    end
    subgraph S1[Step1 DevLake 的运行]
    direction LR
    main -- "通过 \`runner.DirectRun\`\n 移交控制权" --> D1[DevLake];
    end
    S1-->S2-->S3-->S4

图中信息非常多,当然并不期望马上就能消化完,仅仅作为阅读后文的参考即可。

一起来实现一个最简单的插件

在本节中,我们将介绍如何从头创建一个数据收集插件。要收集的数据是 Apache 项目的所有 Committers 和 Contributors 信息,目的是检查其是否签署了 CLA。我们将通过:

  • 请求 https://people.apache.org/public/icla-info.json 获取 Committers 信息
  • 请求邮件列表 获取 Contributors 信息 我们将演示如何通过 Apache API 请求并缓存所有 Committers 的信息,并提取出结构化的数据。Contributors 的收集仅做一些思路的介绍。

一、 创建新的插件

**注意:**在开始之前,请确保DevLake已经能正确启动了。

关于插件的其他信息: 一般来说, 我们需要这几个目录: api, modelstasks api 实现 config-ui 等其他服务所需的api

  • connection example connection model example models 保存数据库模型和Migration脚本.

注:如果这些概念让你感到迷惑,不要担心,我们稍后会逐一解释。

DevLake 提供了专门的工具 Generator 来创建插件,可以通过运行go run generator/main.go creat-plugin icla来构建新插件,创建的时候会需要输入「是否需要默认的apiClient with_api_client」和「要收集的网站endpoint」。

  • with_api_client用于选择是否需要通过api_client发送HTTP APIs。
  • endpoint用于确认插件将请求哪个网站,在本案例中是https://people.apache.org/

现在我们的插件里有三个文件,其中api_client.gotask_data.go在子文件夹tasks/中。 1

接下来让我们试着运行plugin_main.go中的main函数来启动插件,运行结果应该如下:

$go run plugins/icla/plugin_main.go
[2022-06-02 18:07:30]  INFO failed to create dir logs: mkdir logs: file exists
press `c` to send cancel signal
[2022-06-02 18:07:30]  INFO  [icla] start plugin
invalid ICLA_TOKEN, but ignore this error now
[2022-06-02 18:07:30]  INFO  [icla] scheduler for api https://people.apache.org/ worker: 25, request: 18000, duration: 1h0m0s
[2022-06-02 18:07:30]  INFO  [icla] total step: 0

😋 没有报错,那就是成功啦~ plugin_main.go这里定义了插件,有一些配置是保存在task_data.go中。这两个文件就构成了最简单的插件,而文件api_client.go后面会用来发送HTTP APIs。

二、 创建数据收集子任务

在开始创建之前,我们需要先了解一下子任务的执行过程。

  1. Apache DevLake会调用plugin_main.PrepareTaskData(),准备一些子任务所需要的环境数据,本项任务中需要创建一个apiClient。
  2. Apache DevLake接着会调用定义在plugin_main.SubTaskMetas()的子任务,子任务都是互相独立的函数,可以用于完成注入发送API请求,处理数据等任务。

每个子任务必须在SubTaskMeta中定义,并实现其中的SubTaskEntryPoint函数,其结构为

type SubTaskEntryPoint func(c SubTaskContext) error

更多信息见:https://devlake.apache.org/blog/how-apache-devlake-runs/

注:如果这些概念让你感到迷惑,跳过跟着一步步做就好。

2.1 创建 Collector 来请求数据

同样的,运行go run generator/main.go create-collector icla committer来创建子任务。Generator运行完成后,会自动创建新的文件,并在plugin_main.go/SubTaskMetas中激活。

  • Collector将从HTTP或其他数据源收集数据,并将数据保存到rawLayer中。
  • httpCollectorSubTaskEntryPoint中,默认会使用helper.NewApiCollector来创建新的ApiCollector对象,并调用其execute()来并行收集。

注:如果这些概念让你感到迷惑,跳过就好。

现在你可以注意到在plugin_main.go/PrepareTaskData.ApiClient中有引用data.ApiClient,它是Apache DevLake推荐用于从HTTP APIs请求数据的工具。这个工具支持一些很有用的功能,比如请求限制、代理和重试。当然,如果你喜欢,也可以使用http库来代替,只示会显得更加繁琐而已。

回到正题,现在的目标是从https://people.apache.org/public/icla-info.json收集数据,因此需要完成以下步骤:

我们已经在之前中把https://people.apache.org/填入tasks/api_client.go/ENDPOINT了,现在在看一眼确认下。

  1. public/icla-info.json填入UrlTemplate,删除不必要的迭代器,并在ResponseParser中添加println("receive data:", res)以查看收集是否成功。

好了,现在Collector已经创建好了,再次运行main来启动插件,如果一切顺利的话,输出应该是这样的:

[2022-06-06 12:24:52]  INFO  [icla] start plugin
invalid ICLA_TOKEN, but ignore this error now
[2022-06-06 12:24:52]  INFO  [icla] scheduler for api https://people.apache.org/ worker: 25, request: 18000, duration: 1h0m0s
[2022-06-06 12:24:52]  INFO  [icla] total step: 1
[2022-06-06 12:24:52]  INFO  [icla] executing subtask CollectCommitter
[2022-06-06 12:24:52]  INFO  [icla] [CollectCommitter] start api collection
receive data: 0x140005763f0
[2022-06-06 12:24:55]  INFO  [icla] [CollectCommitter] finished records: 1
[2022-06-06 12:24:55]  INFO  [icla] [CollectCommitter] end api collection
[2022-06-06 12:24:55]  INFO  [icla] finished step: 1 / 1

从以上日志中,可以看到已经能打印出收到数据的日志了,最后一步是在ResponseParser中对响应体进行解码,并将其返回给DevLake,以便将其存储在数据库中。

ResponseParser: func(res *http.Response) ([]json.RawMessage, error) {
    body := &struct {
        LastUpdated string          `json:"last_updated"`
        Committers  json.RawMessage `json:"committers"`
    }{}
    err := helper.UnmarshalResponse(res, body)
    if err != nil {
        return nil, err
    }
    println("receive data:", len(body.Committers))
    return []json.RawMessage{body.Committers}, nil
},

再次运行函数main,结果如下,此时可以在数据库表_raw_icla_committer中看到一条新的数据。

……
receive data: 272956 /* <- 这个数字表示收到了272956Committer */
[2022-06-06 13:46:57]  INFO  [icla] [CollectCommitter] finished records: 1
[2022-06-06 13:46:57]  INFO  [icla] [CollectCommitter] end api collection
[2022-06-06 13:46:57]  INFO  [icla] finished step: 1 / 1

2.2 创建 Extractor,从 rawLayer 中提取数据

  • Extractor将从rawLayer中提取数据并保存到工具db表中。
  • 除了一些具体的处理内容,主流程与采集器类似。

从HTTP API收集的数据目前仅仅保存在表_raw_XXXX中,但其使用起来却很不容易。因此我们将继续从其中提取Committer的名字。目前Apache DevLake建议用gorm来保存数据,所以我们将用gorm创建一个模型,并将其添加到plugin_main.go/AutoMigrate()中。

plugins/icla/models/committer.go

package models

import (
	"github.com/apache/incubator-devlake/models/common"
)

type IclaCommitter struct {
	UserName     string `gorm:"primaryKey;type:varchar(255)"`
	Name         string `gorm:"primaryKey;type:varchar(255)"`
	common.NoPKModel
}

func (IclaCommitter) TableName() string {
	return "_tool_icla_committer"
}

plugins/icla/plugin_main.go

在做完以上步骤以后,就可以再次运行插件,刚定义的数据表_tool_icla_committer会自动创建,就像下面的截图。

接下来,让我们运行go run generator/main.go create-extractor icla committer并输入命令行提示的内容,来创建新的子任务。

运行完成后,来看看刚才创建的committer_extractor.go中的函数extract,很明显参数中的resData.data是原始数据,我们需要用json解码,并创建IclaCommitter模型来保存它们。

Extract: func(resData *helper.RawData) ([]interface{}, error) {
    names := &map[string]string{}
    err := json.Unmarshal(resData.Data, names)
    if err != nil {
        return nil, err
    }
    extractedModels := make([]interface{}, 0)
    for userName, name := range *names {
        extractedModels = append(extractedModels, &models.IclaCommitter{
            UserName: userName,
            Name:     name,
        })fco
    }
    return extractedModels, nil
},

再次运行插件,结果如下:

[2022-06-06 15:39:40]  INFO  [icla] start plugin
invalid ICLA_TOKEN, but ignore this error now
[2022-06-06 15:39:40]  INFO  [icla] scheduler for api https://people.apache.org/ worker: 25, request: 18000, duration: 1h0m0s
[2022-06-06 15:39:40]  INFO  [icla] total step: 2
[2022-06-06 15:39:40]  INFO  [icla] executing subtask CollectCommitter
[2022-06-06 15:39:40]  INFO  [icla] [CollectCommitter] start api collection
receive data: 272956
[2022-06-06 15:39:44]  INFO  [icla] [CollectCommitter] finished records: 1
[2022-06-06 15:39:44]  INFO  [icla] [CollectCommitter] end api collection
[2022-06-06 15:39:44]  INFO  [icla] finished step: 1 / 2
[2022-06-06 15:39:44]  INFO  [icla] executing subtask ExtractCommitter
[2022-06-06 15:39:46]  INFO  [icla] [ExtractCommitter] finished records: 1
[2022-06-06 15:39:46]  INFO  [icla] finished step: 2 / 2

可以看到有两个任务运行完成,同时观察数据库发现,提交者的数据已经保存在_tool_icla_committer中了~

2.3 子任务 - Converter

注意。这里有两种方式(开源或自己使用)。因此 Converter 不是必须的,但我们鼓励使用它,因为 Converter 和 DomainLayer 非常有助于建立通用的仪表盘。关于 DomainLayer 的更多信息请见:https://devlake.apache.org/docs/DataModels/DevLakeDomainLayerSchema/

  • Converter 将处理 DomainLayer 的数据,并将其保存到 DomainLayer 层中。
  • 使用helper.NewDataConverter来创建一个 DataConvertor 的对象,然后调用execute()来运行。

2.4 动手试试更多类型的请求吧~

有时 OpenApi 会受到 token 或其他保护,只有获得 token 才来能访问。例如在本案例中,我们只有在登录private@apahce.com后,才能收集到关于普通 Contributor 签署ICLA的数据。但这里受限于篇幅,仅仅简单介绍一下如何收集需要授权的数据。

让我们注意api_client.go文件,其中NewIclaApiClient通过.env加载配置了ICLA_TOKEN,它让我们可以在.env中添加ICLA_TOKEN=XXXX,并在apiClient.SetHeaders()中使用它来模拟登录状态。代码如下。

当然,我们可以使用username/password来获取模拟登录后的token,试着根据实际情况进行调整即可。

更多相关细节请看https://github.com/apache/incubator-devlake

Step 2.5 实现 PluginModel 接口的 GetTablesInfo() 方法

如下gitlab插件示例所示 将所有需要被外部插件访问到的 model 均添加到返回值中。

var _ core.PluginModel = (*Gitlab)(nil)

func (plugin Gitlab) GetTablesInfo() []core.Tabler {
	return []core.Tabler{
		&models.GitlabConnection{},
		&models.GitlabAccount{},
		&models.GitlabCommit{},
		&models.GitlabIssue{},
		&models.GitlabIssueLabel{},
		&models.GitlabJob{},
		&models.GitlabMergeRequest{},
		&models.GitlabMrComment{},
		&models.GitlabMrCommit{},
		&models.GitlabMrLabel{},
		&models.GitlabMrNote{},
		&models.GitlabPipeline{},
		&models.GitlabProject{},
		&models.GitlabProjectCommit{},
		&models.GitlabReviewer{},
		&models.GitlabTag{},
	}
}

可以使用如下方式来使用该接口

if pm, ok := plugin.(core.PluginModel); ok {
    tables := pm.GetTablesInfo()
    for _, table := range tables {
        // do something
    }
}

2.6 将插件提交给开源社区

恭喜你! 第一个插件已经创建完毕! 🎖 我们鼓励开源贡献~ 接下来还需要学习 migrationScripts 和 domainLayers 来编写规范的、平台无关的代码。更多信息请访问https://devlake.apache.org/docs/DataModels/DevLakeDomainLayerSchema,或联系我们以获得热情洋溢的帮助。

come on