fluent-plugin-opentelemetry 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8f0a6c1b314b0737a832920decef09f6066acd10d578271b65edc4bc535b540
4
- data.tar.gz: b5b1fd56d7c410ac7decfc50b67459277cb14d1a20b762bfdc414edaeaf1f4ef
3
+ metadata.gz: 7e88abfcc25a1079994f2801f2b32665bb39bb32408cff6efb91125ef9b60149
4
+ data.tar.gz: 2b64e708b7ea494e4c35e4218c4f33e2ca15f04ac55672d95dc9d7456435303c
5
5
  SHA512:
6
- metadata.gz: fe86042e9aa128f24f0479332aee03e753ef82e6e04fe34b6d49cc1226753f2ca26a775d5b938cce67cf02b710f5230c8106f278cb5afc19dc69faefc439a3c5
7
- data.tar.gz: 65c174366007116fd6b7fa2422914224414f5ec7c976732550d448fd219edd25c7b413e1a077ad243ab341d2e2fbd688369e1c525ba417c8418c32d5d17532b2
6
+ metadata.gz: 26fde29bef629246d797440d306b863ed6ed8cd57d0871f6c66b52b0c3109b453e6d836cb5b99d5b545d778cbb9f4893fdc37166499baf61e92de5bf478f60c5
7
+ data.tar.gz: 1510198977d2f3b71a58a7a85e9a948b40716bf036c84968b8bb5c1507d63553fd1a06dbc70633d788d110dbc510ef898710eaaf2727d76adfd8baba9b5c9e81
data/.rubocop.yml CHANGED
@@ -1,3 +1,7 @@
1
+ plugins:
2
+ - rubocop-performance
3
+ - rubocop-fluentd
4
+
1
5
  AllCops:
2
6
  Exclude:
3
7
  - 'lib/opentelemetry/**/*'
@@ -6,12 +10,27 @@ AllCops:
6
10
  SuggestExtensions: false
7
11
  TargetRubyVersion: 3.2
8
12
 
13
+ # rubocop-fluentd
14
+ Lint/FluentdPluginLogScope:
15
+ Enabled: true
16
+ Lint/FluentdPluginConfigParamDefaultTime:
17
+ Enabled: true
18
+ Performance/FluentdPluginLogStringInterpolation:
19
+ Enabled: true
20
+
21
+ # rubocop
22
+ Gemspec/DevelopmentDependencies:
23
+ Enabled: false
24
+
9
25
  Layout/LineLength:
10
26
  Enabled: false
11
27
 
12
28
  Lint/UnusedMethodArgument:
13
29
  Enabled: false
14
30
 
31
+ Lint/SuppressedException:
32
+ Enabled: false
33
+
15
34
  Metrics:
16
35
  Enabled: false
