加载中

如何编写 Java 编解码插件

注意

Java 编解码器目前仅支持 Java 输入和输出插件。它们不适用于 Ruby 输入或输出插件。

要为 Logstash 开发新的 Java 编解码器,您需要编写一个符合 Logstash Java Codecs API 的新 Java 类,进行打包,然后使用 logstash-plugin 工具进行安装。我们将逐步介绍这些步骤。

首先,复制 示例编解码器插件。该插件 API 目前是 Logstash 代码库的一部分,因此您必须有一个本地副本。您可以使用以下 git 命令获取 Logstash 代码库的副本

git clone --branch <branch_name> --single-branch https://github.com/elastic/logstash.git <target_folder>

branch_name 应对应包含 Java 插件 API 首选修订版本的 Logstash 版本。

注意

Java 插件 API 的 GA 版本可在 Logstash 代码库的 7.2 及更高版本的分支中找到。

为 Logstash 代码库的本地副本指定 target_folder。如果您不指定 target_folder,它将默认在当前文件夹下创建一个名为 logstash 的新文件夹。

在获取了适当修订版本的 Logstash 代码库副本后,您需要对其进行编译以生成包含 Java 插件 API 的 .jar 文件。从 Logstash 代码库的根目录($LS_HOME)开始,您可以使用 ./gradlew assemble(如果您在 Windows 上运行,则为 gradlew.bat assemble)进行编译。这将生成 $LS_HOME/logstash-core/build/libs/logstash-core-x.y.z.jar 文件,其中 xyz 代表 Logstash 的版本。

成功编译 Logstash 后,您需要告知 Java 插件在哪里可以找到 logstash-core-x.y.z.jar 文件。在插件项目的根文件夹中创建一个名为 gradle.properties 的新文件。该文件应包含单行:

LOGSTASH_CORE_PATH=<target_folder>/logstash-core

其中 target_folder 是 Logstash 代码库本地副本的根文件夹。

示例编解码器插件通过可配置的分隔符来解码消息,并将其消息的字符串表示形式用分隔符分隔来编码消息。例如,如果编解码器配置的分隔符为 /,则输入文本 event1/event2/ 将被解码为两个单独的事件,其 message 字段分别为 event1event2。请注意,这只是一个示例编解码器,并未涵盖生产级编解码器应涵盖的所有边缘情况。

让我们看一下该编解码器过滤器中的主类

@LogstashPlugin(name="java_codec_example")
public class JavaCodecExample implements Codec {

    public static final PluginConfigSpec<String> DELIMITER_CONFIG =
            PluginConfigSpec.stringSetting("delimiter", ",");

    private final String id;
    private final String delimiter;

    public JavaCodecExample(final Configuration config, final Context context) {
        this(config.get(DELIMITER_CONFIG));
    }

    private JavaCodecExample(String delimiter) {
        this.id = UUID.randomUUID().toString();
        this.delimiter = delimiter;
    }

    @Override
    public void decode(ByteBuffer byteBuffer, Consumer<Map<String, Object>> consumer) {
        // a not-production-grade delimiter decoder
        byte[] byteInput = new byte[byteBuffer.remaining()];
        byteBuffer.get(byteInput);
        if (byteInput.length > 0) {
            String input = new String(byteInput);
            String[] split = input.split(delimiter);
            for (String s : split) {
                Map<String, Object> map = new HashMap<>();
                map.put("message", s);
                consumer.accept(map);
            }
        }
    }

    @Override
    public void flush(ByteBuffer byteBuffer, Consumer<Map<String, Object>> consumer) {
        // if the codec maintains any internal state such as partially-decoded input, this
        // method should flush that state along with any additional input supplied in
        // the ByteBuffer

        decode(byteBuffer, consumer);
    }

    @Override
    public void encode(Event event, OutputStream outputStream) throws IOException {
        outputStream.write((event.toString() + delimiter).getBytes(Charset.defaultCharset()));
    }

    @Override
    public Collection<PluginConfigSpec<?>> configSchema() {
        // should return a list of all configuration options for this plugin
        return Collections.singletonList(DELIMITER_CONFIG);
    }

    @Override
    public Codec cloneCodec() {
        return new JavaCodecExample(this.delimiter);
    }

    @Override
    public String getId() {
        return this.id;
    }

}
  1. 这是一个简化的实现

让我们逐步检查并分析该类的每个部分。

@LogstashPlugin(name="java_codec_example")
public class JavaCodecExample implements Codec {

关于类声明的注释

  • 所有 Java 插件都必须用 @LogstashPlugin 注解。此外:

    • 必须提供注解的 name 属性,它定义了插件的名称,该名称将在 Logstash 管道定义中使用。例如,此编解码器将在 Logstash 管道定义中相应输入或输出的编解码器部分中引用为 codec => java_codec_example { }
    • name 属性的值必须与类名匹配,忽略大小写和下划线。
  • 该类必须实现 co.elastic.logstash.api.Codec 接口。

