fluent-plugin-opentelemetry 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7caa8c736a332ab38da184793751d6760bc1d8bf59c996bffcbe324dd5da7ab5
4
- data.tar.gz: d4c07bffd3ccdd29e8b151a134e5bbe77e1686d8e0ad9a84e5ecd432224d1dd1
3
+ metadata.gz: dad225b751029746a86f4bdeae0b9f415d0756c25d9c560e7606ac11c13ade0b
4
+ data.tar.gz: '091d4d6113b42822cc47e6cd386aa10e2044f9a146d401e437eefe21b11df10e'
5
5
  SHA512:
6
- metadata.gz: 48d509ee79d59085508bd591c051d7ba8cc33efc3e0cb1d64e8ad04f28ed60444fdd4f322e65d4e41ffc84c3d0e7c7770bf7f3cfe1d1076df692ba5dfb8dddcc
7
- data.tar.gz: 9c21c80065796cdadc5fb8aad7c4c1b810cff27a1581d5e83a21d4de920ff34f9318d52f39337c599da8148bbbb6f4f1ac6e365459373079d906cd6812397c77
6
+ metadata.gz: d5919a65f7d4adf3491c16cc3a098283bb69f7fd84a09e88730d15df7919c878838eee465d3315f5f762acd323f2a0983c2445f74b36384526ea1a1dc2e04f51
7
+ data.tar.gz: fe3517def463d5c8ae2c559df0e42438d518ae4d74f0ccfc6e900c570ca74b1cca2d1b31a97da33e79d566ba304884c5646f13a46869d3ea8147ba209143d1c9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.4] - 2026-09-04
4
+
5
+ Fixes:
6
+
7
+ - Apply <transport tls> ca_path to HTTP connections (#55)
8
+ - Apply <transport tls> settings to gRPC connections (#54)
9
+ - out_opentelemetry: skip invalid records instead of failing the chunk (#53)
10
+
11
+ ## [0.5.3] - 2026-06-25
12
+
13
+ Fixes:
14
+
15
+ - in_opentelemetry: enforce size limits on incoming payloads
16
+
3
17
  ## [0.5.2] - 2026-03-15
4
18
 
5
19
  Improvements:
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # fluent-plugin-opentelemetry
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/fluent-plugin-opentelemetry.svg)](https://badge.fury.io/rb/fluent-plugin-opentelemetry)
4
+ [![Test](https://github.com/fluent-plugins-nursery/fluent-plugin-opentelemetry/actions/workflows/test.yml/badge.svg)](https://github.com/fluent-plugins-nursery/fluent-plugin-opentelemetry/actions/workflows/test.yml)
5
+
3
6
  [Fluentd](https://fluentd.org/) input/output plugin to forward [OpenTelemetry Protocol](https://github.com/open-telemetry/opentelemetry-proto) data.
4
7
 
5
8
  ## Installation
@@ -65,10 +68,12 @@ Example:
65
68
 
66
69
  This requires to receive data via HTTP/HTTPS.
67
70
 
68
- | parameter | type | description | default |
69
- |-----------|---------|------------------------|-----------|
70
- | bind | string | The address to bind to | `0.0.0.0` |
71
- | port | integer | The port to listen to | `4318` |
71
+ | parameter | type | description | default |
72
+ |--------------------------|---------|---------------------------------------------------------------------------------------------------------------------------|-----------|
73
+ | bind | string | The address to bind to | `0.0.0.0` |
74
+ | port | integer | The port to listen to | `4318` |
75
+ | body_size_limit | size | The size limit of the POSTed element. This value should be larger than the 'chunk_limit_size' in out_opentelemetry plugin | `32M` (32MiB) |
76
+ | decompression_size_limit | size | The size limit of the decompressed element | `256M` (256MiB) |
72
77
 
73
78
  #### `<grpc>` section
74
79
 
@@ -87,6 +92,11 @@ It needs to install `grpc` gem manually to use this feature.
87
92
 
88
93
  Refer [Config: Transport Section](https://docs.fluentd.org/configuration/transport-section)
89
94
 
95
+ When the `<grpc>` section is configured, `<transport tls>` applies to the gRPC listener as well. gRPC uses `ca_path`, `cert_path`, `private_key_path`, `private_key_passphrase` and `client_cert_auth`, and the other TLS parameters have no effect on the gRPC listener. `client_cert_auth` requires `ca_path`, because gRPC has no default trust store for client certificates.
96
+
97
+ > [!WARNING]
98
+ > This changes the behavior of an existing configuration that has both `<transport tls>` and `<grpc>`. The gRPC listener used to accept plaintext regardless of `<transport tls>`, and it now requires TLS. Plaintext OTLP/gRPC clients have to be switched to TLS.
99
+
90
100
  #### Example
91
101
 
92
102
  ```
@@ -144,6 +154,19 @@ To output the data, it requires to use output `opentelemetry` plugin.
144
154
  >
145
155
  > **PromQL Example:** `rate(fluentd_process_cpu_time_seconds_total[$__rate_interval])`
146
156
 
157
+ > [!NOTE]
158
+ > **Process Metrics (CPU / Memory)**
159
+ >
160
+ > Fluentd operates on a multi-process architecture consisting of a Supervisor process and one or more Worker processes.
161
+ >
162
+ > Because this plugin runs inside a specific worker process, the process-level metrics it collects (such as CPU time and memory usage) represent **only the footprint of that single worker process**, not the total resource consumption of the entire Fluentd instance.
163
+ >
164
+ > If you are using a multi-worker configuration (`<system> workers N </system>`), be aware that the actual total CPU and memory usage of Fluentd will be higher than what this plugin reports.
165
+ >
166
+ > For accurate, instance-wide resource monitoring, we strongly recommend using external OS-level monitoring tools rather than relying on application-internal metrics:
167
+ > * Container Environments (Kubernetes/Docker): Use tools like `cAdvisor` or `kube-state-metrics`, etc.
168
+ > * VM/Bare-metal Environments: Use `node_exporter` combined with `process-exporter`, etc.
169
+
147
170
  ### Output `opentelemetry` plugin
148
171
 
149
172
  To send data, this plugin requires `<http>` or `<grpc>` section.
@@ -189,9 +212,11 @@ It needs to install `grpc` gem manually to use this feature.
189
212
 
190
213
  | parameter | type | description | default |
191
214
  |------------------------|---------|------------------------------------------------|---------|
215
+ | ca_path | string | Specifies the path of CA Certificate file used to verify the server | `nil` |
192
216
  | cert_path | string | Specifies the path of Certificate file | `nil` |
193
217
  | private_key_path | string | Specifies the path of Private Key file | `nil` |
194
218
  | private_key_passphrase | string | Specifies the public CA private key passphrase | `nil` |
219
+ | insecure | bool | Skips the verification of the server certificate | `false` |
195
220
 
196
221
  | parameter | type | description | available values | default |
197
222
  |-------------|------|-------------------------------------------------------------|--------------------------------|---------|
@@ -200,6 +225,18 @@ It needs to install `grpc` gem manually to use this feature.
200
225
 
201
226
  Refer [Config: Transport Section](https://docs.fluentd.org/configuration/transport-section)
202
227
 
228
+ The HTTP connection uses `ca_path`, `cert_path`, `private_key_path`, `private_key_passphrase`, `min_version`, `max_version` and `insecure`, and the other TLS parameters have no effect on it. `ca_path` is added to the default trusted CAs of the system instead of replacing them.
229
+
230
+ Fluentd rejects a `<transport tls>` section that configures no certificate at all, so pair `ca_path` with `insecure true` to verify the server without presenting a client certificate. On the HTTP connection, `insecure true` skips the verification only when `ca_path` is omitted.
231
+
232
+ > [!WARNING]
233
+ > This changes the behavior of an existing configuration that has both `ca_path` and `insecure true`. The HTTP connection used to skip the verification, and it now verifies the server certificate and its hostname. Drop `ca_path` to keep the previous behavior.
234
+
235
+ When the `<grpc>` section is configured, `<transport tls>` applies to the gRPC connection as well. gRPC uses `ca_path`, `cert_path`, `private_key_path` and `private_key_passphrase`, and the other TLS parameters have no effect on the gRPC connection. `insecure` does not weaken the gRPC connection: the server certificate is verified against `ca_path`, or against the default root certificates of gRPC when `ca_path` is omitted.
236
+
237
+ > [!WARNING]
238
+ > This changes the behavior of an existing configuration that has both `<transport tls>` and `<grpc>`. The gRPC connection used to be plaintext regardless of `<transport tls>`, and it now uses TLS, so the collector has to accept it.
239
+
203
240
  #### `<buffer>` section
204
241
 
205
242
  | parameter | type | description | default |
@@ -231,4 +268,3 @@ Refer [Config: Buffer Section](https://docs.fluentd.org/configuration/buffer-sec
231
268
  * Copyright(c) 2025- Shizuo Fujita
232
269
  * License
233
270
  * Apache License, Version 2.0
234
-
data/TODO.md CHANGED
@@ -8,10 +8,13 @@ Ref. https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specif
8
8
  - [x] [MUST] Gzip compression, denoted by gzip.
9
9
 
10
10
  ### OTLP/gRPC Concurrent Requests
11
- - [ ] [SHOULD] The implementations that need to achieve high throughput SHOULD support concurrent Unary calls to achieve higher throughput.
11
+ - [x] [SHOULD] The implementations that need to achieve high throughput SHOULD support concurrent Unary calls to achieve higher throughput.
12
+ * NOTE: Supported by `flush_thread_count` settings in `<buffer>`.
12
13
  - [ ] [SHOULD] The client SHOULD send new requests without waiting for the response to the earlier sent requests, essentially creating a pipeline of requests that are currently in flight that are not acknowledged.
13
- - [ ] [SHOULD] The number of concurrent requests SHOULD be configurable.
14
- - [ ] [SHOULD] The client implementation SHOULD expose an option to turn on and off the waiting during a shutdown.
14
+ - [x] [SHOULD] The number of concurrent requests SHOULD be configurable.
15
+ * NOTE: Supported by `flush_thread_count` settings in `<buffer>`.
16
+ - [x] [SHOULD] The client implementation SHOULD expose an option to turn on and off the waiting during a shutdown.
17
+ * NOTE: Supported by `flush_at_shutdown` settings in `<buffer>`.
15
18
 
16
19
  ### OTLP/gRPC Response
17
20
  - Full Success
@@ -30,7 +33,8 @@ Ref. https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specif
30
33
  - [ ] [SHOULD] Retryable errors indicate that telemetry data processing failed, and the client SHOULD record the error and may retry exporting the same data.
31
34
  - [ ] [SHOULD] The client SHOULD maintain a counter of such dropped data.
32
35
  - [ ] [SHOULD] The client SHOULD interpret gRPC status codes as retryable or not-retryable according to the following table.
33
- - [ ] [SHOULD] When retrying, the client SHOULD implement an exponential backoff strategy.
36
+ - [x] [SHOULD] When retrying, the client SHOULD implement an exponential backoff strategy.
37
+ * NOTE: Fluentd's retry mechanism has `retry_type: exponential_backoff` by default.
34
38
  - [ ] [SHOULD] The client SHOULD interpret RESOURCE_EXHAUSTED code as retryable only if the server signals that the recovery from resource exhaustion is possible.
35
39
 
36
40
  ### OTLP/gRPC Throttling
@@ -91,18 +95,21 @@ Ref. https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specif
91
95
  - OTLP/HTTP Throttling
92
96
  - [ ] [SHOULD] If the server receives more requests than the client is allowed or the server is overloaded, the server SHOULD respond with HTTP 429 Too Many Requests or HTTP 503 Service Unavailable.
93
97
  - [ ] [SHOULD] The client SHOULD honour the waiting interval specified in the "Retry-After" header if it is present.
94
- - [ ] [SHOULD] The "Retry-After" header is not present in the response, then the client SHOULD implement an exponential backoff strategy between retries.
98
+ - [x] [SHOULD] The "Retry-After" header is not present in the response, then the client SHOULD implement an exponential backoff strategy between retries.
99
+ * NOTE: Fluentd's retry mechanism has `retry_type: exponential_backoff` by default.
95
100
  - All Other Responses
96
101
  - [x] [SHOULD] If the server disconnects without returning a response, the client SHOULD retry and send the same request.
97
102
  - [x] [SHOULD] The client SHOULD implement an exponential backoff strategy between retries to avoid overwhelming the server.
98
103
 
99
104
  ### OTLP/HTTP Connection
100
- - [ ] [SHOULD] If the client cannot connect to the server, the client SHOULD retry the connection using an exponential backoff strategy between retries.
101
- - [ ] [SHOULD] The client SHOULD keep the connection alive between requests.
105
+ - [x] [SHOULD] If the client cannot connect to the server, the client SHOULD retry the connection using an exponential backoff strategy between retries.
106
+ * NOTE: Fluentd's retry mechanism has `retry_type: exponential_backoff` by default.
107
+ - [x] [SHOULD] The client SHOULD keep the connection alive between requests.
102
108
  - [ ] [SHOULD] Server implementations SHOULD accept OTLP/HTTP with binary-encoded Protobuf payload and OTLP/HTTP with JSON-encoded Protobuf payload requests on the same port and multiplex the requests to the corresponding payload decoder based on the "Content-Type" request header.
103
109
 
104
110
  ### OTLP/HTTP Concurrent Requests
105
- - [ ] [SHOULD] To achieve higher total throughput, the client MAY send requests using several parallel HTTP connections. In that case, the maximum number of parallel connections SHOULD be configurable.
111
+ - [x] [SHOULD] To achieve higher total throughput, the client MAY send requests using several parallel HTTP connections. In that case, the maximum number of parallel connections SHOULD be configurable.
112
+ * NOTE: Supported by `flush_thread_count` settings in `<buffer>`.
106
113
 
107
114
  # Future Versions and Interoperability
108
115
  - [ ] [MUST] When possible, the interoperability MUST be ensured between all versions of OTLP that are not declared obsolete.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fluent/plugin/input"
4
4
  require "fluent/plugin/opentelemetry/constant"
5
+ require "fluent/plugin/opentelemetry/grpc_tls"
5
6
  require "fluent/plugin/opentelemetry/http_input_handler"
6
7
  require "fluent/plugin_helper/http_server"
7
8
  require "fluent/plugin_helper/thread"
@@ -27,6 +28,11 @@ module Fluent::Plugin
27
28
  config_param :bind, :string, default: "0.0.0.0"
28
29
  desc "The port to listen to."
29
30
  config_param :port, :integer, default: 4318
31
+
32
+ desc "The size limit of the POSTed element. This value should be larger than the 'chunk_limit_size' in out_opentelemetry plugin."
33
+ config_param :body_size_limit, :size, default: 32 * 1024 * 1024
34
+ desc "The size limit of the decompressed element."
35
+ config_param :decompression_size_limit, :size, default: 256 * 1024 * 1024
30
36
  end
31
37
 
32
38
  config_section :grpc, required: false, multi: false, init: false, param_name: :grpc_config do
@@ -52,13 +58,15 @@ module Fluent::Plugin
52
58
  unless [@http_config, @grpc_config].any?
53
59
  raise Fluent::ConfigError, "Please configure either <http> or <grpc> section, or both."
54
60
  end
61
+
62
+ Opentelemetry::GrpcTLS.validate_server!(@transport_config) if @grpc_config
55
63
  end
56
64
 
57
65
  def start
58
66
  super
59
67
 
60
68
  if @http_config
61
- http_handler = Opentelemetry::HttpInputHandler.new
69
+ http_handler = Opentelemetry::HttpInputHandler.new(@http_config, log)
62
70
  http_server_create_http_server(:in_opentelemetry_http_server, addr: @http_config.bind, port: @http_config.port, logger: log) do |serv|
63
71
  serv.post("/v1/logs") do |req|
64
72
  http_handler.logs(req) { |record| router.emit(tag_for(Opentelemetry::RECORD_TYPE_LOGS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_LOGS, "message" => record }) }
@@ -73,8 +81,8 @@ module Fluent::Plugin
73
81
  end
74
82
 
75
83
  if @grpc_config
84
+ @grpc_handler = Opentelemetry::GrpcInputHandler.new(@grpc_config, @transport_config, log)
76
85
  thread_create(:in_opentelemetry_grpc_server) do
77
- @grpc_handler = Opentelemetry::GrpcInputHandler.new(@grpc_config, log)
78
86
  @grpc_handler.run(
79
87
  logs: lambda { |record|
80
88
  router.emit(tag_for(Opentelemetry::RECORD_TYPE_LOGS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_LOGS, "message" => record })
@@ -5,7 +5,7 @@ require "openssl"
5
5
 
6
6
  module Fluent::Plugin::Opentelemetry
7
7
  CONTENT_TYPE = "Content-Type"
8
- CONTENT_TYPE_PAIN = "text/plain"
8
+ CONTENT_TYPE_PLAIN = "text/plain"
9
9
  CONTENT_TYPE_PROTOBUF = "application/x-protobuf"
10
10
  CONTENT_TYPE_JSON = "application/json"
11
11
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fluent/plugin/opentelemetry/grpc_tls"
3
4
  require "fluent/plugin/opentelemetry/response"
4
5
  require "opentelemetry/proto/collector/logs/v1/logs_service_services_pb"
5
6
  require "opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb"
@@ -49,14 +50,15 @@ module Fluent::Plugin::Opentelemetry
49
50
  end
50
51
  end
51
52
 
52
- def initialize(grpc_config, logger)
53
+ def initialize(grpc_config, transport_config, logger)
53
54
  @grpc_config = grpc_config
54
55
  @logger = logger
56
+ @credentials = Fluent::Plugin::Opentelemetry::GrpcTLS.server_credentials(transport_config)
55
57
  end
56
58
 
57
59
  def run(logs:, metrics:, traces:)
58
60
  @server = GRPC::RpcServer.new(interceptors: [ExceptionInterceptor.new])
59
- @server.add_http2_port("#{@grpc_config.bind}:#{@grpc_config.port}", :this_port_is_insecure)
61
+ @server.add_http2_port("#{@grpc_config.bind}:#{@grpc_config.port}", @credentials)
60
62
 
61
63
  logs_handler = ServiceHandler::Logs.new
62
64
  logs_handler.callback = lambda { |request|
@@ -8,6 +8,7 @@ require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
8
8
  require "opentelemetry/proto/collector/trace/v1/trace_service_services_pb"
9
9
 
10
10
  require "fluent/plugin/opentelemetry/constant"
11
+ require "fluent/plugin/opentelemetry/grpc_tls"
11
12
  require "google/protobuf"
12
13
 
13
14
  class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
@@ -56,10 +57,11 @@ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
56
57
  channel_args["grpc.keepalive_time_ms"] = grpc_config.keepalive_time * 1000
57
58
  channel_args["grpc.keepalive_timeout_ms"] = grpc_config.keepalive_timeout * 1000
58
59
  channel_args["grpc.keepalive_permit_without_calls"] = 1
60
+ credentials = Fluent::Plugin::Opentelemetry::GrpcTLS.channel_credentials(@transport_config)
59
61
  @services = {
60
- Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS => ServiceStub::Logs.new(@grpc_config.endpoint, :this_channel_is_insecure, channel_args: channel_args),
61
- Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS => ServiceStub::Metrics.new(@grpc_config.endpoint, :this_channel_is_insecure, channel_args: channel_args),
62
- Fluent::Plugin::Opentelemetry::RECORD_TYPE_TRACES => ServiceStub::Traces.new(@grpc_config.endpoint, :this_channel_is_insecure, channel_args: channel_args)
62
+ Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS => ServiceStub::Logs.new(@grpc_config.endpoint, credentials, channel_args: channel_args),
63
+ Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS => ServiceStub::Metrics.new(@grpc_config.endpoint, credentials, channel_args: channel_args),
64
+ Fluent::Plugin::Opentelemetry::RECORD_TYPE_TRACES => ServiceStub::Traces.new(@grpc_config.endpoint, credentials, channel_args: channel_args)
63
65
  }
64
66
  end
65
67
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/opentelemetry/constant"
4
+
5
+ require "openssl"
6
+
7
+ module Fluent::Plugin::Opentelemetry
8
+ module GrpcTLS
9
+ module_function
10
+
11
+ def validate_server!(transport_config)
12
+ return unless tls?(transport_config)
13
+
14
+ if transport_config.cert_path.nil? || transport_config.private_key_path.nil?
15
+ raise Fluent::ConfigError, "<transport tls> cert_path and private_key_path are required when <grpc> is used"
16
+ end
17
+
18
+ if transport_config.client_cert_auth && transport_config.ca_path.nil?
19
+ raise Fluent::ConfigError, "<transport tls> client_cert_auth requires ca_path when <grpc> is used"
20
+ end
21
+
22
+ [transport_config.ca_path, transport_config.cert_path, transport_config.private_key_path].compact.each do |path|
23
+ raise Fluent::ConfigError, "<transport tls> cannot read '#{path}'" unless File.readable?(path)
24
+ end
25
+ end
26
+
27
+ def channel_credentials(transport_config)
28
+ return :this_channel_is_insecure unless tls?(transport_config)
29
+
30
+ GRPC::Core::ChannelCredentials.new(
31
+ read_pem(transport_config.ca_path),
32
+ read_private_key(transport_config),
33
+ read_pem(transport_config.cert_path)
34
+ )
35
+ end
36
+
37
+ def server_credentials(transport_config)
38
+ return :this_port_is_insecure unless tls?(transport_config)
39
+
40
+ GRPC::Core::ServerCredentials.new(
41
+ read_pem(transport_config.ca_path),
42
+ [{ private_key: read_private_key(transport_config), cert_chain: read_pem(transport_config.cert_path) }],
43
+ transport_config.client_cert_auth
44
+ )
45
+ end
46
+
47
+ def tls?(transport_config)
48
+ transport_config.protocol == :tls
49
+ end
50
+
51
+ def read_pem(path)
52
+ return nil unless path
53
+
54
+ File.read(path)
55
+ rescue SystemCallError, IOError => e
56
+ raise Fluent::ConfigError, "<transport tls> failed to read '#{path}': #{e.message}"
57
+ end
58
+
59
+ def read_private_key(transport_config)
60
+ pem = read_pem(transport_config.private_key_path)
61
+ return nil unless pem
62
+
63
+ OpenSSL::PKey.read(pem, transport_config.private_key_passphrase.to_s).to_pem
64
+ rescue OpenSSL::PKey::PKeyError => e
65
+ raise Fluent::ConfigError,
66
+ "<transport tls> failed to load private_key_path '#{transport_config.private_key_path}': #{e.message}"
67
+ end
68
+
69
+ private_class_method :tls?, :read_pem, :read_private_key
70
+ end
71
+ end
@@ -25,13 +25,26 @@ module Fluent::PluginHelper::HttpServer
25
25
  @request.body&.close
26
26
  end
27
27
  end
28
+
29
+ unless method_defined?(:body_stream)
30
+ def body_stream
31
+ @request.body
32
+ end
33
+ end
28
34
  end
29
35
  end
30
36
  end
31
37
 
32
38
  class Fluent::Plugin::Opentelemetry::HttpInputHandler
39
+ class SizeLimitError < StandardError; end
40
+
33
41
  using Fluent::PluginHelper::HttpServer::Extension
34
42
 
43
+ def initialize(http_config, logger)
44
+ @http_config = http_config
45
+ @logger = logger
46
+ end
47
+
35
48
  def logs(req, &block)
36
49
  common(req, Fluent::Plugin::Opentelemetry::Request::Logs, Fluent::Plugin::Opentelemetry::Response::Logs, &block)
37
50
  end
@@ -49,16 +62,40 @@ class Fluent::Plugin::Opentelemetry::HttpInputHandler
49
62
  def common(req, request_class, response_class)
50
63
  content_type = req.headers["content-type"]
51
64
  content_encoding = req.headers["content-encoding"]&.first
52
- body = req.body
65
+
66
+ content_length = req.headers["content-length"]&.first&.to_i
67
+ if content_length && content_length >= @http_config.body_size_limit
68
+ @logger.warn { "Received too big content length: #{content_length}" }
69
+ return response_payload_too_large
70
+ end
71
+
72
+ begin
73
+ body = read_body(req, limit: @http_config.body_size_limit)
74
+ rescue SizeLimitError
75
+ @logger.warn { "Received payload exceeding body_size_limit" }
76
+ return response_payload_too_large
77
+ end
78
+
53
79
  return response_unsupported_media_type unless valid_content_type?(content_type)
54
80
  return response_bad_request(content_type) unless valid_content_encoding?(content_encoding)
55
81
 
56
- body = Zlib::GzipReader.new(StringIO.new(body)).read if content_encoding == Fluent::Plugin::Opentelemetry::CONTENT_ENCODING_GZIP
82
+ if content_encoding == Fluent::Plugin::Opentelemetry::CONTENT_ENCODING_GZIP
83
+ begin
84
+ body = decompress(body, limit: @http_config.decompression_size_limit)
85
+ rescue SizeLimitError
86
+ @logger.warn { "Decompressed payload exceeding decompression_size_limit" }
87
+ return response_payload_too_large
88
+ rescue Zlib::Error => e
89
+ @logger.warn { "Failed to decompress gzip payload: #{e.message}" }
90
+ return response_bad_request(content_type)
91
+ end
92
+ end
57
93
 
58
94
  begin
59
95
  record = request_class.new(body).record
60
- rescue Google::Protobuf::ParseError
96
+ rescue Google::Protobuf::ParseError => e
61
97
  # The format in request body does not comply with the OpenTelemetry protocol.
98
+ @logger.warn { "Failed to parse OpenTelemetry payload: #{e.message}" }
62
99
  return response_bad_request(content_type)
63
100
  end
64
101
 
@@ -70,6 +107,43 @@ class Fluent::Plugin::Opentelemetry::HttpInputHandler
70
107
  req.close
71
108
  end
72
109
 
110
+ def read_body(request, limit:)
111
+ body = +""
112
+ while (chunk = request.body_stream&.read)
113
+ body << chunk
114
+ if body.bytesize > limit
115
+ raise SizeLimitError, "Too large payload"
116
+ end
117
+ end
118
+ body
119
+ end
120
+
121
+ BYTES_TO_READ = 64 * 1024
122
+ private_constant :BYTES_TO_READ
123
+
124
+ def decompress(compressed_data, limit:)
125
+ io = StringIO.new(compressed_data)
126
+ out = +""
127
+ loop do
128
+ reader = Zlib::GzipReader.new(io)
129
+ while (chunk = reader.read(BYTES_TO_READ))
130
+ out << chunk
131
+ if out.bytesize > limit
132
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
133
+ end
134
+ end
135
+
136
+ unused = reader.unused
137
+ reader.finish
138
+ unless unused.nil?
139
+ adjust = unused.length
140
+ io.pos -= adjust
141
+ end
142
+ break if io.eof?
143
+ end
144
+ out
145
+ end
146
+
73
147
  def valid_content_type?(content_type)
74
148
  case content_type
75
149
  when Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PROTOBUF, Fluent::Plugin::Opentelemetry::CONTENT_TYPE_JSON
@@ -90,7 +164,11 @@ class Fluent::Plugin::Opentelemetry::HttpInputHandler
90
164
  end
91
165
 
92
166
  def response_unsupported_media_type
93
- response(415, Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PAIN, "415 unsupported media type, supported: [application/json, application/x-protobuf]")
167
+ response(415, Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PLAIN, "415 unsupported media type, supported: [application/json, application/x-protobuf]")
168
+ end
169
+
170
+ def response_payload_too_large
171
+ response(413, Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PLAIN, "413 Payload Too Large")
94
172
  end
95
173
 
96
174
  def response_bad_request(content_type)
@@ -19,6 +19,7 @@ class Fluent::Plugin::Opentelemetry::HttpOutputHandler
19
19
  tls_settings[:client_cert] = @transport_config.cert_path
20
20
  tls_settings[:client_key] = @transport_config.private_key_path
21
21
  tls_settings[:client_key_pass] = @transport_config.private_key_passphrase
22
+ tls_settings[:ssl_ca_file] = @transport_config.ca_path unless @transport_config.ca_path.to_s.empty?
22
23
  tls_settings[:ssl_min_version] = Fluent::Plugin::Opentelemetry::TLS_VERSIONS_MAP[@transport_config.min_version]
23
24
  tls_settings[:ssl_max_version] = Fluent::Plugin::Opentelemetry::TLS_VERSIONS_MAP[@transport_config.max_version]
24
25
  end
@@ -29,7 +30,7 @@ class Fluent::Plugin::Opentelemetry::HttpOutputHandler
29
30
  connect_timeout: http_config.connect_timeout
30
31
  }
31
32
 
32
- Excon.defaults[:ssl_verify_peer] = false if @transport_config.insecure
33
+ tls_settings[:ssl_verify_peer] = false if @transport_config.insecure && tls_settings[:ssl_ca_file].nil?
33
34
  @connections = {
34
35
  Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS => Excon.new(http_logs_endpoint, proxy: @http_config.proxy, persistent: true, **tls_settings, **timeout_settings),
35
36
  Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS => Excon.new(http_metrics_endpoint, proxy: @http_config.proxy, persistent: true, **tls_settings, **timeout_settings),
@@ -3,7 +3,7 @@
3
3
  module Fluent
4
4
  module Plugin
5
5
  module Opentelemetry
6
- VERSION = "0.5.2"
6
+ VERSION = "0.5.4"
7
7
  end
8
8
  end
9
9
  end
@@ -90,7 +90,7 @@ module Fluent::Plugin
90
90
  end
91
91
 
92
92
  def write(chunk)
93
- BatchProcessor.build_export_requests(chunk).each do |export_request|
93
+ BatchProcessor.build_export_requests(chunk, log).each do |export_request|
94
94
  if @grpc_handler
95
95
  @grpc_handler.export(export_request)
96
96
  else
@@ -106,22 +106,72 @@ module Fluent::Plugin
106
106
  Opentelemetry::RECORD_TYPE_TRACES => "resourceSpans"
107
107
  }.freeze
108
108
 
109
- def self.build_export_requests(chunk)
109
+ class InvalidRecords
110
+ MAX_SAMPLE_BYTESIZE = 100
111
+ MAX_SAMPLES = 3
112
+ private_constant :MAX_SAMPLE_BYTESIZE, :MAX_SAMPLES
113
+
114
+ attr_reader :count
115
+
116
+ def initialize
117
+ @count = 0
118
+ @samples = []
119
+ end
120
+
121
+ def add(sample = nil)
122
+ @count += 1
123
+ return if sample.nil? || @samples.size >= MAX_SAMPLES
124
+
125
+ text = sample.byteslice(0, MAX_SAMPLE_BYTESIZE).scrub("")
126
+ @samples << text unless @samples.include?(text)
127
+ end
128
+
129
+ def to_s
130
+ @samples.empty? ? @count.to_s : "#{@count} (#{@samples.join(' | ')})"
131
+ end
132
+ end
133
+
134
+ def self.build_export_requests(chunk, logger)
110
135
  requests = {
111
136
  Opentelemetry::RECORD_TYPE_LOGS => {},
112
137
  Opentelemetry::RECORD_TYPE_METRICS => {},
113
138
  Opentelemetry::RECORD_TYPE_TRACES => {}
114
139
  }
140
+ invalid = Hash.new { |hash, key| hash[key] = InvalidRecords.new }
115
141
 
116
142
  chunk.each do |_, record| # rubocop:disable Style/HashEachMethods
117
143
  record_type = record["type"]
118
144
  resource_key = RESOURCE_KEY_MAP[record_type]
119
- record["message"] = JSON.parse(record["message"])
120
- resource_hash = record["message"][resource_key][0]["resource"].hash
145
+ unless resource_key
146
+ invalid["unknown type"].add(record_type.inspect)
147
+ next
148
+ end
149
+
150
+ begin
151
+ record["message"] = JSON.parse(record["message"])
152
+ rescue JSON::ParserError, TypeError => e
153
+ invalid["broken message"].add(e.message)
154
+ next
155
+ end
156
+
157
+ resources = record["message"][resource_key] if record["message"].is_a?(Hash)
158
+ unless resources.is_a?(Array) && resources.first.is_a?(Hash)
159
+ invalid["no #{resource_key}"].add
160
+ next
161
+ end
162
+
163
+ resource_hash = resources.first["resource"].hash
121
164
  if requests[record_type][resource_hash].nil?
122
165
  requests[record_type][resource_hash] = record
123
166
  else
124
- requests[record_type][resource_hash]["message"][resource_key].concat(record["message"][resource_key])
167
+ requests[record_type][resource_hash]["message"][resource_key].concat(resources)
168
+ end
169
+ end
170
+
171
+ unless invalid.empty?
172
+ logger.warn do
173
+ details = invalid.map { |reason, records| "#{reason}=#{records}" }
174
+ "Skipped invalid records (total: #{invalid.values.sum(&:count)}): #{details.join(', ')}"
125
175
  end
126
176
  end
127
177
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-opentelemetry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shizuo Fujita
@@ -112,6 +112,7 @@ files:
112
112
  - lib/fluent/plugin/opentelemetry/constant.rb
113
113
  - lib/fluent/plugin/opentelemetry/grpc_input_handler.rb
114
114
  - lib/fluent/plugin/opentelemetry/grpc_output_handler.rb
115
+ - lib/fluent/plugin/opentelemetry/grpc_tls.rb
115
116
  - lib/fluent/plugin/opentelemetry/http_input_handler.rb
116
117
  - lib/fluent/plugin/opentelemetry/http_output_handler.rb
117
118
  - lib/fluent/plugin/opentelemetry/request.rb
@@ -138,6 +139,7 @@ licenses:
138
139
  metadata:
139
140
  homepage_uri: https://github.com/fluent-plugins-nursery/fluent-plugin-opentelemetry
140
141
  source_code_uri: https://github.com/fluent-plugins-nursery/fluent-plugin-opentelemetry
142
+ changelog_uri: https://github.com/fluent-plugins-nursery/fluent-plugin-opentelemetry/blob/main/CHANGELOG.md
141
143
  rubygems_mfa_required: 'true'
142
144
  rdoc_options: []
143
145
  require_paths:
@@ -153,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
155
  - !ruby/object:Gem::Version
154
156
  version: '0'
155
157
  requirements: []
156
- rubygems_version: 4.0.6
158
+ rubygems_version: 4.0.18
157
159
  specification_version: 4
158
160
  summary: Fluentd input/output plugin to forward OpenTelemetry Protocol data.
159
161
  test_files: []