17
36
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-07-23
4
+
5
+ - in_opentelemetry: add ${type} placeholder support in tag parameter (#8)
6
+ - in_opentelemetry: use String for record keys (#6)
7
+
8
+ ## [0.2.0] - 2025-06-26
9
+
10
+ - Make gRPC sending and receiving optional (#4)
11
+ - To use gRPC sending and receiving, you need to install `grpc` gem manually.
12
+
3
13
  ## [0.1.0] - 2025-03-31
4
14
 
5
15
  - Initial release
data/README.md CHANGED
@@ -34,6 +34,33 @@ To receive data, this plugin requires `<http>` or `<grpc>` section, or both.
34
34
  |-----------|--------|----------------------|----------|
35
35
  | tag | string | The tag of the event | required |
36
36
 
37
+ `tag` parameter supports `${type}` placeholder and it will be expanded with data type.
38
+
39
+ * log data
40
+ * `${type}` will be replaced to `logs`
41
+ * metric data
42
+ * `${type}` will be replaced to `metrics`
43
+ * trace data
44
+ * `${type}` will be replaced to `traces`
45
+
46
+ This can be used to change the output destination for each message type.
47
+
48
+ Example:
49
+
50
+ ```
51
+ <source>
52
+ @type opentelemetry
53
+
54
+ # Expand to opentelemetry.logs, opentelemetry.metrics, opentelemetry.traces according to received data.
55
+ tag opentelemetry.${type}
56
+
57
+ <http>
58
+ bind 127.0.0.1
59
+ port 4318
60
+ </http>
61
+ </source>
62
+ ```
63
+
37
64
  #### `<http>` section
38
65
 
39
66
  This requires to receive data via HTTP/HTTPS.
@@ -46,6 +73,7 @@ This requires to receive data via HTTP/HTTPS.
46
73
  #### `<grpc>` section
47
74
 
48
75
  This requires to receive data via gRPC.
76
+ It needs to install `grpc` gem manually to use this feature.
49
77
 
50
78
  > [!WARNING]
51
79
  > Now, gRPC feature status is experimental.
@@ -53,7 +81,7 @@ This requires to receive data via gRPC.
53
81
  | parameter | type | description | default |
54
82
  |-----------|---------|------------------------|-----------|
55
83
  | bind | string | The address to bind to | `0.0.0.0` |
56
- | port | integer | The port to listen to | `4318` |
84
+ | port | integer | The port to listen to | `4317` |
57
85
 
58
86
  * `<transport>` section
59
87
 
@@ -103,6 +131,7 @@ This requires to send data via HTTP/HTTPS.
103
131
  #### `<grpc>` section
104
132
 
105
133
  This requires to send data via gRPC.
134
+ It needs to install `grpc` gem manually to use this feature.
106
135
 
107
136
  > [!WARNING]
108
137
  > Now, gRPC feature status is experimental.
@@ -2,29 +2,15 @@
2
2
 
3
3
  require "fluent/plugin/input"
4
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"
5
+ require "fluent/plugin/opentelemetry/http_input_handler"
8
6
  require "fluent/plugin_helper/http_server"
9
7
  require "fluent/plugin_helper/thread"
10
8
 
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
9
+ begin
10
+ require "grpc"
26
11
 
27
- using Fluent::PluginHelper::HttpServer::Extension
12
+ require "fluent/plugin/opentelemetry/grpc_input_handler"
13
+ rescue LoadError
28
14
  end
29
15
 
30
16
  module Fluent::Plugin
@@ -57,6 +43,12 @@ module Fluent::Plugin
57
43
  def configure(conf)
58
44
  super
59
45
 
46
+ expand_tag_placeholders(@tag)
47
+
48
+ if @grpc_config && !defined?(GRPC)
49
+ raise Fluent::ConfigError, "To use gRPC feature, please install grpc gem such as 'fluent-gem install grpc'."
50
+ end
51
+
60
52
  unless [@http_config, @grpc_config].any?
61
53
  raise Fluent::ConfigError, "Please configure either <http> or <grpc> section, or both."
62
54
  end
@@ -66,146 +58,50 @@ module Fluent::Plugin
66
58
  super
67
59
 
68
60
  if @http_config
69
- http_handler = HttpHandler.new
61
+ http_handler = Opentelemetry::HttpInputHandler.new
70
62
  http_server_create_http_server(:in_opentelemetry_http_server, addr: @http_config.bind, port: @http_config.port, logger: log) do |serv|
71
63
  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 }) }
64
+ http_handler.logs(req) { |record| router.emit(tag_for(Opentelemetry::RECORD_TYPE_LOGS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_LOGS, "message" => record }) }
73
65
  end
74
66
  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 }) }
67
+ http_handler.metrics(req) { |record| router.emit(tag_for(Opentelemetry::RECORD_TYPE_METRICS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_METRICS, "message" => record }) }
76
68
  end
77
69
  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 }) }
70
+ http_handler.traces(req) { |record| router.emit(tag_for(Opentelemetry::RECORD_TYPE_TRACES), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_TRACES, "message" => record }) }
79
71
  end
80
72
  end
81
73
  end
82
74
 
83
75
  if @grpc_config
84
76
  thread_create(:in_opentelemetry_grpc_server) do
85
- grpc_handler = GrpcHandler.new(@grpc_config, log)
77
+ grpc_handler = Opentelemetry::GrpcInputHandler.new(@grpc_config, log)
86
78
  grpc_handler.run(
87
79
  logs: lambda { |record|
88
- router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_LOGS, message: record })
80
+ router.emit(tag_for(Opentelemetry::RECORD_TYPE_LOGS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_LOGS, "message" => record })
89
81
  },