  • Java 插件不能在 org.logstashco.elastic.logstash 包中创建,以防止与 Logstash 本身的类发生潜在冲突。

下面的代码片段包含设置定义和引用它的方法

public static final PluginConfigSpec<String> DELIMITER_CONFIG =
        PluginConfigSpec.stringSetting("delimiter", ",");

@Override
public Collection<PluginConfigSpec<?>> configSchema() {
    return Collections.singletonList(DELIMITER_CONFIG);
}

PluginConfigSpec 类允许开发人员指定插件支持的设置,包括设置名称、数据类型、弃用状态、必需状态和默认值。在此示例中,delimiter 设置定义了编解码器将用于分割事件的分隔符。它不是一个必需的设置,如果未显式设置,则其默认值为 ,

configSchema 方法必须返回插件支持的所有设置的列表。Logstash 执行引擎将验证所有必需的设置都存在,并且不存在不支持的设置。

private final String id;
private final String delimiter;

public JavaCodecExample(final Configuration config, final Context context) {
    this(config.get(DELIMITER_CONFIG));
}

private JavaCodecExample(String delimiter) {
    this.id = UUID.randomUUID().toString();
    this.delimiter = delimiter;
}

所有 Java 编解码器插件都必须有一个接受 ConfigurationContext 参数的构造函数。这将是运行时实例化它们的构造函数。所有插件设置的检索和验证都应在此构造函数中进行。在此示例中,用于分隔事件的分隔符从其设置中检索并存储在本地变量中,以便稍后在 decodeencode 方法中使用。编解码器的 ID 被初始化为一个随机 UUID(大多数编解码器都应这样做),并且本地 encoder 变量被初始化为使用指定的字符集进行编码和解码。

任何其他初始化也可以在此构造函数中进行。如果在编解码器插件的配置或初始化过程中遇到任何无法恢复的错误,则应抛出描述性异常。该异常将被记录下来,并阻止 Logstash 启动。

@Override
public void decode(ByteBuffer byteBuffer, Consumer<Map<String, Object>> consumer) {
    // a not-production-grade delimiter decoder
    byte[] byteInput = new byte[byteBuffer.remaining()];
    byteBuffer.get(byteInput);
    if (byteInput.length > 0) {
        String input = new String(byteInput);
        String[] split = input.split(delimiter);
        for (String s : split) {
            Map<String, Object> map = new HashMap<>();
            map.put("message", s);
            consumer.accept(map);
        }
    }
}

@Override
public void flush(ByteBuffer byteBuffer, Consumer<Map<String, Object>> consumer) {
    // if the codec maintains any internal state such as partially-decoded input, this
    // method should flush that state along with any additional input supplied in
    // the ByteBuffer

    decode(byteBuffer, consumer);
}

@Override
public void encode(Event event, OutputStream outputStream) throws IOException {
    outputStream.write((event.toString() + delimiter).getBytes(Charset.defaultCharset()));
}
  1. 这是一个简化的实现

decodeflushencode 方法提供了编解码器的核心功能。编解码器可以由输入插件用于将字节序列或流解码为事件,也可以由输出插件用于将事件编码为字节序列。

decode 方法从指定的 ByteBuffer 解码事件,并将它们传递给提供的 Consumer。输入必须提供一个已准备好读取的 ByteBuffer,其中 byteBuffer.position() 指示下一个读取位置,byteBuffer.limit() 指示缓冲区中第一个不安全读取的字节。编解码器必须确保在将控制权返回给输入之前,byteBuffer.position() 反映最后一个读取的位置。然后,输入负责通过 byteBuffer.clear()byteBuffer.compact() 将缓冲区返回到写入模式,然后再恢复写入。在上例中,decode 方法简单地根据指定的分隔符拆分传入的字节流。像 java-line 这样的生产级编解码器不会做出 supplied byte stream 的末尾对应于事件结束的简化假设。

事件应构造为 Map<String, Object> 实例,并通过 Consumer<Map<String, Object>>.accept() 方法推送到事件管道。为了减少分配和 GC 压力,编解码器可以通过在调用 Consumer<Map<String, Object>>.accept() 之间修改其字段来重用同一个 map 实例,因为事件管道将根据 map 数据的副本创建事件。

flush 方法与 decode 方法协同工作,以解码指定的 ByteBuffer 中的所有剩余事件以及在之前调用 decode 方法后可能保留的任何内部状态。作为编解码器可能维护的内部状态的一个例子,请考虑一个字节输入流 event1/event2/event3,其分隔符为 /。由于缓冲或其他原因,输入可能会向编解码器的 decode 方法提供一个不完整的字节流,例如 event1/eve。在这种情况下,编解码器可以保存第二个事件的前三个字符 eve,而不是假设提供的字节流在事件边界处结束。如果下次调用 decode 提供了 nt2/ev 字节,编解码器将把保存的 eve 字节前置以生成完整的 event2 事件,然后保存剩余的 ev 字节,以便在提供该事件的其余字节时进行解码。调用 flush 会向编解码器发出信号,表明提供的字节代表事件流的结束,并且所有剩余的字节都应解码为事件。上面的 flush 示例是一个简化的实现,它在调用 decode 之间不维护关于部分提供的字节流的任何状态。

encode 方法将事件编码为字节序列,并将其写入指定的 OutputStream。由于单个编解码器实例在 Logstash 管道的输出阶段被所有管道工作进程共享,因此编解码器在调用其 encode 方法时不应保留状态。

@Override
public Codec cloneCodec() {
    return new JavaCodecExample(this.delimiter);
}

cloneCodec 方法应返回编解码器的一个相同实例,但 ID 除外。由于编解码器可能在调用 decode 方法之间保持状态,因此多线程的输入插件应为每个线程通过 cloneCodec 方法使用一个单独的编解码器实例。由于单个编解码器实例在 Logstash 管道的输出阶段被所有管道工作进程共享,因此编解码器在调用其 encode 方法时不应保留状态。在上例中,编解码器被克隆,使用相同分隔符但 ID 不同。

@Override
public String getId() {
    return id;
}

对于编解码器插件,getId 方法应始终返回在实例化时设置的 ID。这通常是 UUID。

最后,但同样重要的是,强烈鼓励编写单元测试。示例编解码器插件包含一个 示例单元测试,您可以将其用作您自己测试的模板。

Java 插件被打包成 Ruby gems 以进行依赖管理和与 Ruby 插件的互操作性。一旦它们被打包成 gem,就可以像 Ruby 插件一样使用 logstash-plugin 工具进行安装。由于 Java 插件开发不应要求了解 Ruby 或其工具链,因此将 Java 插件打包成 Ruby gem 的过程已通过示例 Java 插件提供的 Gradle 构建文件中的自定义任务进行了自动化。以下各节将介绍如何配置和执行该打包任务,以及如何在 Logstash 中安装打包好的 Java 插件。

以下部分出现在示例 Java 插件附带的 build.gradle 文件的顶部附近:

// ===========================================================================
// plugin info
// ===========================================================================
group                      'org.logstashplugins'
version                    "${file("VERSION").text.trim()}"
description                = "Example Java filter implementation"
pluginInfo.licenses        = ['Apache-2.0']
pluginInfo.longDescription = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using \$LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
pluginInfo.authors         = ['Elasticsearch']
pluginInfo.email           = ['info@elastic.co']
pluginInfo.homepage        = "https://elastic.ac.cn/guide/en/logstash/current/index.html"
pluginInfo.pluginType      = "filter"
pluginInfo.pluginClass     = "JavaFilterExample"
pluginInfo.pluginName      = "java_filter_example"
// ===========================================================================
  1. 必须匹配主插件类的包
  2. 从必需的 VERSION 文件读取
  3. SPDX 许可证 ID 列表

您应该为您的插件配置以上值。

