fluent-plugin-opentelemetry 0.3.0 → 0.4.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: 7e88abfcc25a1079994f2801f2b32665bb39bb32408cff6efb91125ef9b60149
4
- data.tar.gz: 2b64e708b7ea494e4c35e4218c4f33e2ca15f04ac55672d95dc9d7456435303c
3
+ metadata.gz: 1fce288ab5297db7255a6f89bd709731863d5a8952a08a40276f2f06381bfdf6
4
+ data.tar.gz: df4713f998552481517d0af45085b99551751e89600d9c6f46e541349173aaf3
5
5
  SHA512:
6
- metadata.gz: 26fde29bef629246d797440d306b863ed6ed8cd57d0871f6c66b52b0c3109b453e6d836cb5b99d5b545d778cbb9f4893fdc37166499baf61e92de5bf478f60c5
7
- data.tar.gz: 1510198977d2f3b71a58a7a85e9a948b40716bf036c84968b8bb5c1507d63553fd1a06dbc70633d788d110dbc510ef898710eaaf2727d76adfd8baba9b5c9e81
6
+ metadata.gz: 8e1d534c9ed6045031b9a40e58751c9c4a1c77e519d37a06dffa9d9d9e97eacccb8e0f90b3931ef925b20268dd97163423aaf05d4013f4b73192fbd46f322f82
7
+ data.tar.gz: e11de5fbc7b9282a205a653b05f7ed6e6554ac5760ef02b3e9f143875cc771720609095012f343ea5bc0a02daeba6e85baafb7e2a1ffe5fdcb0a7756f40e12a0
data/.editorconfig ADDED
@@ -0,0 +1,14 @@
1
+ # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs
2
+ # @see http://editorconfig.org
3
+ root = true
4
+
5
+ [*]
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+ indent_style = space
11
+ tab_width = 4
12
+
13
+ [*.{rb,yml,yaml,md,conf}]
14
+ indent_size = 2
data/.rubocop.yml CHANGED
@@ -12,9 +12,12 @@ AllCops:
12
12
 
13
13
  # rubocop-fluentd
14
14
  Lint/FluentdPluginLogScope:
15
+ AssumeConfigLogLevel: 'info'
15
16
  Enabled: true
16
17
  Lint/FluentdPluginConfigParamDefaultTime:
17
18
  Enabled: true
19
+ Lint/FluentdPluginIgnoreStandardError:
20
+ Enabled: true
18
21
  Performance/FluentdPluginLogStringInterpolation:
19
22
  Enabled: true