90
82
  metrics: lambda { |record|
91
- router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_METRICS, message: record })
83
+ router.emit(tag_for(Opentelemetry::RECORD_TYPE_METRICS), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_METRICS, "message" => record })
92
84
  },
93
85
  traces: lambda { |record|
94
- router.emit(@tag, Fluent::EventTime.now, { type: Opentelemetry::RECORD_TYPE_TRACES, message: record })
86
+ router.emit(tag_for(Opentelemetry::RECORD_TYPE_TRACES), Fluent::EventTime.now, { "type" => Opentelemetry::RECORD_TYPE_TRACES, "message" => record })
95
87
  }
96
88
  )
97
89
  end
98
90
  end
99
91
  end
100
92
 
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
93
+ private
109
94
 
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
95
+ def expand_tag_placeholders(tag)
96
+ @expand_tag_placeholders ||= {
97
+ Opentelemetry::RECORD_TYPE_LOGS => tag.gsub(Opentelemetry::PLACEHOLDER_TYPE, Opentelemetry::PLACEHOLDER_TYPE_LOGS),
98
+ Opentelemetry::RECORD_TYPE_METRICS => tag.gsub(Opentelemetry::PLACEHOLDER_TYPE, Opentelemetry::PLACEHOLDER_TYPE_METRICS),
99
+ Opentelemetry::RECORD_TYPE_TRACES => tag.gsub(Opentelemetry::PLACEHOLDER_TYPE, Opentelemetry::PLACEHOLDER_TYPE_TRACES)
100
+ }
164
101
  end
165
102
 
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
103
+ def tag_for(type)
104
+ @expand_tag_placeholders[type]
209
105
  end
210
106
  end
211
107
  end
@@ -16,6 +16,11 @@ module Fluent::Plugin::Opentelemetry
16
16
  RECORD_TYPE_METRICS = "opentelemetry_metrics"
17
17
  RECORD_TYPE_TRACES = "opentelemetry_traces"
18
18
 
19
+ PLACEHOLDER_TYPE = "${type}"
20
+ PLACEHOLDER_TYPE_LOGS = "logs"
21
+ PLACEHOLDER_TYPE_METRICS = "metrics"
22
+ PLACEHOLDER_TYPE_TRACES = "traces"
23
+
19
24
  TLS_VERSIONS_MAP =
20
25
  begin
