fluent-plugin-opentelemetry 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/.rubocop.yml +75 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +202 -0
- data/README.md +154 -0
- data/Rakefile +35 -0
- data/TODO.md +108 -0
- data/lib/fluent/plugin/in_opentelemetry.rb +211 -0
- data/lib/fluent/plugin/opentelemetry/constant.rb +29 -0
- data/lib/fluent/plugin/opentelemetry/request.rb +67 -0
- data/lib/fluent/plugin/opentelemetry/response.rb +90 -0
- data/lib/fluent/plugin/opentelemetry/service_handler.rb +37 -0
- data/lib/fluent/plugin/opentelemetry/service_stub.rb +45 -0
- data/lib/fluent/plugin/out_opentelemetry.rb +207 -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 +140 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fluent/plugin/input"
|
4
|
+
require "fluent/plugin/opentelemetry/constant"
|
5
|
+
require "fluent/plugin/opentelemetry/request"
|
6
|
+
require "fluent/plugin/opentelemetry/response"
|
7
|
+
require "fluent/plugin/opentelemetry/service_handler"
|
8
|
+
require "fluent/plugin_helper/http_server"
|
9
|
+
require "fluent/plugin_helper/thread"
|
10
|
+
|
11
|
+
require "zlib"
|
12
|
+
|
13
|
+
unless Fluent::PluginHelper::HttpServer::Request.method_defined?(:headers)
|
14
|
+
# This API was introduced at fluentd v1.19.0.
|
15
|
+
# Ref. https://github.com/fluent/fluentd/pull/4903
|
16
|
+
# If we have supported v1.19.0+ only, we can remove this patch.
|
17
|
+
module Fluent::PluginHelper::HttpServer
|
18
|
+
module Extension
|
19
|
+
refine Request do
|
20
|
+
def headers
|
21
|
+
@request.headers
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
using Fluent::PluginHelper::HttpServer::Extension
|
28
|
+
end
|
29
|
+
|
30
|
+
module Fluent::Plugin
|
31
|
+
class OpentelemetryInput < Input
|
32
|
+
Fluent::Plugin.register_input("opentelemetry", self)
|
33
|
+
|
34
|
+
helpers :thread, :http_server
|
35
|
+
|
36
|
+
desc "The tag of the event."
|
37
|
+
config_param :tag, :string
|
38
|
+
|
39
|
+
config_section :http, required: false, multi: false, init: false, param_name: :http_config do
|
40
|
+
desc "The address to bind to."
|
41
|
+
config_param :bind, :string, default: "0.0.0.0"
|
42
|
+
desc "The port to listen to."
|
43
|
+
config_param :port, :integer, default: 4318
|
44
|
+
end
|
45
|
+
|
46
|
+
config_section :grpc, required: false, multi: false, init: false, param_name: :grpc_config do
|
47
|
+
desc "The address to bind to."
|
48
|
+
config_param :bind, :string, default: "0.0.0.0"
|
49
|
+
desc "The port to listen to."
|
50
|
+
config_param :port, :integer, default: 4317
|
51
|
+
end
|
52
|
+
|
53
|
+
config_section :transport, required: false, multi: false, init: false, param_name: :transport_config do
|
54
|
+
config_argument :protocol, :enum, list: [:tls], default: nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure(conf)
|
58
|
+
super
|
59
|
+
|
60
|
+
unless [@http_config, @grpc_config].any?
|
61
|
+
raise Fluent::ConfigError, "Please configure either <http> or <grpc> section, or both."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def start
|
66
|
+
super
|
67
|
+
|
68
|
+
if @http_config
|
69
|
+
http_handler = HttpHandler.new
|
70
|
+
http_server_create_http_server(:in_opentelemetry_http_server, addr: @http_config.bind, port: @http_config.port, logger: log) do |serv|
|
71
|
+
serv.post("/v1/logs") do |req|
|
72
|
+
http_handler.logs(req) { |record| router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_LOGS, message: record }) }
|
73
|
+
end
|
74
|
+
serv.post("/v1/metrics") do |req|
|
75
|
+
http_handler.metrics(req) { |record| router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_METRICS, message: record }) }
|
76
|
+
end
|
77
|
+
serv.post("/v1/traces") do |req|
|
78
|
+
http_handler.traces(req) { |record| router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_TRACES, message: record }) }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
if @grpc_config
|
84
|
+
thread_create(:in_opentelemetry_grpc_server) do
|
85
|
+
grpc_handler = GrpcHandler.new(@grpc_config, log)
|
86
|
+
grpc_handler.run(
|
87
|
+
logs: lambda { |record|
|
88
|
+
router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_LOGS, message: record })
|
89
|
+
},
|
90
|
+
metrics: lambda { |record|
|
91
|
+
router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_METRICS, message: record })
|
92
|
+
},
|
93
|
+
traces: lambda { |record|
|
94
|
+
router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_TRACES, message: record })
|
95
|
+
}
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class HttpHandler
|
102
|
+
def logs(req, &block)
|
103
|
+
common(req, Opentelemetry::Request::Logs, Opentelemetry::Response::Logs, &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
def metrics(req, &block)
|
107
|
+
common(req, Opentelemetry::Request::Metrics, Opentelemetry::Response::Metrics, &block)
|
108
|
+
end
|
109
|
+
|
110
|
+
def traces(req, &block)
|
111
|
+
common(req, Opentelemetry::Request::Traces, Opentelemetry::Response::Traces, &block)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def common(req, request_class, response_class)
|
117
|
+
content_type = req.headers["content-type"]
|
118
|
+
content_encoding = req.headers["content-encoding"]&.first
|
119
|
+
return response_unsupported_media_type unless valid_content_type?(content_type)
|
120
|
+
return response_bad_request(content_type) unless valid_content_encoding?(content_encoding)
|
121
|
+
|
122
|
+
body = req.body
|
123
|
+
body = Zlib::GzipReader.new(StringIO.new(body)).read if content_encoding == Opentelemetry::CONTENT_ENCODING_GZIP
|
124
|
+
|
125
|
+
begin
|
126
|
+
record = request_class.new(body).record
|
127
|
+
rescue Google::Protobuf::ParseError
|
128
|
+
# The format in request body does not comply with the OpenTelemetry protocol.
|
129
|
+
return response_bad_request(content_type)
|
130
|
+
end
|
131
|
+
|
132
|
+
yield record
|
133
|
+
|
134
|
+
res = response_class.new
|
135
|
+
response(200, content_type, res.body(type: Opentelemetry::Response.type(content_type)))
|
136
|
+
end
|
137
|
+
|
138
|
+
def valid_content_type?(content_type)
|
139
|
+
case content_type
|
140
|
+
when Opentelemetry::CONTENT_TYPE_PROTOBUF, Opentelemetry::CONTENT_TYPE_JSON
|
141
|
+
true
|
142
|
+
else
|
143
|
+
false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def valid_content_encoding?(content_encoding)
|
148
|
+
return true if content_encoding.nil?
|
149
|
+
|
150
|
+
content_encoding == Opentelemetry::CONTENT_ENCODING_GZIP
|
151
|
+
end
|
152
|
+
|
153
|
+
def response(code, content_type, body)
|
154
|
+
[code, { Opentelemetry::CONTENT_TYPE => content_type }, body]
|
155
|
+
end
|
156
|
+
|
157
|
+
def response_unsupported_media_type
|
158
|
+
response(415, Opentelemetry::CONTENT_TYPE_PAIN, "415 unsupported media type, supported: [application/json, application/x-protobuf]")
|
159
|
+
end
|
160
|
+
|
161
|
+
def response_bad_request(content_type)
|
162
|
+
response(400, content_type, "") # TODO: fix body message
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
class GrpcHandler
|
167
|
+
class ExceptionInterceptor < GRPC::ServerInterceptor
|
168
|
+
def request_response(request:, call:, method:)
|
169
|
+
# call actual service
|
170
|
+
yield
|
171
|
+
rescue StandardError => e
|
172
|
+
puts "[#{method}] Error: #{e.message}"
|
173
|
+
raise
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def initialize(grpc_config, logger)
|
178
|
+
@grpc_config = grpc_config
|
179
|
+
@logger = logger
|
180
|
+
end
|
181
|
+
|
182
|
+
def run(logs:, metrics:, traces:)
|
183
|
+
server = GRPC::RpcServer.new(interceptors: [ExceptionInterceptor.new])
|
184
|
+
server.add_http2_port("#{@grpc_config.bind}:#{@grpc_config.port}", :this_port_is_insecure)
|
185
|
+
|
186
|
+
logs_handler = Opentelemetry::ServiceHandler::Logs.new
|
187
|
+
logs_handler.callback = lambda { |request|
|
188
|
+
logs.call(request.to_json)
|
189
|
+
Opentelemetry::Response::Logs.build
|
190
|
+
}
|
191
|
+
server.handle(logs_handler)
|
192
|
+
|
193
|
+
metrics_handler = Opentelemetry::ServiceHandler::Metrics.new
|
194
|
+
metrics_handler.callback = lambda { |request|
|
195
|
+
metrics.call(request.to_json)
|
196
|
+
Opentelemetry::Response::Metrics.build
|
197
|
+
}
|
198
|
+
server.handle(metrics_handler)
|
199
|
+
|
200
|
+
traces_handler = Opentelemetry::ServiceHandler::Traces.new
|
201
|
+
traces_handler.callback = lambda { |request|
|
202
|
+
traces.call(request.to_json)
|
203
|
+
Opentelemetry::Response::Traces.build
|
204
|
+
}
|
205
|
+
server.handle(traces_handler)
|
206
|
+
|
207
|
+
server.run_till_terminated
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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_PAIN = "text/plain"
|
9
|
+
CONTENT_TYPE_PROTOBUF = "application/x-protobuf"
|
10
|
+
CONTENT_TYPE_JSON = "application/json"
|
11
|
+
|
12
|
+
CONTENT_ENCODING = "Content-Encoding"
|
13
|
+
CONTENT_ENCODING_GZIP = "gzip"
|
14
|
+
|
15
|
+
RECORD_TYPE_LOGS = "opentelemetry_logs"
|
16
|
+
RECORD_TYPE_METRICS = "opentelemetry_metrics"
|
17
|
+
RECORD_TYPE_TRACES = "opentelemetry_traces"
|
18
|
+
|
19
|
+
TLS_VERSIONS_MAP =
|
20
|
+
begin
|
21
|
+
map = {
|
22
|
+
TLSv1: OpenSSL::SSL::TLS1_VERSION,
|
23
|
+
TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION,
|
24
|
+
TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION
|
25
|
+
}
|
26
|
+
map[:TLSv1_3] = OpenSSL::SSL::TLS1_3_VERSION if defined?(OpenSSL::SSL::TLS1_3_VERSION)
|
27
|
+
map.freeze
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fluent/plugin/opentelemetry/constant"
|
4
|
+
require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
|
5
|
+
require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
|
6
|
+
require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
|
7
|
+
|
8
|
+
require "google/protobuf"
|
9
|
+
|
10
|
+
class Fluent::Plugin::Opentelemetry::Request
|
11
|
+
class Logs
|
12
|
+
def initialize(body)
|
13
|
+
@request =
|
14
|
+
if body.start_with?("{")
|
15
|
+
Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode_json(body, ignore_unknown_fields: true)
|
16
|
+
else
|
17
|
+
Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(body)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def body
|
22
|
+
@request.to_proto
|
23
|
+
end
|
24
|
+
|
25
|
+
def record
|
26
|
+
@request.to_json
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Metrics
|
31
|
+
def initialize(body)
|
32
|
+
@request =
|
33
|
+
if body.start_with?("{")
|
34
|
+
Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode_json(body, ignore_unknown_fields: true)
|
35
|
+
else
|
36
|
+
Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode(body)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def body
|
41
|
+
@request.to_proto
|
42
|
+
end
|
43
|
+
|
44
|
+
def record
|
45
|
+
@request.to_json
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Traces
|
50
|
+
def initialize(body)
|
51
|
+
@request =
|
52
|
+
if body.start_with?("{")
|
53
|
+
Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode_json(body, ignore_unknown_fields: true)
|
54
|
+
else
|
55
|
+
Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(body)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def body
|
60
|
+
@request.to_proto
|
61
|
+
end
|
62
|
+
|
63
|
+
def record
|
64
|
+
@request.to_json
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fluent/plugin/opentelemetry/constant"
|
4
|
+
require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
|
5
|
+
require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
|
6
|
+
require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
|
7
|
+
|
8
|
+
require "google/protobuf"
|
9
|
+
|
10
|
+
module Fluent::Plugin::Opentelemetry::Response
|
11
|
+
def self.type(content_type)
|
12
|
+
case content_type
|
13
|
+
when Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PROTOBUF
|
14
|
+
:protobuf
|
15
|
+
when Fluent::Plugin::Opentelemetry::CONTENT_TYPE_JSON
|
16
|
+
:json
|
17
|
+
else
|
18
|
+
raise "unknown content-type: #{content_type}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Logs
|
23
|
+
def self.build(rejected: 0, error: "")
|
24
|
+
Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceResponse.new(
|
25
|
+
partial_success: Opentelemetry::Proto::Collector::Logs::V1::ExportLogsPartialSuccess.new(
|
26
|
+
rejected_log_records: rejected,
|
27
|
+
error_message: error
|
28
|
+
)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(rejected: 0, error: "")
|
33
|
+
@response = Logs.build(rejected: rejected, error: error)
|
34
|
+
end
|
35
|
+
|
36
|
+
def body(type:)
|
37
|
+
if type == :protobuf
|
38
|
+
@response.to_proto
|
39
|
+
else
|
40
|
+
@response.to_json
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Metrics
|
46
|
+
def self.build(rejected: 0, error: "")
|
47
|
+
Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceResponse.new(
|
48
|
+
partial_success: Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsPartialSuccess.new(
|
49
|
+
rejected_data_points: rejected,
|
50
|
+
error_message: error
|
51
|
+
)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(rejected: 0, error: "")
|
56
|
+
@response = Metrics.build(rejected: rejected, error: error)
|
57
|
+
end
|
58
|
+
|
59
|
+
def body(type:)
|
60
|
+
if type == :protobuf
|
61
|
+
@response.to_proto
|
62
|
+
else
|
63
|
+
@response.to_json
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class Traces
|
69
|
+
def self.build(rejected: 0, error: "")
|
70
|
+
Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceResponse.new(
|
71
|
+
partial_success: Opentelemetry::Proto::Collector::Trace::V1::ExportTracePartialSuccess.new(
|
72
|
+
rejected_spans: rejected,
|
73
|
+
error_message: error
|
74
|
+
)
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def initialize(rejected: 0, error: "")
|
79
|
+
@response = Traces.build(rejected: rejected, error: error)
|
80
|
+
end
|
81
|
+
|
82
|
+
def body(type:)
|
83
|
+
if type == :protobuf
|
84
|
+
@response.to_proto
|
85
|
+
else
|
86
|
+
@response.to_json
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry/proto/collector/logs/v1/logs_service_services_pb"
|
4
|
+
require "opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb"
|
5
|
+
require "opentelemetry/proto/collector/trace/v1/trace_service_services_pb"
|
6
|
+
|
7
|
+
class Fluent::Plugin::Opentelemetry::ServiceHandler
|
8
|
+
class Logs < Opentelemetry::Proto::Collector::Logs::V1::LogsService::Service
|
9
|
+
def callback=(block)
|
10
|
+
@callback = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def export(req, _call)
|
14
|
+
@callback.call(req)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Metrics < Opentelemetry::Proto::Collector::Metrics::V1::MetricsService::Service
|
19
|
+
def callback=(block)
|
20
|
+
@callback = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def export(req, _call)
|
24
|
+
@callback.call(req)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Traces < Opentelemetry::Proto::Collector::Trace::V1::TraceService::Service
|
29
|
+
def callback=(block)
|
30
|
+
@callback = block
|
31
|
+
end
|
32
|
+
|
33
|
+
def export(req, _call)
|
34
|
+
@callback.call(req)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "opentelemetry/proto/collector/logs/v1/logs_service_pb"
|
4
|
+
require "opentelemetry/proto/collector/logs/v1/logs_service_services_pb"
|
5
|
+
require "opentelemetry/proto/collector/metrics/v1/metrics_service_pb"
|
6
|
+
require "opentelemetry/proto/collector/metrics/v1/metrics_service_services_pb"
|
7
|
+
require "opentelemetry/proto/collector/trace/v1/trace_service_pb"
|
8
|
+
require "opentelemetry/proto/collector/trace/v1/trace_service_services_pb"
|
9
|
+
|
10
|
+
require "grpc"
|
11
|
+
|
12
|
+
class Fluent::Plugin::Opentelemetry::ServiceStub
|
13
|
+
class Logs
|
14
|
+
def initialize(host, creds, **kw)
|
15
|
+
@stub = Opentelemetry::Proto::Collector::Logs::V1::LogsService::Stub.new(host, creds, **kw)
|
16
|
+
end
|
17
|
+
|
18
|
+
def export(json)
|
19
|
+
message = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode_json(json)
|
20
|
+
@stub.export(message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Metrics
|
25
|
+
def initialize(host, creds, **kw)
|
26
|
+
@stub = Opentelemetry::Proto::Collector::Metrics::V1::MetricsService::Stub.new(host, creds, **kw)
|
27
|
+
end
|
28
|
+
|
29
|
+
def export(json)
|
30
|
+
message = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode_json(json)
|
31
|
+
@stub.export(message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Traces
|
36
|
+
def initialize(host, creds, **kw)
|
37
|
+
@stub = Opentelemetry::Proto::Collector::Trace::V1::TraceService::Stub.new(host, creds, **kw)
|
38
|
+
end
|
39
|
+
|
40
|
+
def export(json)
|
41
|
+
message = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode_json(json)
|
42
|
+
@stub.export(message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fluent/plugin/opentelemetry/constant"
|
4
|
+
require "fluent/plugin/opentelemetry/request"
|
5
|
+
require "fluent/plugin/opentelemetry/service_stub"
|
6
|
+
require "fluent/plugin/output"
|
7
|
+
|
8
|
+
require "excon"
|
9
|
+
require "grpc"
|
10
|
+
require "json"
|
11
|
+
require "stringio"
|
12
|
+
require "zlib"
|
13
|
+
|
14
|
+
module Fluent::Plugin
|
15
|
+
class OpentelemetryOutput < Output
|
16
|
+
class RetryableResponse < StandardError; end
|
17
|
+
|
18
|
+
Fluent::Plugin.register_output("opentelemetry", self)
|
19
|
+
|
20
|
+
helpers :server
|
21
|
+
|
22
|
+
config_section :buffer do
|
23
|
+
config_set_default :chunk_keys, ["tag"]
|
24
|
+
end
|
25
|
+
|
26
|
+
config_section :http, required: false, multi: false, init: false, param_name: :http_config do
|
27
|
+
desc "The endpoint"
|
28
|
+
config_param :endpoint, :string, default: "http://127.0.0.1:4318"
|
29
|
+
desc "The proxy for HTTP request"
|
30
|
+
config_param :proxy, :string, default: ENV["HTTP_PROXY"] || ENV["http_proxy"]
|
31
|
+
|
32
|
+
desc "Raise UnrecoverableError when the response is non success, 4xx/5xx"
|
33
|
+
config_param :error_response_as_unrecoverable, :bool, default: true
|
34
|
+
desc "The list of retryable response code"
|
35
|
+
config_param :retryable_response_codes, :array, value_type: :integer, default: [429, 502, 503, 504]
|
36
|
+
|
37
|
+
desc "Compress request body"
|
38
|
+
config_param :compress, :enum, list: %i[text gzip], default: :text
|
39
|
+
|
40
|
+
desc "The read timeout in seconds"
|
41
|
+
config_param :read_timeout, :integer, default: 60
|
42
|
+
desc "The write timeout in seconds"
|
43
|
+
config_param :write_timeout, :integer, default: 60
|
44
|
+
desc "The connect timeout in seconds"
|
45
|
+
config_param :connect_timeout, :integer, default: 60
|
46
|
+
end
|
47
|
+
|
48
|
+
config_section :grpc, required: false, multi: false, init: false, param_name: :grpc_config do
|
49
|
+
desc "The endpoint"
|
50
|
+
config_param :endpoint, :string, default: "127.0.0.1:4317"
|
51
|
+
end
|
52
|
+
|
53
|
+
config_section :transport, required: false, multi: false, init: false, param_name: :transport_config do
|
54
|
+
config_argument :protocol, :enum, list: [:tls], default: nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure(conf)
|
58
|
+
super
|
59
|
+
|
60
|
+
unless [@http_config, @grpc_config].one?
|
61
|
+
raise Fluent::ConfigError, "Please configure either <http> or <grpc> section."
|
62
|
+
end
|
63
|
+
|
64
|
+
@http_handler = HttpHandler.new(@http_config, @transport_config, log) if @http_config
|
65
|
+
@grpc_handler = GrpcHandler.new(@grpc_config, @transport_config, log) if @grpc_config
|
66
|
+
end
|
67
|
+
|
68
|
+
def multi_workers_ready?
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def format(tag, time, record)
|
73
|
+
JSON.generate(record)
|
74
|
+
end
|
75
|
+
|
76
|
+
def write(chunk)
|
77
|
+
record = JSON.parse(chunk.read)
|
78
|
+
|
79
|
+
if @grpc_handler
|
80
|
+
@grpc_handler.export(record)
|
81
|
+
else
|
82
|
+
@http_handler.export(record)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class HttpHandler
|
87
|
+
def initialize(http_config, transport_config, logger)
|
88
|
+
@http_config = http_config
|
89
|
+
@transport_config = transport_config
|
90
|
+
@logger = logger
|
91
|
+
|
92
|
+
@tls_settings = {}
|
93
|
+
if @transport_config.protocol == :tls
|
94
|
+
@tls_settings[:client_cert] = @transport_config.cert_path
|
95
|
+
@tls_settings[:client_key] = @transport_config.private_key_path
|
96
|
+
@tls_settings[:client_key_pass] = @transport_config.private_key_passphrase
|
97
|
+
@tls_settings[:ssl_min_version] = Opentelemetry::TLS_VERSIONS_MAP[@transport_config.min_version]
|
98
|
+
@tls_settings[:ssl_max_version] = Opentelemetry::TLS_VERSIONS_MAP[@transport_config.max_version]
|
99
|
+
end
|
100
|
+
|
101
|
+
@timeout_settings = {
|
102
|
+
read_timeout: http_config.read_timeout,
|
103
|
+
write_timeout: http_config.write_timeout,
|
104
|
+
connect_timeout: http_config.connect_timeout
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def export(record)
|
109
|
+
uri, connection = create_http_connection(record)
|
110
|
+
response = connection.post
|
111
|
+
|
112
|
+
if response.status != 200
|
113
|
+
if response.status == 400
|
114
|
+
# The client MUST NOT retry the request when it receives HTTP 400 Bad Request response.
|
115
|
+
raise Fluent::UnrecoverableError, "got unrecoverable error response from '#{uri}', response code is #{response.status}"
|
116
|
+
end
|
117
|
+
|
118
|
+
if @http_config.retryable_response_codes&.include?(response.status)
|
119
|
+
raise RetryableResponse, "got retryable error response from '#{uri}', response code is #{response.status}"
|
120
|
+
end
|
121
|
+
if @http_config.error_response_as_unrecoverable
|
122
|
+
raise Fluent::UnrecoverableError, "got unrecoverable error response from '#{uri}', response code is #{response.status}"
|
123
|
+
else
|
124
|
+
@logger.error "got error response from '#{uri}', response code is #{response.status}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def http_logs_endpoint
|
132
|
+
"#{@http_config.endpoint}/v1/logs"
|
133
|
+
end
|
134
|
+
|
135
|
+
def http_metrics_endpoint
|
136
|
+
"#{@http_config.endpoint}/v1/metrics"
|
137
|
+
end
|
138
|
+
|
139
|
+
def http_traces_endpoint
|
140
|
+
"#{@http_config.endpoint}/v1/traces"
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_http_connection(record)
|
144
|
+
msg = record["message"]
|
145
|
+
|
146
|
+
begin
|
147
|
+
case record["type"]
|
148
|
+
when Opentelemetry::RECORD_TYPE_LOGS
|
149
|
+
uri = http_logs_endpoint
|
150
|
+
body = Opentelemetry::Request::Logs.new(msg).body
|
151
|
+
when Opentelemetry::RECORD_TYPE_METRICS
|
152
|
+
uri = http_metrics_endpoint
|
153
|
+
body = Opentelemetry::Request::Metrics.new(msg).body
|
154
|
+
when Opentelemetry::RECORD_TYPE_TRACES
|
155
|
+
uri = http_traces_endpoint
|
156
|
+
body = Opentelemetry::Request::Traces.new(msg).body
|
157
|
+
end
|
158
|
+
rescue Google::Protobuf::ParseError => e
|
159
|
+
# The message format does not comply with the OpenTelemetry protocol.
|
160
|
+
raise ::Fluent::UnrecoverableError, e.message
|
161
|
+
end
|
162
|
+
|
163
|
+
headers = { Opentelemetry::CONTENT_TYPE => Opentelemetry::CONTENT_TYPE_PROTOBUF }
|
164
|
+
if @http_config.compress == :gzip
|
165
|
+
headers[Opentelemetry::CONTENT_ENCODING] = Opentelemetry::CONTENT_ENCODING_GZIP
|
166
|
+
gz = Zlib::GzipWriter.new(StringIO.new)
|
167
|
+
gz << body
|
168
|
+
body = gz.close.string
|
169
|
+
end
|
170
|
+
|
171
|
+
Excon.defaults[:ssl_verify_peer] = false if @transport_config.insecure
|
172
|
+
connection = Excon.new(uri, body: body, headers: headers, proxy: @http_config.proxy, persistent: true, **@tls_settings, **@timeout_settings)
|
173
|
+
[uri, connection]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class GrpcHandler
|
178
|
+
def initialize(grpc_config, transport_config, logger)
|
179
|
+
@grpc_config = grpc_config
|
180
|
+
@transport_config = transport_config
|
181
|
+
@logger = logger
|
182
|
+
end
|
183
|
+
|
184
|
+
def export(record)
|
185
|
+
msg = record["message"]
|
186
|
+
|
187
|
+
credential = :this_channel_is_insecure
|
188
|
+
|
189
|
+
case record["type"]
|
190
|
+
when Opentelemetry::RECORD_TYPE_LOGS
|
191
|
+
service = Opentelemetry::ServiceStub::Logs.new(@grpc_config.endpoint, credential)
|
192
|
+
when Opentelemetry::RECORD_TYPE_METRICS
|
193
|
+
service = Opentelemetry::ServiceStub::Metrics.new(@grpc_config.endpoint, credential)
|
194
|
+
when Opentelemetry::RECORD_TYPE_TRACES
|
195
|
+
service = Opentelemetry::ServiceStub::Traces.new(@grpc_config.endpoint, credential)
|
196
|
+
end
|
197
|
+
|
198
|
+
begin
|
199
|
+
service.export(msg)
|
200
|
+
rescue Google::Protobuf::ParseError => e
|
201
|
+
# The message format does not comply with the OpenTelemetry protocol.
|
202
|
+
raise ::Fluent::UnrecoverableError, e.message
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|