20
23
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-10-10
4
+
5
+ - in_opentelemetry_metrics: Add plugin to support fluentd metrics export (#10)
6
+
3
7
  ## [0.3.0] - 2025-07-23
4
8
 
5
9
  - in_opentelemetry: add ${type} placeholder support in tag parameter (#8)
data/README.md CHANGED
@@ -24,7 +24,7 @@ $ bundle
24
24
 
25
25
  ## Configuration
26
26
 
27
- ### Input plugin
27
+ ### Input `opentelemetry` plugin
28
28
 
29
29
  To receive data, this plugin requires `<http>` or `<grpc>` section, or both.
30
30
 
@@ -106,7 +106,40 @@ Refer [Config: Transport Section](https://docs.fluentd.org/configuration/transpo
106
106
  </source>
107
107
  ```
108
108
 
109
- ### Output plugin
109
+ ### Input `opentelemetry_metrics` plugin
110
+
111
+ This plugin emits Fluentd's metric data that conforms to the OpenTelemetry Protocol.
112
+ To output the data, it requires to use output `opentelemetry` plugin.
113
+
114
+ #### Root section
115
+
116
+ | parameter | type | description | default |
117
+ |--------------------|--------|-------------------------------------------------------|-------------|
118
+ | tag | string | The tag of the event | required |
119
+ | emit_interval | time | Determine the rate to emit internal metrics as events | `60` |
120
+ | metric_name_prefix | string | The prefix of metric name | `fluentd_` |
121
+
122
+ #### Example
123
+
124
+ ```
125
+ # Emit Fluentd metrics
126
+ <source>
127
+ @type opentelemetry_metrics
128
+ tag opentelemetry.fluentd.metrics
129
+ emit_interval 300s
130
+ </source>
131
+
132
+ # Send Fluentd metrics to OpenTelemetry Collector
133
+ <match opentelemetry.fluentd.metrics>
134
+ @type opentelemetry
135
+
136
+ <http>
137
+ endpoint "https://127.0.0.1:4318"
138
+ </http>
139
+ </match>
140
+ ```
141
+
142
+ ### Output `opentelemetry` plugin
110
143
 
111
144
  To send data, this plugin requires `<http>` or `<grpc>` section.
112
145
 
@@ -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
@@ -9,10 +9,10 @@ require "google/protobuf"
9
9
 
10
10
  class Fluent::Plugin::Opentelemetry::Request
11
11
  class Logs
12
- def initialize(body)
12
+ def initialize(body, ignore_unknown_fields: true)
13
13
  @request =
14
14
  if body.start_with?("{")
15
- Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode_json(body, ignore_unknown_fields: true)
15
+ Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode_json(body, ignore_unknown_fields: ignore_unknown_fields)
16
16
  else
17
17
  Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(body)
18
18
  end
@@ -28,10 +28,10 @@ class Fluent::Plugin::Opentelemetry::Request
28
28
  end
29
29
 
30
30
  class Metrics
31
- def initialize(body)
31
+ def initialize(body, ignore_unknown_fields: true)
32
32
  @request =
33
33
  if body.start_with?("{")
34
- Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode_json(body, ignore_unknown_fields: true)
34
+ Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode_json(body, ignore_unknown_fields: ignore_unknown_fields)
35
35
  else
36
36
  Opentelemetry::Proto::Collector::Metrics::V1::ExportMetricsServiceRequest.decode(body)
37
37
  end
@@ -47,10 +47,10 @@ class Fluent::Plugin::Opentelemetry::Request
47
47
  end
48
48
 
49
49
  class Traces
50
- def initialize(body)
50
+ def initialize(body, ignore_unknown_fields: true)
51
51
  @request =
52
52
  if body.start_with?("{")
53
- Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode_json(body, ignore_unknown_fields: true)
53
+ Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode_json(body, ignore_unknown_fields: ignore_unknown_fields)
54
54
  else
55
55
  Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(body)
56
56
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module Opentelemetry
6
+ VERSION = "0.4.0"
7
+ end
8
+ end
9
+ 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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shizuo Fujita
@@ -72,6 +72,7 @@ executables: []
72
72
  extensions: []
73
73
  extra_rdoc_files: []
74
74
  files:
75
+ - ".editorconfig"
75
76
  - ".rubocop.yml"
76
77
  - CHANGELOG.md
77
78
  - LICENSE
@@ -79,6 +80,7 @@ files:
79
80
  - Rakefile
80
81
  - TODO.md
81
82
  - lib/fluent/plugin/in_opentelemetry.rb
83
+ - lib/fluent/plugin/in_opentelemetry_metrics.rb
82
84
  - lib/fluent/plugin/opentelemetry/constant.rb
83
85
  - lib/fluent/plugin/opentelemetry/grpc_input_handler.rb
84
86
  - lib/fluent/plugin/opentelemetry/grpc_output_handler.rb
@@ -86,6 +88,7 @@ files:
86
88
  - lib/fluent/plugin/opentelemetry/http_output_handler.rb
87
89
  - lib/fluent/plugin/opentelemetry/request.rb
88
90
  - lib/fluent/plugin/opentelemetry/response.rb
91
+ - lib/fluent/plugin/opentelemetry/version.rb
89
92
  - lib/fluent/plugin/out_opentelemetry.rb
90
93
  - lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb
91
94
  - lib/opentelemetry/proto/collector/logs/v1/logs_service_services_pb.rb
@@ -122,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
125
  - !ruby/object:Gem::Version
123
126
  version: '0'
124
127
  requirements: []
125
- rubygems_version: 3.7.0
128
+ rubygems_version: 3.7.2
126
129
  specification_version: 4
127
130
  summary: Fluentd input/output plugin to forward OpenTelemetry Protocol data.
128
131
  test_files: []