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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +50 -0
- data/LICENSE +202 -0
- data/NOTICE +23 -0
- data/README.md +278 -0
- data/lib/fluent/plugin/in_opentelemetry.rb +118 -0
- data/lib/fluent/plugin/in_opentelemetry_metrics.rb +285 -0
- data/lib/fluent/plugin/opentelemetry/constant.rb +35 -0
- data/lib/fluent/plugin/opentelemetry/grpc_input_handler.rb +89 -0
- data/lib/fluent/plugin/opentelemetry/grpc_output_handler.rb +161 -0
- data/lib/fluent/plugin/opentelemetry/http_input_handler.rb +177 -0
- data/lib/fluent/plugin/opentelemetry/http_output_handler.rb +157 -0
- data/lib/fluent/plugin/opentelemetry/log_record_builder.rb +229 -0
- data/lib/fluent/plugin/opentelemetry/output_response.rb +21 -0
- data/lib/fluent/plugin/opentelemetry/request.rb +67 -0
- data/lib/fluent/plugin/opentelemetry/response.rb +90 -0
- data/lib/fluent/plugin/opentelemetry/version.rb +9 -0
- data/lib/fluent/plugin/opentelemetry_radiant/version.rb +14 -0
- data/lib/fluent/plugin/out_opentelemetry.rb +11 -0
- data/lib/fluent/plugin/out_opentelemetry_radiant.rb +205 -0
- data/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb +27 -0
- data/lib/opentelemetry/proto/collector/logs/v1/logs_service_services_pb.rb +48 -0
- data/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb +27 -0
- data/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb.rb +48 -0
- data/lib/opentelemetry/proto/collector/profiles/v1development/profiles_service_pb.rb +27 -0
- data/lib/opentelemetry/proto/collector/profiles/v1development/profiles_service_services_pb.rb +47 -0
- data/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb +27 -0
- data/lib/opentelemetry/proto/collector/trace/v1/trace_service_services_pb.rb +48 -0
- data/lib/opentelemetry/proto/common/v1/common_pb.rb +26 -0
- data/lib/opentelemetry/proto/logs/v1/logs_pb.rb +29 -0
- data/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb +41 -0
- data/lib/opentelemetry/proto/profiles/v1development/profiles_pb.rb +36 -0
- data/lib/opentelemetry/proto/resource/v1/resource_pb.rb +23 -0
- data/lib/opentelemetry/proto/trace/v1/trace_pb.rb +33 -0
- 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
|