  • version 值将自动从插件代码库根目录的 VERSION 文件中读取。
  • pluginInfo.pluginType 应设置为 inputfiltercodecoutput 之一。
  • pluginInfo.pluginName 必须与主插件类上的 @LogstashPlugin 注解中指定的名称匹配。Gradle 打包任务将验证这一点,并在名称不匹配时返回错误。

将插件打包成 Ruby gem 需要几个 Ruby 源文件以及一个 gemspec 文件和一个 Gemfile。这些 Ruby 文件仅用于定义 Ruby gem 结构或在 Logstash 启动时注册 Java 插件。它们在运行时事件处理过程中不使用。Gradle 打包任务会根据上面部分配置的值自动生成所有这些文件。

您可以使用以下命令运行 Gradle 打包任务:

./gradlew gem

对于 Windows 平台:根据需要将命令中的 ./gradlew 替换为 gradlew.bat

该任务将在您的插件代码库的根目录中生成一个 gem 文件,其名称为 logstash-{{plugintype}}-<pluginName>-<version>.gem

将 Java 插件打包成 Ruby gem 后,您可以使用此命令在 Logstash 中安装它:

bin/logstash-plugin install --no-verify --local /path/to/javaPlugin.gem

对于 Windows 平台:根据需要用反斜杠替换命令中的正斜杠。

要测试该插件,请使用以下命令启动 Logstash

echo "foo,bar" | bin/logstash -e 'input { java_stdin { codec => java_codec_example } }'

使用上述配置时,预期的 Logstash 输出(不包括初始化)如下:

{
      "@version" => "1",
       "message" => "foo",
    "@timestamp" => yyyy-MM-ddThh:mm:ss.SSSZ,
          "host" => "<yourHostName>"
}
{
      "@version" => "1",
       "message" => "bar\n",
    "@timestamp" => yyyy-MM-ddThh:mm:ss.SSSZ,
          "host" => "<yourHostName>"
}

如果您对 Logstash 中的 Java 插件支持有任何反馈,请在我们的 主 Github 问题 上发表评论,或在 Logstash 论坛上发帖。

© . This site is unofficial and not affiliated with Elasticsearch BV.