fluent-plugin-opentelemetry-radiant 0.1.0 → 0.1.1
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 +4 -4
- data/CHANGELOG.md +5 -3
- data/README.md +8 -0
- data/lib/fluent/plugin/opentelemetry/grpc_output_handler.rb +131 -26
- data/lib/fluent/plugin/opentelemetry_radiant/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08cf17bafeddfd37c8ea288aeea8bdc9f3c9c6f12383d37b2b78d43a5e16087a'
|
|
4
|
+
data.tar.gz: ec2cdb92e14a3323c84451256523b8df2084cbb5865fa93f47620a4ac56f4990
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57c372462c52f818e06427d2867e3534c267ccd4a4a43182ce1fe365255ce59143054959d26629f283487faef0a03a66f368dc10909d59aad2a161100b6794ee
|
|
7
|
+
data.tar.gz: ca0f93fc4f847113465ba7c54625491ec7491a1303da6bc1e85f99e7606c5230ccfc650fcb7ee1f0dce0344eee168d3fcf3ea968f37ed3049ae927454219f163
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.1.1] - 2026-10-28
|
|
2
2
|
|
|
3
|
-
-
|
|
3
|
+
- Check gRPC TLS files before each export and reconnect when certificates or
|
|
4
|
+
keys change, without a Fluentd reload or restart. Keep the current channel if the new files cannot be loaded.
|
|
4
5
|
|
|
5
|
-
## [0.1.0] - 2026-
|
|
6
|
+
## [0.1.0] - 2026-10-27
|
|
6
7
|
|
|
8
|
+
- Normalize log strings to valid UTF-8 without mutating records, preventing binary or invalid bytes from dropping whole chunks.
|
|
7
9
|
- Add secret-masked authentication headers for HTTP and lowercase gRPC metadata.
|
|
8
10
|
- Add HTTP CA and mutual TLS settings without changing global Excon defaults;
|
|
9
11
|
warn once at start when HTTP certificate verification is disabled.
|
data/README.md
CHANGED
|
@@ -200,6 +200,14 @@ a `Fluent::ConfigError` identifying the path. An encrypted client key can use
|
|
|
200
200
|
The HTTP `min_version` / `max_version` settings are not applied to gRPC, whose
|
|
201
201
|
TLS negotiation is managed by gRPC core.
|
|
202
202
|
|
|
203
|
+
**Certificate rotation.** HTTP opens new connections with the TLS files as they
|
|
204
|
+
are on disk. gRPC checks the configured TLS files before each send and reconnects
|
|
205
|
+
when their contents change. If the files cannot be read or the new credentials
|
|
206
|
+
are invalid, gRPC warns and keeps its current channel, then checks again on the
|
|
207
|
+
next send. Headers (including tokens) are read when the configuration loads, so
|
|
208
|
+
a token change needs a config reload. Mount Kubernetes Secrets as a directory,
|
|
209
|
+
not with `subPath`, because kubelet does not update `subPath` mounts.
|
|
210
|
+
|
|
203
211
|
| gRPC option | Default | Meaning |
|
|
204
212
|
|---|---|---|
|
|
205
213
|
| `compress` | `text` | `text` sends uncompressed messages; `gzip` enables gRPC gzip compression. |
|
|
@@ -19,6 +19,8 @@ require "opentelemetry/proto/collector/trace/v1/trace_service_services_pb"
|
|
|
19
19
|
require "fluent/plugin/opentelemetry/constant"
|
|
20
20
|
require "fluent/plugin/opentelemetry/output_response"
|
|
21
21
|
require "google/protobuf"
|
|
22
|
+
require "digest"
|
|
23
|
+
require "set"
|
|
22
24
|
|
|
23
25
|
class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
|
|
24
26
|
include Fluent::Plugin::Opentelemetry::OutputResponse
|
|
@@ -30,6 +32,8 @@ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
|
|
|
30
32
|
[GRPC::Core::StatusCodes.const_get(name), name.to_s]
|
|
31
33
|
end.freeze
|
|
32
34
|
|
|
35
|
+
class TlsFileError < StandardError; end
|
|
36
|
+
|
|
33
37
|
class ServiceStub
|
|
34
38
|
class Logs
|
|
35
39
|
def initialize(host, creds, **kw)
|
|
@@ -82,26 +86,38 @@ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
|
|
|
82
86
|
raise Fluent::ConfigError, "certificate verification cannot be turned off for gRPC; set ca_path to the server's CA instead"
|
|
83
87
|
end
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
@transport_config = transport_config
|
|
90
|
+
@tls_mutex = Mutex.new
|
|
91
|
+
@active_exports = Hash.new(0)
|
|
92
|
+
@tls_failures = Set.new
|
|
93
|
+
if transport_config&.protocol == :tls
|
|
94
|
+
@tls_paths = {
|
|
95
|
+
ca_path: transport_config.ca_path, cert_path: transport_config.cert_path,
|
|
96
|
+
private_key_path: transport_config.private_key_path
|
|
97
|
+
}.compact
|
|
98
|
+
end
|
|
99
|
+
pems = read_tls_files if @tls_paths
|
|
100
|
+
credential = channel_credentials(pems)
|
|
101
|
+
@tls_fingerprints = fingerprints(pems) if @tls_paths
|
|
102
|
+
@channel_args = {}
|
|
87
103
|
if grpc_config.compress == :gzip
|
|
88
|
-
channel_args = GRPC::Core::CompressionOptions.new(default_algorithm: :gzip).to_channel_arg_hash
|
|
104
|
+
@channel_args = GRPC::Core::CompressionOptions.new(default_algorithm: :gzip).to_channel_arg_hash
|
|
89
105
|
end
|
|
90
|
-
channel_args["grpc.keepalive_time_ms"] = grpc_config.keepalive_time * 1000
|
|
91
|
-
channel_args["grpc.keepalive_timeout_ms"] = grpc_config.keepalive_timeout * 1000
|
|
92
|
-
channel_args["grpc.keepalive_permit_without_calls"] = 1
|
|
93
|
-
@
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
106
|
+
@channel_args["grpc.keepalive_time_ms"] = grpc_config.keepalive_time * 1000
|
|
107
|
+
@channel_args["grpc.keepalive_timeout_ms"] = grpc_config.keepalive_timeout * 1000
|
|
108
|
+
@channel_args["grpc.keepalive_permit_without_calls"] = 1
|
|
109
|
+
@channel_args["grpc.primary_user_agent"] = "grpc-ruby/#{GRPC::VERSION}"
|
|
110
|
+
@channel, @services = build_services(credential)
|
|
111
|
+
rescue TlsFileError => e
|
|
112
|
+
raise Fluent::ConfigError, e.message, cause: nil
|
|
98
113
|
end
|
|
99
114
|
|
|
100
115
|
def export(record)
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
response = with_service(record["type"]) do |service|
|
|
117
|
+
raise Fluent::UnrecoverableError, "got unknown record type '#{record['type']}'" unless service
|
|
103
118
|
|
|
104
|
-
|
|
119
|
+
service.export(record["message"], **call_options)
|
|
120
|
+
end
|
|
105
121
|
warn_partial_success(response) if record["type"] == Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS
|
|
106
122
|
rescue Google::Protobuf::ParseError => e
|
|
107
123
|
raise Fluent::UnrecoverableError, e.message
|
|
@@ -111,7 +127,9 @@ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
|
|
|
111
127
|
end
|
|
112
128
|
|
|
113
129
|
def export_logs(request)
|
|
114
|
-
response =
|
|
130
|
+
response = with_service(Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS) do |service|
|
|
131
|
+
service.export_request(request, **call_options)
|
|
132
|
+
end
|
|
115
133
|
warn_partial_success(response)
|
|
116
134
|
rescue GRPC::BadStatus => e
|
|
117
135
|
code_name = CODE_NAMES.fetch(e.code, e.code.to_s)
|
|
@@ -128,34 +146,121 @@ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
|
|
|
128
146
|
@services.fetch(Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS)
|
|
129
147
|
end
|
|
130
148
|
|
|
149
|
+
def service_for(type)
|
|
150
|
+
type == Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS ? logs_service : @services[type]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def with_service(type)
|
|
154
|
+
return yield service_for(type) unless @tls_paths
|
|
155
|
+
|
|
156
|
+
channel = nil
|
|
157
|
+
service = @tls_mutex.synchronize do
|
|
158
|
+
refresh_tls_files
|
|
159
|
+
channel = @channel
|
|
160
|
+
@active_exports[channel] += 1
|
|
161
|
+
service_for(type)
|
|
162
|
+
end
|
|
163
|
+
yield service
|
|
164
|
+
ensure
|
|
165
|
+
if channel
|
|
166
|
+
@tls_mutex.synchronize do
|
|
167
|
+
@active_exports[channel] -= 1
|
|
168
|
+
if @active_exports[channel].zero?
|
|
169
|
+
@active_exports.delete(channel)
|
|
170
|
+
channel.close unless channel.equal?(@channel)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build_services(credential)
|
|
177
|
+
channel = GRPC::Core::Channel.new(@grpc_config.endpoint, @channel_args, credential)
|
|
178
|
+
options = { channel_override: channel }
|
|
179
|
+
services = {
|
|
180
|
+
Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS => ServiceStub::Logs.new(@grpc_config.endpoint, credential, **options),
|
|
181
|
+
Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS => ServiceStub::Metrics.new(@grpc_config.endpoint, credential, **options),
|
|
182
|
+
Fluent::Plugin::Opentelemetry::RECORD_TYPE_TRACES => ServiceStub::Traces.new(@grpc_config.endpoint, credential, **options)
|
|
183
|
+
}
|
|
184
|
+
[channel, services]
|
|
185
|
+
rescue StandardError
|
|
186
|
+
channel&.close
|
|
187
|
+
raise
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def refresh_tls_files
|
|
191
|
+
begin
|
|
192
|
+
pems = read_tls_files
|
|
193
|
+
current = fingerprints(pems)
|
|
194
|
+
if current != @tls_fingerprints
|
|
195
|
+
channel, services = build_services(channel_credentials(pems, rotation: true))
|
|
196
|
+
old_channel = @channel
|
|
197
|
+
@channel, @services, @tls_fingerprints = channel, services, current
|
|
198
|
+
end
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
message = e.is_a?(TlsFileError) ? e.message : "could not rebuild gRPC TLS channel for #{@tls_paths.values.join(', ')} (#{e.class})"
|
|
201
|
+
failure = [message, current]
|
|
202
|
+
if @tls_failures.add?(failure)
|
|
203
|
+
@logger.warn "#{message}; keeping the current gRPC channel and retrying the TLS files on the next export"
|
|
204
|
+
end
|
|
205
|
+
return
|
|
206
|
+
end
|
|
207
|
+
@tls_failures.clear
|
|
208
|
+
return unless old_channel
|
|
209
|
+
|
|
210
|
+
@logger.info "gRPC TLS files changed; reconnecting with the updated certificate files"
|
|
211
|
+
# Let calls that already selected the old stubs finish before closing them.
|
|
212
|
+
old_channel.close unless @active_exports.key?(old_channel)
|
|
213
|
+
end
|
|
214
|
+
|
|
131
215
|
def call_options
|
|
132
216
|
{ deadline: Time.now + @grpc_config.timeout, metadata: @metadata }
|
|
133
217
|
end
|
|
134
218
|
|
|
135
|
-
def channel_credentials(
|
|
136
|
-
return :this_channel_is_insecure unless
|
|
219
|
+
def channel_credentials(pems, rotation: false)
|
|
220
|
+
return :this_channel_is_insecure unless @tls_paths
|
|
137
221
|
|
|
138
|
-
|
|
139
|
-
key_pem =
|
|
140
|
-
cert_pem = read_pem(transport.cert_path)
|
|
222
|
+
transport = @transport_config
|
|
223
|
+
ca_pem, key_pem, cert_pem = pems.values_at(:ca_path, :private_key_path, :cert_path)
|
|
141
224
|
if key_pem.nil? != cert_pem.nil?
|
|
142
|
-
raise
|
|
225
|
+
raise TlsFileError, "gRPC TLS requires both cert_path and private_key_path for a client certificate"
|
|
143
226
|
end
|
|
144
|
-
|
|
227
|
+
# Configure-time checks stay as in 0.1.0. A rotation replaces a working
|
|
228
|
+
# channel, so it also requires a parseable key that matches the certificate.
|
|
229
|
+
if key_pem && (transport.private_key_passphrase || rotation)
|
|
145
230
|
begin
|
|
146
|
-
|
|
231
|
+
key = OpenSSL::PKey.read(key_pem, transport.private_key_passphrase || "")
|
|
147
232
|
rescue OpenSSL::OpenSSLError, ArgumentError
|
|
148
|
-
|
|
233
|
+
action = transport.private_key_passphrase ? "decrypt" : "read"
|
|
234
|
+
raise TlsFileError, "could not #{action} gRPC TLS private_key_path '#{transport.private_key_path}'", cause: nil
|
|
149
235
|
end
|
|
150
236
|
end
|
|
237
|
+
if key_pem && rotation
|
|
238
|
+
begin
|
|
239
|
+
valid_pair = OpenSSL::X509::Certificate.new(cert_pem).check_private_key(key)
|
|
240
|
+
rescue OpenSSL::OpenSSLError, ArgumentError
|
|
241
|
+
valid_pair = false
|
|
242
|
+
end
|
|
243
|
+
unless valid_pair
|
|
244
|
+
raise TlsFileError, "invalid gRPC TLS client certificate/key pair '#{transport.cert_path}' and '#{transport.private_key_path}'", cause: nil
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
key_pem = key.to_pem if key_pem && transport.private_key_passphrase
|
|
151
248
|
GRPC::Core::ChannelCredentials.new(ca_pem, key_pem, cert_pem)
|
|
152
249
|
end
|
|
153
250
|
|
|
251
|
+
def read_tls_files
|
|
252
|
+
@tls_paths.transform_values { |path| read_pem(path) }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def fingerprints(pems)
|
|
256
|
+
pems.transform_values { |pem| Digest::SHA256.hexdigest(pem) }
|
|
257
|
+
end
|
|
258
|
+
|
|
154
259
|
def read_pem(path)
|
|
155
260
|
return nil unless path
|
|
156
261
|
|
|
157
|
-
File.
|
|
262
|
+
File.binread(path)
|
|
158
263
|
rescue SystemCallError, IOError
|
|
159
|
-
raise
|
|
264
|
+
raise TlsFileError, "could not read gRPC TLS file '#{path}'", cause: nil
|
|
160
265
|
end
|
|
161
266
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluent-plugin-opentelemetry-radiant
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Radiant Logic, Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async-http
|