fluent-plugin-opentelemetry-radiant 0.1.0

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +50 -0
  3. data/LICENSE +202 -0
  4. data/NOTICE +23 -0
  5. data/README.md +278 -0
  6. data/lib/fluent/plugin/in_opentelemetry.rb +118 -0
  7. data/lib/fluent/plugin/in_opentelemetry_metrics.rb +285 -0
  8. data/lib/fluent/plugin/opentelemetry/constant.rb +35 -0
  9. data/lib/fluent/plugin/opentelemetry/grpc_input_handler.rb +89 -0
  10. data/lib/fluent/plugin/opentelemetry/grpc_output_handler.rb +161 -0
  11. data/lib/fluent/plugin/opentelemetry/http_input_handler.rb +177 -0
  12. data/lib/fluent/plugin/opentelemetry/http_output_handler.rb +157 -0
  13. data/lib/fluent/plugin/opentelemetry/log_record_builder.rb +229 -0
  14. data/lib/fluent/plugin/opentelemetry/output_response.rb +21 -0
  15. data/lib/fluent/plugin/opentelemetry/request.rb +67 -0
  16. data/lib/fluent/plugin/opentelemetry/response.rb +90 -0
  17. data/lib/fluent/plugin/opentelemetry/version.rb +9 -0
  18. data/lib/fluent/plugin/opentelemetry_radiant/version.rb +14 -0
  19. data/lib/fluent/plugin/out_opentelemetry.rb +11 -0
  20. data/lib/fluent/plugin/out_opentelemetry_radiant.rb +205 -0
  21. data/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb +27 -0
  22. data/lib/opentelemetry/proto/collector/logs/v1/logs_service_services_pb.rb +48 -0
  23. data/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb +27 -0
  24. data/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb.rb +48 -0
  25. data/lib/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb.rb +27 -0
  26. data/lib/opentelemetry/proto/collector/profiles/v1development/profiles_service_services_pb.rb +47 -0
  27. data/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb +27 -0
  28. data/lib/opentelemetry/proto/collector/trace/v1/trace_service_services_pb.rb +48 -0
  29. data/lib/opentelemetry/proto/common/v1/common_pb.rb +26 -0
  30. data/lib/opentelemetry/proto/logs/v1/logs_pb.rb +29 -0
  31. data/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb +41 -0
  32. data/lib/opentelemetry/proto/profiles/v1development/profiles_pb.rb +36 -0
  33. data/lib/opentelemetry/proto/resource/v1/resource_pb.rb +23 -0
  34. data/lib/opentelemetry/proto/trace/v1/trace_pb.rb +33 -0
  35. metadata +198 -0
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/input"
4
+ require "fluent/plugin/opentelemetry/constant"
5
+ require "fluent/plugin/opentelemetry/version"
6
+ require "fluent/plugin_helper/timer"
7
+ require "fluent/version"
8
+
9
+ require "json"
10
+ require "socket"
11
+
12
+ module Fluent::Plugin
13
+ class OpentelemetryMetricsInput < Input
14
+ Fluent::Plugin.register_input("opentelemetry_metrics", self)
15
+
16
+ helpers :timer
17
+
18
+ desc "Determine the rate to emit internal metrics as events."
19
+ config_param :emit_interval, :time, default: 60
20
+
21
+ desc "The tag of the event."
22
+ config_param :tag, :string
23
+
24
+ desc "The prefix of metric name."
25
+ config_param :metric_name_prefix, :string, default: "fluentd_"
26
+
27
+ def start
28
+ super
29
+
30
+ @metrics = Metrics.new(metric_name_prefix: @metric_name_prefix)
31
+ timer_execute(:in_opentelemetry_metrics, @emit_interval) do
32
+ router.emit(@tag, Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_METRICS, "message" => @metrics.record })
33
+ end
34
+ end
35
+
36
+ module Extension
37
+ refine Time do
38
+ def to_nano_sec
39
+ (to_i * 1_000_000_000) + nsec
40
+ end
41
+ end
42
+ end
43
+
44
+ class Metrics
45
+ using Extension
46
+
47
+ def initialize(metric_name_prefix:)
48
+ @start_time_unix_nano = Time.now.to_nano_sec
49
+ @metric_name_prefix = metric_name_prefix.to_s
50
+ @hostname = Socket.gethostname
51
+ @monitor_info = MonitorInfo.new
52
+ end
53
+
54
+ def record
55
+ values.to_json
56
+ end
57
+
58
+ def values
59
+ metrics_data
60
+ end
61
+
62
+ private
63
+
64
+ def metrics_data
65
+ {
66
+ "resourceMetrics" => [
67
+ {
68
+ "resource" => {
69
+ "attributes" => [
70
+ string_value_attribute("service.name", "fluentd"),
71
+ string_value_attribute("service.version", Fluent::VERSION),
72
+ string_value_attribute("host.name", @hostname),
73
+ string_value_attribute("process.runtime.name", "ruby"),
74
+ string_value_attribute("process.runtime.version", RUBY_VERSION),
75
+ int_value_attribute("process.pid", Process.pid)
76
+ ]
77
+ },
78
+ "scopeMetrics" => scope_metrics
79
+ }
80
+ ]
81
+ }
82
+ end
83
+
84
+ def scope_metrics
85
+ [
86
+ {
87
+ "scope" => {
88
+ "name" => "fluent-plugin-opentelemetry",
89
+ "version" => Fluent::Plugin::Opentelemetry::VERSION
90
+ },
91
+ "metrics" => metrics
92
+ }
93
+ ]
94
+ end
95
+
96
+ def metrics
97
+ time_nano_sec = Time.now.to_nano_sec
98
+ metrics = []
99
+
100
+ @monitor_info.plugins_info_all.each do |record|
101
+ attributes = {
102
+ plugin_id: record["plugin_id"],
103
+ plugin: plugin_name(record["plugin_category"], record["type"]),
104
+ plugin_category: record["plugin_category"],
105
+ plugin_type: record["type"]
106
+ }.map { |k, v| string_value_attribute(k, v) }
107
+
108
+ record.each do |key, value|
109
+ next unless value.is_a?(Numeric)
110
+
111
+ metrics << {
112
+ "name" => @metric_name_prefix + key.to_s,
113
+ "unit" => "1",
114
+ # TODO: "description"
115
+ "gauge" => {
116
+ "dataPoints" => [
117
+ {
118
+ "startTimeUnixNano" => @start_time_unix_nano,
119
+ "timeUnixNano" => time_nano_sec,
120
+ "asDouble" => value,
121
+ "attributes" => attributes
122
+ }
123
+ ]
124
+ }
125
+ }
126
+ end
127
+ end
128
+
129
+ metrics
130
+ end
131
+
132
+ def plugin_name(category, type)
133
+ prefix =
134
+ case category
135
+ when "input"
136
+ "in"
137
+ when "output"
138
+ "out"
139
+ else
140
+ category
141
+ end
142
+
143
+ "#{prefix}_#{type}"
144
+ end
145
+
146
+ def string_value_attribute(key, value)
147
+ {
148
+ "key" => key.to_s,
149
+ "value" => {
150
+ "stringValue" => value.to_s
151
+ }
152
+ }
153
+ end
154
+
155
+ def int_value_attribute(key, value)
156
+ {
157
+ "key" => key.to_s,
158
+ "value" => {
159
+ "intValue" => value
160
+ }
161
+ }
162
+ end
163
+ end
164
+
165
+ # Imported from Fluent::Plugin::MonitorAgentInput
166
+ class MonitorInfo
167
+ # They are deprecated but remain for compatibiscripts/pluginslity
168
+ MONITOR_INFO = {
169
+ "output_plugin" => -> { is_a?(::Fluent::Plugin::Output) },
170
+ "buffer_queue_length" => lambda {
171
+ throw(:skip) unless instance_variable_defined?(:@buffer) && !@buffer.nil? && @buffer.is_a?(::Fluent::Plugin::Buffer)
172
+ @buffer.queue.size
173
+ },
174
+ "buffer_timekeys" => lambda {
175
+ throw(:skip) unless instance_variable_defined?(:@buffer) && !@buffer.nil? && @buffer.is_a?(::Fluent::Plugin::Buffer)
176
+ @buffer.timekeys
177
+ },
178
+ "buffer_total_queued_size" => lambda {
179
+ throw(:skip) unless instance_variable_defined?(:@buffer) && !@buffer.nil? && @buffer.is_a?(::Fluent::Plugin::Buffer)
180
+ @buffer.stage_size + @buffer.queue_size
181
+ },
182
+ "retry_count" => -> { respond_to?(:num_errors) ? num_errors : nil }
183
+ }.freeze
184
+
185
+ def all_plugins
186
+ array = []
187
+
188
+ # get all input plugins
189
+ array.concat Fluent::Engine.root_agent.inputs
190
+
191
+ # get all output plugins
192
+ array.concat Fluent::Engine.root_agent.outputs
193
+
194
+ # get all filter plugins
195
+ array.concat Fluent::Engine.root_agent.filters
196
+
197
+ Fluent::Engine.root_agent.labels.each_value do |l|
198
+ # TODO: Add label name to outputs / filters for identifying plugins
199
+ array.concat l.outputs
200
+ array.concat l.filters
201
+ end
202
+
203
+ array
204
+ end
205
+
206
+ def plugin_category(pe)
207
+ case pe
208
+ when Fluent::Plugin::Input
209
+ "input"
210
+ when Fluent::Plugin::Output, Fluent::Plugin::MultiOutput, Fluent::Plugin::BareOutput
211
+ "output"
212
+ when Fluent::Plugin::Filter
213
+ "filter"
214
+ else
215
+ "unknown"
216
+ end
217
+ end
218
+
219
+ def plugins_info_all(opts = {})
220
+ all_plugins.map do |pe|
221
+ get_monitor_info(pe, opts)
222
+ end
223
+ end
224
+
225
+ IGNORE_ATTRIBUTES = %i(@config_root_section @config @masked_config).freeze
226
+
227
+ # get monitor info from the plugin `pe` and return a hash object
228
+ def get_monitor_info(pe, opts = {})
229
+ obj = {}
230
+
231
+ # Common plugin information
232
+ obj["plugin_id"] = pe.plugin_id
233
+ obj["plugin_category"] = plugin_category(pe)
234
+ obj["type"] = pe.config["@type"]
235
+ obj["config"] = pe.config if opts[:with_config]
236
+
237
+ # run MONITOR_INFO in plugins' instance context and store the info to obj
238
+ MONITOR_INFO.each_pair do |key, code|
239
+ catch(:skip) do
240
+ obj[key] = pe.instance_exec(&code)
241
+ end
242
+ rescue NoMethodError => e
243
+ unless @first_warn
244
+ log.error "NoMethodError in monitoring plugins", key: key, plugin: pe.class, error: e
245
+ log.error_backtrace
246
+ @first_warn = true
247
+ end
248
+ rescue StandardError => e
249
+ log.warn "unexpected error in monitoring plugins", key: key, plugin: pe.class, error: e
250
+ end
251
+
252
+ if pe.respond_to?(:statistics)
253
+ obj.merge!(pe.statistics["output"] || {})
254
+ obj.merge!(pe.statistics["filter"] || {})
255
+ obj.merge!(pe.statistics["input"] || {})
256
+ end
257
+
258
+ obj["retry"] = get_retry_info(pe.retry) if opts[:with_retry] && pe.instance_variable_defined?(:@retry)
259
+
260
+ # include all instance variables if :with_debug_info is set
261
+ if opts[:with_debug_info]
262
+ iv = {}
263
+ pe.instance_eval do
264
+ instance_variables.each do |sym|
265
+ next if IGNORE_ATTRIBUTES.include?(sym)
266
+
267
+ key = sym.to_s[1..] # removes first '@'
268
+ iv[key] = instance_variable_get(sym)
269
+ end
270
+ end
271
+ obj["instance_variables"] = iv
272
+ elsif (ivars = opts[:ivars])
273
+ iv = {}
274
+ ivars.each do |name|
275
+ iname = "@#{name}"
276
+ iv[name] = pe.instance_variable_get(iname) if pe.instance_variable_defined?(iname)
277
+ end
278
+ obj["instance_variables"] = iv
279
+ end
280
+
281
+ obj
282
+ end
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin"
4
+ require "openssl"
5
+
6
+ module Fluent::Plugin::Opentelemetry
7
+ CONTENT_TYPE = "Content-Type"
8
+ CONTENT_TYPE_PLAIN = "text/plain"
9
+ CONTENT_TYPE_PAIN = CONTENT_TYPE_PLAIN
10
+ CONTENT_TYPE_PROTOBUF = "application/x-protobuf"
11
+ CONTENT_TYPE_JSON = "application/json"
12
+
13
+ CONTENT_ENCODING = "Content-Encoding"
14
+ CONTENT_ENCODING_GZIP = "gzip"
15
+
16
+ RECORD_TYPE_LOGS = "opentelemetry_logs"
17
+ RECORD_TYPE_METRICS = "opentelemetry_metrics"
18
+ RECORD_TYPE_TRACES = "opentelemetry_traces"
19
+
20
+ PLACEHOLDER_TYPE = "${type}"
21
+ PLACEHOLDER_TYPE_LOGS = "logs"
22
+ PLACEHOLDER_TYPE_METRICS = "metrics"
23
+ PLACEHOLDER_TYPE_TRACES = "traces"
24
+
25
+ TLS_VERSIONS_MAP =
26
+ begin
27
+ map = {
28
+ TLSv1: OpenSSL::SSL::TLS1_VERSION,
29
+ TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION,
30
+ TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION
31
+ }
32
+ map[:TLSv1_3] = OpenSSL::SSL::TLS1_3_VERSION if defined?(OpenSSL::SSL::TLS1_3_VERSION)
33
+ map.freeze
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/opentelemetry/response"
4
+ require "opentelemetry/proto/collector/logs/v1/logs_service_services_pb"
5
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb"
6
+ require "opentelemetry/proto/collector/trace/v1/trace_service_services_pb"
7
+
8
+ module Fluent::Plugin::Opentelemetry
9
+ class GrpcInputHandler
10
+ class ServiceHandler
11
+ class Logs < Opentelemetry::Proto::Collector::Logs::V1::LogsService::Service
12
+ def callback=(block)
13
+ @callback = block
14
+ end
15
+
16
+ def export(req, _call)
17
+ @callback.call(req)
18
+ end
19
+ end
20
+
21
+ class Metrics < Opentelemetry::Proto::Collector::Metrics::V1::MetricsService::Service
22
+ def callback=(block)
23
+ @callback = block
24
+ end
25
+
26
+ def export(req, _call)
27
+ @callback.call(req)
28
+ end
29
+ end
30
+
31
+ class Traces < Opentelemetry::Proto::Collector::Trace::V1::TraceService::Service
32
+ def callback=(block)
33
+ @callback = block
34
+ end
35
+
36
+ def export(req, _call)
37
+ @callback.call(req)
38
+ end
39
+ end
40
+ end
41
+
42
+ class ExceptionInterceptor < GRPC::ServerInterceptor
43
+ def request_response(request:, call:, method:)
44
+ # call actual service
45
+ yield
46
+ rescue StandardError => e
47
+ puts "[#{method}] Error: #{e.message}"
48
+ raise
49
+ end
50
+ end
51
+
52
+ def initialize(grpc_config, logger)
53
+ @grpc_config = grpc_config
54
+ @logger = logger
55
+ end
56
+
57
+ def run(logs:, metrics:, traces:)
58
+ @server = GRPC::RpcServer.new(interceptors: [ExceptionInterceptor.new])
59
+ @server.add_http2_port("#{@grpc_config.bind}:#{@grpc_config.port}", :this_port_is_insecure)
60
+
61
+ logs_handler = ServiceHandler::Logs.new
62
+ logs_handler.callback = lambda { |request|
63
+ logs.call(request.to_json)
64
+ Fluent::Plugin::Opentelemetry::Response::Logs.build
65
+ }
66
+ @server.handle(logs_handler)
67
+
68
+ metrics_handler = ServiceHandler::Metrics.new
69
+ metrics_handler.callback = lambda { |request|
70
+ metrics.call(request.to_json)
71
+ Fluent::Plugin::Opentelemetry::Response::Metrics.build
72
+ }
73
+ @server.handle(metrics_handler)
74
+
75
+ traces_handler = ServiceHandler::Traces.new
76
+ traces_handler.callback = lambda { |request|
77
+ traces.call(request.to_json)
78
+ Fluent::Plugin::Opentelemetry::Response::Traces.build
79
+ }
80
+ @server.handle(traces_handler)
81
+
82
+ @server.run_till_terminated
83
+ end
84
+
85
+ def stop
86
+ @server.stop
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025- Shizuo Fujita
4
+ # Copyright 2026 Radiant Logic, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ #
9
+ # Modified by Radiant Logic, Inc. to export prepared OTLP logs requests while
10
+ # retaining the upstream relay path.
11
+
12
+ require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
13
+ require "opentelemetry/proto/collector/logs/v1/logs_service_services_pb"
14
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
15
+ require "opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb"
16
+ require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
17
+ require "opentelemetry/proto/collector/trace/v1/trace_service_services_pb"
18
+
19
+ require "fluent/plugin/opentelemetry/constant"
20
+ require "fluent/plugin/opentelemetry/output_response"
21
+ require "google/protobuf"
22
+
23
+ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
24
+ include Fluent::Plugin::Opentelemetry::OutputResponse
25
+
26
+ RETRYABLE_CODES = %i[CANCELLED DEADLINE_EXCEEDED ABORTED OUT_OF_RANGE UNAVAILABLE DATA_LOSS RESOURCE_EXHAUSTED].map do |name|
27
+ GRPC::Core::StatusCodes.const_get(name)
28
+ end.freeze
29
+ CODE_NAMES = GRPC::Core::StatusCodes.constants.to_h do |name|
30
+ [GRPC::Core::StatusCodes.const_get(name), name.to_s]
31
+ end.freeze
32
+
33
+ class ServiceStub
34
+ class Logs
35
+ def initialize(host, creds, **kw)
36
+ @stub = Opentelemetry::Proto::Collector::Logs::V1::LogsService::Stub.new(host, creds, **kw)
37
+ end
38
+
39
+ def export(json, **kw)
40
+ message = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode_json(json)
41
+ export_request(message, **kw)
42
+ end
43
+
44
+ def export_request(message, **kw)
45
+ @stub.export(message, **kw)
46
+ end
47
+ end
48
+
49
+ class Metrics
50
+ def initialize(host, creds, **kw)
51
+ @stub = Opentelemetry::Proto::Collector::Metrics::V1::MetricsService::Stub.new(host, creds, **kw)
52
+ end
53
+
54
+ def export(json, **kw)
55
+ message = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode_json(json)
56
+ @stub.export(message, **kw)
57
+ end
58
+ end
59
+
60
+ class Traces
61
+ def initialize(host, creds, **kw)
62
+ @stub = Opentelemetry::Proto::Collector::Trace::V1::TraceService::Stub.new(host, creds, **kw)
63
+ end
64
+
65
+ def export(json, **kw)
66
+ message = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode_json(json)
67
+ @stub.export(message, **kw)
68
+ end
69
+ end
70
+ end
71
+
72
+ def initialize(grpc_config, transport_config, logger, headers = {})
73
+ @grpc_config = grpc_config
74
+ @logger = logger
75
+ @headers = headers
76
+ @metadata = headers.transform_keys(&:downcase).reject { |key, _| %w[content-type content-encoding].include?(key) }
77
+
78
+ unless grpc_config.endpoint.match?(%r{\A(?:\[[^\]]+\]|[^:/\s]+):[0-9]+\z})
79
+ raise Fluent::ConfigError, "gRPC endpoint must be host:port (without ://); use <transport tls> for TLS"
80
+ end
81
+ if transport_config&.insecure
82
+ raise Fluent::ConfigError, "certificate verification cannot be turned off for gRPC; set ca_path to the server's CA instead"
83
+ end
84
+
85
+ credential = channel_credentials(transport_config)
86
+ channel_args = {}
87
+ if grpc_config.compress == :gzip
88
+ channel_args = GRPC::Core::CompressionOptions.new(default_algorithm: :gzip).to_channel_arg_hash
89
+ 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
+ @services = {
94
+ Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS => ServiceStub::Logs.new(grpc_config.endpoint, credential, channel_args: channel_args),
95
+ Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS => ServiceStub::Metrics.new(grpc_config.endpoint, credential, channel_args: channel_args),
96
+ Fluent::Plugin::Opentelemetry::RECORD_TYPE_TRACES => ServiceStub::Traces.new(grpc_config.endpoint, credential, channel_args: channel_args)
97
+ }
98
+ end
99
+
100
+ def export(record)
101
+ service = @services[record["type"]]
102
+ raise Fluent::UnrecoverableError, "got unknown record type '#{record['type']}'" unless service
103
+
104
+ response = service.export(record["message"], **call_options)
105
+ warn_partial_success(response) if record["type"] == Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS
106
+ rescue Google::Protobuf::ParseError => e
107
+ raise Fluent::UnrecoverableError, e.message
108
+ rescue GRPC::BadStatus => e
109
+ # Preserve upstream relay retry behavior without exposing echoed credentials.
110
+ raise GRPC::BadStatus.new_status_exception(e.code, redact_headers(e.details)), cause: nil
111
+ end
112
+
113
+ def export_logs(request)
114
+ response = logs_service.export_request(request, **call_options)
115
+ warn_partial_success(response)
116
+ rescue GRPC::BadStatus => e
117
+ code_name = CODE_NAMES.fetch(e.code, e.code.to_s)
118
+ message = "OTLP gRPC export failed: #{code_name}: #{redact_headers(e.details)}"
119
+ error_class = RETRYABLE_CODES.include?(e.code) ? Fluent::Plugin::OpentelemetryOutput::RetryableResponse : Fluent::UnrecoverableError
120
+ # Fluentd logs errors when retrying chunks. Do not retain a cause containing
121
+ # unredacted server details or response metadata.
122
+ raise error_class, message, cause: nil
123
+ end
124
+
125
+ private
126
+
127
+ def logs_service
128
+ @services.fetch(Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS)
129
+ end
130
+
131
+ def call_options
132
+ { deadline: Time.now + @grpc_config.timeout, metadata: @metadata }
133
+ end
134
+
135
+ def channel_credentials(transport)
136
+ return :this_channel_is_insecure unless transport&.protocol == :tls
137
+
138
+ ca_pem = read_pem(transport.ca_path)
139
+ key_pem = read_pem(transport.private_key_path)
140
+ cert_pem = read_pem(transport.cert_path)
141
+ if key_pem.nil? != cert_pem.nil?
142
+ raise Fluent::ConfigError, "gRPC TLS requires both cert_path and private_key_path for a client certificate"
143
+ end
144
+ if key_pem && transport.private_key_passphrase
145
+ begin
146
+ key_pem = OpenSSL::PKey.read(key_pem, transport.private_key_passphrase).to_pem
147
+ rescue OpenSSL::OpenSSLError, ArgumentError
148
+ raise Fluent::ConfigError, "could not decrypt gRPC TLS private_key_path '#{transport.private_key_path}'", cause: nil
149
+ end
150
+ end
151
+ GRPC::Core::ChannelCredentials.new(ca_pem, key_pem, cert_pem)
152
+ end
153
+
154
+ def read_pem(path)
155
+ return nil unless path
156
+
157
+ File.read(path)
158
+ rescue SystemCallError, IOError
159
+ raise Fluent::ConfigError, "could not read gRPC TLS file '#{path}'", cause: nil
160
+ end
161
+ end