fluent-plugin-opentelemetry 0.5.3 → 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: 2e1331e92688a03b2da70e3712257f4dfd4ae1cec7c5da354084f6fc883ae5cf
4
- data.tar.gz: 403339bc82d1c09823afc7a5c2b16199cafab92300a0e56eb446fb33e55e1407
3
+ metadata.gz: dad225b751029746a86f4bdeae0b9f415d0756c25d9c560e7606ac11c13ade0b
4
+ data.tar.gz: '091d4d6113b42822cc47e6cd386aa10e2044f9a146d401e437eefe21b11df10e'
5
5
  SHA512:
6
- metadata.gz: d3cc4c75562d9698c668b02e172d8eeb2c15de4233cb1ebe55607e6daeb18efe19df1bd20f2b0c586f79e07d63301bcf880221d904ebcf11c34047b7a00fd12d
7
- data.tar.gz: 83d8d1e66e74c93a23f46d7580591b72d1bd3dfd064365ce262057890cb8aff0377cae4cf7c88ad810ed94842a5eb7c91e526109efd50fcff7d131c6afb4f3a7
6
+ metadata.gz: d5919a65f7d4adf3491c16cc3a098283bb69f7fd84a09e88730d15df7919c878838eee465d3315f5f762acd323f2a0983c2445f74b36384526ea1a1dc2e04f51
7
+ data.tar.gz: fe3517def463d5c8ae2c559df0e42438d518ae4d74f0ccfc6e900c570ca74b1cca2d1b31a97da33e79d566ba304884c5646f13a46869d3ea8147ba209143d1c9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
3
11
  ## [0.5.3] - 2026-06-25
4
12
 
5
13
  Fixes:
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
@@ -89,6 +92,11 @@ It needs to install `grpc` gem manually to use this feature.
89
92
 
90
93
  Refer [Config: Transport Section](https://docs.fluentd.org/configuration/transport-section)
91
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
+
92
100
  #### Example
93
101
 
94
102
  ```
@@ -204,9 +212,11 @@ It needs to install `grpc` gem manually to use this feature.
204
212
 
205
213
  | parameter | type | description | default |
206
214
  |------------------------|---------|------------------------------------------------|---------|
215
+ | ca_path | string | Specifies the path of CA Certificate file used to verify the server | `nil` |
207
216
  | cert_path | string | Specifies the path of Certificate file | `nil` |
208
217
  | private_key_path | string | Specifies the path of Private Key file | `nil` |
209
218
  | private_key_passphrase | string | Specifies the public CA private key passphrase | `nil` |
219
+ | insecure | bool | Skips the verification of the server certificate | `false` |
210
220
 
211
221
  | parameter | type | description | available values | default |
212
222
  |-------------|------|-------------------------------------------------------------|--------------------------------|---------|
@@ -215,6 +225,18 @@ It needs to install `grpc` gem manually to use this feature.
215
225
 
216
226
  Refer [Config: Transport Section](https://docs.fluentd.org/configuration/transport-section)
217
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
+
218
240
  #### `<buffer>` section
219
241
 
220
242
  | parameter | type | description | default |
@@ -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"
@@ -57,6 +58,8 @@ module Fluent::Plugin
57
58
  unless [@http_config, @grpc_config].any?
58
59
  raise Fluent::ConfigError, "Please configure either <http> or <grpc> section, or both."
59
60
  end
61
+
62
+ Opentelemetry::GrpcTLS.validate_server!(@transport_config) if @grpc_config
60
63
  end
61
64
 
62
65
  def start
@@ -78,8 +81,8 @@ module Fluent::Plugin
78
81
  end
79
82
 
80
83
  if @grpc_config
84
+ @grpc_handler = Opentelemetry::GrpcInputHandler.new(@grpc_config, @transport_config, log)
81
85
  thread_create(:in_opentelemetry_grpc_server) do
82
- @grpc_handler = Opentelemetry::GrpcInputHandler.new(@grpc_config, log)
83
86
  @grpc_handler.run(
84
87
  logs: lambda { |record|
85
88
  router.emit(tag_for(Opentelemetry::RECORD_TYPE_LOGS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_LOGS, "message" => record })
@@ -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
@@ -119,6 +119,7 @@ class Fluent::Plugin::Opentelemetry::HttpInputHandler
119
119
  end
120
120
 
121
121
  BYTES_TO_READ = 64 * 1024
122
+ private_constant :BYTES_TO_READ
122
123
 
123
124
  def decompress(compressed_data, limit:)
124
125
  io = StringIO.new(compressed_data)
@@ -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.3"
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.3
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
@@ -154,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
154
155
  - !ruby/object:Gem::Version
155
156
  version: '0'
156
157
  requirements: []
157
- rubygems_version: 4.0.10
158
+ rubygems_version: 4.0.18
158
159
  specification_version: 4
159
160
  summary: Fluentd input/output plugin to forward OpenTelemetry Protocol data.
160
161
  test_files: []