21
26
  map = {
@@ -0,0 +1,85 @@
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
+ end
85
+ end
@@ -0,0 +1,76 @@
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 "fluent/plugin/opentelemetry/constant"
11
+ require "google/protobuf"
12
+
13
+ class Fluent::Plugin::Opentelemetry::GrpcOutputHandler
14
+ class ServiceStub
15
+ class Logs
16
+ def initialize(host, creds, **kw)
17
+ @stub = Opentelemetry::Proto::Collector::Logs::V1::LogsService::Stub.new(host, creds, **kw)
18
+ end
19
+
20
+ def export(json)
21
+ message = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode_json(json)
22
+ @stub.export(message)
23
+ end
24
+ end
25
+
26
+ class Metrics
27
+ def initialize(host, creds, **kw)
28
+ @stub = Opentelemetry::Proto::Collector::Metrics::V1::MetricsService::Stub.new(host, creds, **kw)
29
+ end
30
+
31
+ def export(json)
32
+ message = Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode_json(json)
33
+ @stub.export(message)
34
+ end
35
+ end
36
+
37
+ class Traces
38
+ def initialize(host, creds, **kw)
39
+ @stub = Opentelemetry::Proto::Collector::Trace::V1::TraceService::Stub.new(host, creds, **kw)
40
+ end
41
+
42
+ def export(json)
43
+ message = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode_json(json)
44
+ @stub.export(message)
45
+ end
46
+ end
47
+ end
48
+
49
+ def initialize(grpc_config, transport_config, logger)
50
+ @grpc_config = grpc_config
51
+ @transport_config = transport_config
52
+ @logger = logger
53
+ end
54
+
55
+ def export(record)
56
+ msg = record["message"]
57
+
58
+ credential = :this_channel_is_insecure
59
+
60
+ case record["type"]
61
+ when Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS
62
+ service = ServiceStub::Logs.new(@grpc_config.endpoint, credential)
63
+ when Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS
64
+ service = ServiceStub::Metrics.new(@grpc_config.endpoint, credential)
65
+ when Fluent::Plugin::Opentelemetry::RECORD_TYPE_TRACES
66
+ service = ServiceStub::Traces.new(@grpc_config.endpoint, credential)
67
+ end
68
+
69
+ begin
70
+ service.export(msg)
71
+ rescue Google::Protobuf::ParseError => e
72
+ # The message format does not comply with the OpenTelemetry protocol.
73
+ raise ::Fluent::UnrecoverableError, e.message
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/opentelemetry/constant"
4
+ require "fluent/plugin/opentelemetry/request"
5
+ require "fluent/plugin/opentelemetry/response"
6
+ require "google/protobuf"
7
+ require "zlib"
8
+
9
+ unless Fluent::PluginHelper::HttpServer::Request.method_defined?(:headers)
10
+ # This API was introduced at fluentd v1.19.0.
11
+ # Ref. https://github.com/fluent/fluentd/pull/4903
12
+ # If we have supported v1.19.0+ only, we can remove this patch.
13
+ module Fluent::PluginHelper::HttpServer
14
+ module Extension
15
+ refine Request do
16
+ def headers
17
+ @request.headers
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ using Fluent::PluginHelper::HttpServer::Extension
24
+ end
25
+
26
+ class Fluent::Plugin::Opentelemetry::HttpInputHandler
27
+ def logs(req, &block)
28
+ common(req, Fluent::Plugin::Opentelemetry::Request::Logs, Fluent::Plugin::Opentelemetry::Response::Logs, &block)
29
+ end
30
+
31
+ def metrics(req, &block)
32
+ common(req, Fluent::Plugin::Opentelemetry::Request::Metrics, Fluent::Plugin::Opentelemetry::Response::Metrics, &block)
33
+ end
34
+
35
+ def traces(req, &block)
36
+ common(req, Fluent::Plugin::Opentelemetry::Request::Traces, Fluent::Plugin::Opentelemetry::Response::Traces, &block)
37
+ end
38
+
39
+ private
40
+
41
+ def common(req, request_class, response_class)
42
+ content_type = req.headers["content-type"]
43
+ content_encoding = req.headers["content-encoding"]&.first
44
+ return response_unsupported_media_type unless valid_content_type?(content_type)
45
+ return response_bad_request(content_type) unless valid_content_encoding?(content_encoding)
46
+
47
+ body = req.body
48
+ body = Zlib::GzipReader.new(StringIO.new(body)).read if content_encoding == Fluent::Plugin::Opentelemetry::CONTENT_ENCODING_GZIP
49
+
50
+ begin
51
+ record = request_class.new(body).record
52
+ rescue Google::Protobuf::ParseError
53
+ # The format in request body does not comply with the OpenTelemetry protocol.
54
+ return response_bad_request(content_type)
55
+ end
56
+
57
+ yield record
58
+
59
+ res = response_class.new
60
+ response(200, content_type, res.body(type: Fluent::Plugin::Opentelemetry::Response.type(content_type)))
61
+ end
62
+
63
+ def valid_content_type?(content_type)
64
+ case content_type
65
+ when Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PROTOBUF, Fluent::Plugin::Opentelemetry::CONTENT_TYPE_JSON
66
+ true
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ def valid_content_encoding?(content_encoding)
73
+ return true if content_encoding.nil?
74
+
75
+ content_encoding == Fluent::Plugin::Opentelemetry::CONTENT_ENCODING_GZIP
76
+ end
77
+
78
+ def response(code, content_type, body)
79
+ [code, { Fluent::Plugin::Opentelemetry::CONTENT_TYPE => content_type }, body]
80
+ end
81
+
82
+ def response_unsupported_media_type
83
+ response(415, Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PAIN, "415 unsupported media type, supported: [application/json, application/x-protobuf]")
84
+ end
85
+
86
+ def response_bad_request(content_type)
87
+ response(400, content_type, "") # TODO: fix body message
88
+ end
89
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/opentelemetry/constant"
4
+ require "fluent/plugin/opentelemetry/request"
5
+
6
+ require "excon"
7
+ require "google/protobuf"
8
+ require "stringio"
9
+ require "zlib"
10
+
11
+ class Fluent::Plugin::Opentelemetry::HttpOutputHandler
12
+ def initialize(http_config, transport_config, logger)
13
+ @http_config = http_config
14
+ @transport_config = transport_config
15
+ @logger = logger
16
+
17
+ @tls_settings = {}
18
+ if @transport_config.protocol == :tls
19
+ @tls_settings[:client_cert] = @transport_config.cert_path
20
+ @tls_settings[:client_key] = @transport_config.private_key_path
21
+ @tls_settings[:client_key_pass] = @transport_config.private_key_passphrase
22
+ @tls_settings[:ssl_min_version] = Fluent::Plugin::Opentelemetry::TLS_VERSIONS_MAP[@transport_config.min_version]
23
+ @tls_settings[:ssl_max_version] = Fluent::Plugin::Opentelemetry::TLS_VERSIONS_MAP[@transport_config.max_version]
24
+ end
25
+
26
+ @timeout_settings = {
27
+ read_timeout: http_config.read_timeout,
28
+ write_timeout: http_config.write_timeout,
29
+ connect_timeout: http_config.connect_timeout
30
+ }
31
+ end
32
+
33
+ def export(record)
34
+ uri, connection = create_http_connection(record)
35
+ response = connection.post
36
+
37
+ if response.status != 200
38
+ if response.status == 400
39
+ # The client MUST NOT retry the request when it receives HTTP 400 Bad Request response.
40
+ raise Fluent::UnrecoverableError, "got unrecoverable error response from '#{uri}', response code is #{response.status}"
41
+ end
42
+
43
+ if @http_config.retryable_response_codes&.include?(response.status)
44
+ raise Fluent::Plugin::OpentelemetryOutput::RetryableResponse, "got retryable error response from '#{uri}', response code is #{response.status}"
45
+ end
46
+ if @http_config.error_response_as_unrecoverable
47
+ raise Fluent::UnrecoverableError, "got unrecoverable error response from '#{uri}', response code is #{response.status}"
48
+ else
49
+ @logger.error "got error response from '#{uri}', response code is #{response.status}"
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def http_logs_endpoint
57
+ "#{@http_config.endpoint}/v1/logs"
58
+ end
59
+
60
+ def http_metrics_endpoint
61
+ "#{@http_config.endpoint}/v1/metrics"
62
+ end
63
+
64
+ def http_traces_endpoint
65
+ "#{@http_config.endpoint}/v1/traces"
66
+ end
67
+
68
+ def create_http_connection(record)
69
+ msg = record["message"]
70
+
71
+ begin
72
+ case record["type"]
73
+ when Fluent::Plugin::Opentelemetry::RECORD_TYPE_LOGS
74
+ uri = http_logs_endpoint
75
+ body = Fluent::Plugin::Opentelemetry::Request::Logs.new(msg).body
76
+ when Fluent::Plugin::Opentelemetry::RECORD_TYPE_METRICS
77
+ uri = http_metrics_endpoint
78
+ body = Fluent::Plugin::Opentelemetry::Request::Metrics.new(msg).body
79
+ when Fluent::Plugin::Opentelemetry::RECORD_TYPE_TRACES
80
+ uri = http_traces_endpoint
81
+ body = Fluent::Plugin::Opentelemetry::Request::Traces.new(msg).body
82
+ end
83
+ rescue Google::Protobuf::ParseError => e
84
+ # The message format does not comply with the OpenTelemetry protocol.
85
+ raise ::Fluent::UnrecoverableError, e.message
86
+ end
87
+
88
+ headers = { Fluent::Plugin::Opentelemetry::CONTENT_TYPE => Fluent::Plugin::Opentelemetry::CONTENT_TYPE_PROTOBUF }
89
+ if @http_config.compress == :gzip
90
+ headers[Fluent::Plugin::Opentelemetry::CONTENT_ENCODING] = Fluent::Plugin::Opentelemetry::CONTENT_ENCODING_GZIP
91
+ gz = Zlib::GzipWriter.new(StringIO.new)
92
+ gz << body
93
+ body = gz.close.string
94
+ end
95
+
96
+ Excon.defaults[:ssl_verify_peer] = false if @transport_config.insecure
97
+ connection = Excon.new(uri, body: body, headers: headers, proxy: @http_config.proxy, persistent: true, **@tls_settings, **@timeout_settings)
98
+ [uri, connection]
99
+ end
100
+ end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fluent/plugin/opentelemetry/constant"
4
- require "fluent/plugin/opentelemetry/request"
5
- require "fluent/plugin/opentelemetry/service_stub"
4
+ require "fluent/plugin/opentelemetry/http_output_handler"
6
5
  require "fluent/plugin/output"
7
6
 
8
- require "excon"
9
- require "grpc"
10
7
  require "json"
11
- require "stringio"
12
- require "zlib"
8
+
9
+ begin
10
+ require "grpc"
11
+
12
+ require "fluent/plugin/opentelemetry/grpc_output_handler"
13
+ rescue LoadError
14
+ end
13
15
 
14
16
  module Fluent::Plugin
15
17
  class OpentelemetryOutput < Output
@@ -57,12 +59,16 @@ module Fluent::Plugin
57
59
  def configure(conf)
58
60
  super
59
61
 
62
+ if @grpc_config && !defined?(GRPC)
63
+ raise Fluent::ConfigError, "To use gRPC feature, please install grpc gem such as 'fluent-gem install grpc'."
64
+ end
65
+
60
66
  unless [@http_config, @grpc_config].one?
61
67
  raise Fluent::ConfigError, "Please configure either <http> or <grpc> section."
62
68
  end
63
69
 
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
70
+ @http_handler = Opentelemetry::HttpOutputHandler.new(@http_config, @transport_config, log) if @http_config
71
+ @grpc_handler = Opentelemetry::GrpcOutputHandler.new(@grpc_config, @transport_config, log) if @grpc_config
66
72
  end
67
73
 
68
74
  def multi_workers_ready?
@@ -82,126 +88,5 @@ module Fluent::Plugin
82
88
  @http_handler.export(record)
83
89
  end
84
90
  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
91
  end
207
92
  end
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shizuo Fujita
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '4.30'
68
- - !ruby/object:Gem::Dependency
69
- name: grpc
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '1.71'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '1.71'
82
68
  description: Fluentd input/output plugin to forward OpenTelemetry Protocol data.
83
69
  email:
84
70
  - fujita@clear-code.com
@@ -94,10 +80,12 @@ files:
94
80
  - TODO.md
95
81
  - lib/fluent/plugin/in_opentelemetry.rb
96
82
  - lib/fluent/plugin/opentelemetry/constant.rb
83
+ - lib/fluent/plugin/opentelemetry/grpc_input_handler.rb
84
+ - lib/fluent/plugin/opentelemetry/grpc_output_handler.rb
85
+ - lib/fluent/plugin/opentelemetry/http_input_handler.rb
86
+ - lib/fluent/plugin/opentelemetry/http_output_handler.rb
97
87
  - lib/fluent/plugin/opentelemetry/request.rb
98
88
  - lib/fluent/plugin/opentelemetry/response.rb
99
- - lib/fluent/plugin/opentelemetry/service_handler.rb
100
- - lib/fluent/plugin/opentelemetry/service_stub.rb
101
89
  - lib/fluent/plugin/out_opentelemetry.rb
102
90
  - lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb
103
91
  - lib/opentelemetry/proto/collector/logs/v1/logs_service_services_pb.rb
@@ -134,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
122
  - !ruby/object:Gem::Version
135
123
  version: '0'
136
124
  requirements: []
137
- rubygems_version: 3.6.7
125
+ rubygems_version: 3.7.0
138
126
  specification_version: 4
139
127
  summary: Fluentd input/output plugin to forward OpenTelemetry Protocol data.
140
128
  test_files: []
@@ -1,37 +0,0 @@
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
@@ -1,45 +0,0 @@
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