fluent-plugin-prometheus 2.2.2 → 2.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: a2c3701c496726e677bf8182e4b549d493648b1f639930a7508ead0a07611ed6
4
- data.tar.gz: c7f62e9dfc1a37d8089e07b41a6704e92fc365c0811b960bb628ffafe01134eb
3
+ metadata.gz: c10a2e202128626380ce6fd1003ca82baa6f1ae55bd41642b8f6c0bc44670abc
4
+ data.tar.gz: 34a77887e5da8f4bb4dc08018478a1592d7dec51411e338b4773ab41ec281e06
5
5
  SHA512:
6
- metadata.gz: 53f721520289f3ca5f8b7b47fe4c5daf7829fae4e22f4bd6145dffaa1d4d173fc81909576067136299b0e256a55d8b961d432ac15aa16074f73dfb6003b9fbfb
7
- data.tar.gz: 5cdf65cb05bf988df0cfc255a2af2e0f5b8223e5b54c56d4bbabe081827c111e7e210d45d2256d6f3af9cf4dab9154d2b6aba85c7226a87ebb96b420e9230402
6
+ metadata.gz: 131d844af8a45deb00d2ee53dba0928534a104193aec9a26f5d0760600ffefa2c11cb284fa6e49ae81f1cb4e17520db1b62a2a1acc6af295b964bdfa4c35aa5c
7
+ data.tar.gz: b9da9de5e2019b290ccd45bfca81ad59cdf7942109e961f8919d1121596e00bc63661cf76825251e4f0a9b796929830a6a7a7dade75ddcc00785f0c901317476
@@ -0,0 +1,72 @@
1
+ name: Bug Report
2
+ description: Create a report with a procedure for reproducing the bug
3
+ labels: "waiting-for-triage"
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Please check the [README](https://github.com/fluent/fluent-plugin-prometheus/blob/master/README.md) first. To help us investigate the problem, please provide the following information.
9
+ - type: textarea
10
+ id: description
11
+ attributes:
12
+ label: Describe the bug
13
+ description: A clear and concise description of what the bug is
14
+ validations:
15
+ required: true
16
+ - type: textarea
17
+ id: reproduce
18
+ attributes:
19
+ label: To Reproduce
20
+ description: Steps to reproduce the behavior
21
+ validations:
22
+ required: true
23
+ - type: textarea
24
+ id: expected
25
+ attributes:
26
+ label: Expected behavior
27
+ description: A clear and concise description of what you expected to happen
28
+ validations:
29
+ required: true
30
+ - type: textarea
31
+ id: environment
32
+ attributes:
33
+ label: Your Environment
34
+ description: |
35
+ - Fluentd or td-agent version: `fluentd --version` or `td-agent --version`
36
+ - Operating system: `cat /etc/os-release`
37
+ - Kernel version: `uname -r`
38
+
39
+ Tip: If you hit the problem with older fluentd version, try latest version first.
40
+ value: |
41
+ - Fluentd version:
42
+ - TD Agent/Fluent Package version:
43
+ - fluent-plugin-prometheus version:
44
+ - prometheus-client version:
45
+ - Operating system:
46
+ - Kernel version:
47
+ render: markdown
48
+ validations:
49
+ required: true
50
+ - type: textarea
51
+ id: configuration
52
+ attributes:
53
+ label: Your Configuration
54
+ description: |
55
+ Write your configuration here. Minimum reproducible fluentd.conf is recommended.
56
+ validations:
57
+ required: true
58
+ - type: textarea
59
+ id: logs
60
+ attributes:
61
+ label: Your Error Log
62
+ description: Write your ALL error log here
63
+ render: shell
64
+ validations:
65
+ required: true
66
+ - type: textarea
67
+ id: addtional-context
68
+ attributes:
69
+ label: Additional context
70
+ description: Add any other context about the problem here.
71
+ validations:
72
+ required: false
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Ask a Question
4
+ url: https://discuss.fluentd.org/
5
+ about: I have questions about fluent-plugin-prometheus. Please ask and answer questions at https://discuss.fluentd.org/.
@@ -0,0 +1,38 @@
1
+ name: Feature request
2
+ description: Suggest an idea for this project
3
+ labels: "waiting-for-triage"
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Please check the [README](https://github.com/fluent/fluent-plugin-prometheus/blob/master/README.md) first. To help us understand the request, please provide the following information.
9
+ - type: textarea
10
+ id: description
11
+ attributes:
12
+ label: Is your feature request related to a problem? Please describe.
13
+ description: |
14
+ A clear and concise description of what the problem is.
15
+ Ex. I'm always frustrated when [...]
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: solution
20
+ attributes:
21
+ label: Describe the solution you'd like
22
+ description: A clear and concise description of what you want to happen.
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: alternative
27
+ attributes:
28
+ label: Describe alternatives you've considered
29
+ description: A clear and concise description of any alternative solutions or features you've considered.
30
+ validations:
31
+ required: true
32
+ - type: textarea
33
+ id: addtional-context
34
+ attributes:
35
+ label: Additional context
36
+ description: Add any other context or screenshots about the feature request here.
37
+ validations:
38
+ required: false
@@ -3,4 +3,21 @@ updates:
3
3
  - package-ecosystem: 'github-actions'
4
4
  directory: '/'
5
5
  schedule:
6
- interval: 'weekly'
6
+ interval: 'monthly'
7
+ groups:
8
+ # PR: "Security update [package] from [old] to [new]"
9
+ # This PR should be merged in hurry
10
+ security-updates:
11
+ applies-to: security-updates
12
+ patterns:
13
+ - '*'
14
+
15
+ # PR: "Bump [package] from [old] to [new]"
16
+ # No need to be merged this PR in hurry. It is enough to merge
17
+ # once in a month.
18
+ monthly-updates:
19
+ applies-to: version-updates
20
+ patterns:
21
+ - '*'
22
+ # Allow to create PR both of security and normal updates.
23
+ open-pull-requests-limit: 1
@@ -0,0 +1,24 @@
1
+ name: Add bugs to fluent project
2
+
3
+ on:
4
+ issues:
5
+ types:
6
+ - opened
7
+
8
+ jobs:
9
+ add-to-project:
10
+ name: Add issue to project
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Generate token
14
+ id: generate_token
15
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
16
+ with:
17
+ app-id: ${{ secrets.PROJECT_APP_ID }}
18
+ private-key: ${{ secrets.PROJECT_APP_PRIVATE_KEY }}
19
+ owner: fluent
20
+ - uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
21
+ with:
22
+ project-url: https://github.com/orgs/fluent/projects/4
23
+ github-token: ${{ steps.generate_token.outputs.token }}
24
+ labeled: waiting-for-triage
@@ -27,8 +27,8 @@ jobs:
27
27
  experimental: [false]
28
28
  name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
29
29
  steps:
30
- - uses: actions/checkout@v6
31
- - uses: ruby/setup-ruby@v1
30
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
+ - uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
32
32
  with:
33
33
  ruby-version: ${{ matrix.ruby }}
34
34
  - name: unit testing
data/ChangeLog CHANGED
@@ -1,3 +1,16 @@
1
+ Release 2.3.0 - 2026/09/02
2
+
3
+ * in_prometheus: Change default bind address to 127.0.0.1. (#258)
4
+ This is incompatible behavior change.
5
+ * in_prometheus: Do not disclosure error details for client (#259)
6
+ * in_prometheus: Add `ignore_error_log_interval` parameter to suppress repeated error logs (#259,#264)
7
+ This introduces the log throttling feature.
8
+ * filter_prometheus: Do not let placeholders built from records override the tag ones (#262)
9
+ * out_prometheus: Fix a bug that a failing record never reached to @ERROR (#263)
10
+ * Support `max_series_per_metric` to limit label sets cardinarity. (#265)
11
+ * Refuse wrong <initlabels> which cannot fit into `max_series_per_metric` (#266,#267)
12
+ This blocks `max_series_per_metric` mis-configuration on startup.
13
+
1
14
  Release 2.2.2 - 2026/03/27
2
15
 
3
16
  * in_prometheus: fix: Add IPv6 support for bind parameter (#240)
data/README.md CHANGED
@@ -59,11 +59,12 @@ With following configuration, you can access http://localhost:24231/metrics on a
59
59
 
60
60
  More configuration parameters:
61
61
 
62
- - `bind`: binding interface (default: '0.0.0.0')
62
+ - `bind`: binding interface (default: '127.0.0.1')
63
63
  - `port`: listen port (default: 24231)
64
64
  - `metrics_path`: metrics HTTP endpoint (default: /metrics)
65
65
  - `aggregated_metrics_path`: metrics HTTP endpoint (default: /aggregated_metrics)
66
66
  - `content_encoding`: encoding format for the exposed metrics (default: identity). Supported formats are {identity, gzip}
67
+ - `ignore_error_log_interval`: Suppress repeated error logs in a certain period of time or until message was changed (default: 1h)
67
68
 
68
69
  When using multiple workers, each worker binds to port + `fluent_worker_id`.
69
70
  To scrape metrics from all workers at once, you can access http://localhost:24231/aggregated_metrics.
@@ -265,6 +266,132 @@ You can access nested keys in records via dot or bracket notation (https://docs.
265
266
 
266
267
  See Supported Metric Type and Labels for more configuration parameters.
267
268
 
269
+ #### Limiting label expansion
270
+
271
+ Label values come from records, so a metric grows unboundedly when a label is
272
+ bound to a field with many distinct values. Both plugins can bound it with
273
+ `max_series_per_metric`: once a metric holds that many label sets, a record
274
+ which brings a new one is dropped, while the label sets already known keep
275
+ being instrumented.
276
+
277
+ |parameter|description|default|
278
+ |---|---|---|
279
+ |max_series_per_metric|The maximum number of label sets a metric can hold. `0` means unlimited.|0|
280
+ |ignore_error_log_interval|The interval in seconds to suppress the repeated warning about the drops. `0` logs every occurrence.|3600|
281
+
282
+ **The limit is disabled by default and must be enabled explicitly**, since a
283
+ dropped record is lost and cannot be recovered. A `<metric>` section overrides
284
+ the value given to the plugin, so that a metric which expands faster than the
285
+ others is bound on its own, while one whose labels are known to be bounded
286
+ stays unlimited with `0`:
287
+
288
+ ```
289
+ <filter message>
290
+ @type prometheus
291
+ max_series_per_metric 1000
292
+ <metric>
293
+ name message_foo_counter
294
+ type counter
295
+ desc The total number of foo in message.
296
+ key foo
297
+ max_series_per_metric 10
298
+ <labels>
299
+ path $.kubernetes.pod_name
300
+ </labels>
301
+ </metric>
302
+ </filter>
303
+ ```
304
+
305
+ The label sets are counted per metric name, not per `<metric>` section: sections
306
+ with the same `name`, in one plugin or in two, share one count. Each of them
307
+ refuses a new label set once that shared count reaches its own limit, so a
308
+ section which stays at `0` adds label sets without counting them. Its
309
+ `<initlabels>` are counted anyway. They come from the configuration and their
310
+ number is fixed. The metric holds them in every section.
311
+
312
+ The count is per worker process as well, since a worker has its own registry and
313
+ exposes the metrics it holds itself. With `workers N`, a metric can hold up to N
314
+ times `max_series_per_metric` label sets in total, so divide the number of label
315
+ sets the metric may reach by the number of workers.
316
+
317
+ A record which fails to be instrumented, for example when the value of `key` is
318
+ not a number, does not consume the limit. A pre-initialized label set
319
+ (`initialized` and `<initlabels>`) consumes it from the start, since the metric
320
+ holds it before any record arrives.
321
+
322
+ A `<metric>` section is refused at startup with a configuration error when the
323
+ `<initlabels>` label sets already fill its limit. The limit is exceeded before
324
+ any record arrives, and the section can never take a new label set.
325
+
326
+ These label sets are shared by every section with the same `name`. A section can
327
+ be refused because of another section:
328
+
329
+ ```
330
+ <metric>
331
+ name shared
332
+ type counter
333
+ desc Something foo.
334
+ max_series_per_metric 2
335
+ initialized true
336
+ <labels>
337
+ path $.path
338
+ </labels>
339
+ <initlabels>
340
+ path /a
341
+ </initlabels>
342
+ <initlabels>
343
+ path /b
344
+ </initlabels>
345
+ </metric>
346
+ <metric>
347
+ name shared # the same name, so the 2 label sets count here too
348
+ type counter
349
+ desc Something foo.
350
+ max_series_per_metric 1 # refused: 2 label sets do not fit into 1
351
+ <labels>
352
+ path $.path
353
+ </labels>
354
+ </metric>
355
+ ```
356
+
357
+ The second section is refused even though it declares no `<initlabels>` of its
358
+ own. The same happens when the first section sets `max_series_per_metric 0`.
359
+ The metric holds its label sets in both cases. A limit equal to their number is
360
+ fine because all label sets of the metric are known in advance.
361
+
362
+ The check runs after every `<metric>` section of a plugin is read. It does not
363
+ depend on the order of the sections. Sections that share a `name` across two
364
+ plugins are only checked against the sections read before them.
365
+
366
+ The check runs again when Fluentd reloads the configuration. It counts only the
367
+ label sets from `<initlabels>`. It does not count the label sets that records
368
+ brought, so the metric does not make the reload fail with the label sets it took
369
+ while it was running.
370
+
371
+ A reload does not clear the registry. The metric still holds the label sets from
372
+ the `<initlabels>` of the old configuration, and the check counts them too. So
373
+ it can refuse a new configuration which has fewer `<initlabels>` than the old
374
+ one. Restart the worker to drop them.
375
+
376
+ ##### Observing what the limit leaves out
377
+
378
+ A dropped label set is not routed to `@ERROR`, because it is what the
379
+ configuration asks for. It is reported in two ways instead:
380
+
381
+ * a warning in the Fluentd log, throttled per metric: it is suppressed for
382
+ `ignore_error_log_interval` seconds and reports how many warnings were
383
+ suppressed in the meantime.
384
+ * `fluentd_prometheus_dropped_label_sets_total{name}`, which counts the records
385
+ the metric `name` did not instrument. It is registered on the first drop, so
386
+ it does not show up as long as nothing is dropped, and its label comes from
387
+ the configuration and not from a record, so it cannot expand on its own.
388
+
389
+ Alert on the counter to notice that a metric is losing records:
390
+
391
+ ```
392
+ rate(fluentd_prometheus_dropped_label_sets_total[5m]) > 0
393
+ ```
394
+
268
395
  ## Supported Metric Types
269
396
 
270
397
  For details of each metric type, see [Prometheus documentation](http://prometheus.io/docs/concepts/metric_types/). Also see [metric name guide](http://prometheus.io/docs/practices/naming/).
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "fluent-plugin-prometheus"
3
- spec.version = "2.2.2"
3
+ spec.version = "2.3.0"
4
4
  spec.authors = ["Masahiro Sano"]
5
5
  spec.email = ["sabottenda@gmail.com"]
6
6
  spec.summary = %q{A fluent plugin that collects metrics and exposes for Prometheus.}
@@ -19,7 +19,7 @@ module Fluent::Plugin
19
19
  def configure(conf)
20
20
  super
21
21
  labels = parse_labels_elements(conf)
22
- @metrics = Fluent::Plugin::Prometheus.parse_metrics_elements(conf, @registry, labels)
22
+ @metrics = Fluent::Plugin::Prometheus.parse_metrics_elements(conf, @registry, labels, metric_options)
23
23
  end
24
24
 
25
25
  def filter(tag, time, record)
@@ -11,7 +11,7 @@ module Fluent::Plugin
11
11
 
12
12
  helpers :thread, :http_server
13
13
 
14
- config_param :bind, :string, default: '0.0.0.0'
14
+ config_param :bind, :string, default: '127.0.0.1'
15
15
  config_param :port, :integer, default: 24231
16
16
  config_param :metrics_path, :string, default: '/metrics'
17
17
  config_param :aggregated_metrics_path, :string, default: '/aggregated_metrics'
@@ -36,10 +36,14 @@ module Fluent::Plugin
36
36
  desc 'Content encoding of the exposed metrics, Currently supported encoding is identity, gzip. Ref: https://prometheus.io/docs/instrumenting/exposition_formats/#basic-info'
37
37
  config_param :content_encoding, :enum, list: [:identity, :gzip], default: :identity
38
38
 
39
+ desc 'Suppress repeated error logs in a certain period of time (1h) or until message was changed'
40
+ config_param :ignore_error_log_interval, :time, default: 3600
41
+
39
42
  def initialize
40
43
  super
41
44
  @registry = ::Prometheus::Client.registry
42
45
  @secure = nil
46
+ @error_log_throttle = nil
43
47
  end
44
48
 
45
49
  def configure(conf)
@@ -58,6 +62,8 @@ module Fluent::Plugin
58
62
 
59
63
  @base_port = @port
60
64
  @port += fluentd_worker_id
65
+
66
+ @error_log_throttle = Fluent::Plugin::Prometheus::LogThrottle.new(@ignore_error_log_interval)
61
67
  end
62
68
 
63
69
  def multi_workers_ready?
@@ -210,7 +216,8 @@ module Fluent::Plugin
210
216
  def all_metrics
211
217
  response(::Prometheus::Client::Formats::Text.marshal(@registry))
212
218
  rescue => e
213
- [500, { 'Content-Type' => 'text/plain' }, e.to_s]
219
+ log_error_throttled(:metrics, "in_prometheus: failed to render metrics", error: e)
220
+ [500, { 'Content-Type' => 'text/plain' }, "in_prometheus server error: <#{e.class}>"]
214
221
  end
215
222
 
216
223
  def all_workers_metrics
@@ -223,7 +230,8 @@ module Fluent::Plugin
223
230
  end
224
231
  response(full_result.get_metrics)
225
232
  rescue => e
226
- [500, { 'Content-Type' => 'text/plain' }, e.to_s]
233
+ log_error_throttled(:workers_metrics, "in_prometheus: failed to render workers metrics", error: e)
234
+ [500, { 'Content-Type' => 'text/plain' }, "in_prometheus server error: <#{e.class}>"]
227
235
  end
228
236
 
229
237
  def send_request_to_each_worker
@@ -271,5 +279,17 @@ module Fluent::Plugin
271
279
  end
272
280
  [200, { 'Content-Type' => ::Prometheus::Client::Formats::Text::CONTENT_TYPE, 'Content-Encoding' => @content_encoding.to_s }, body]
273
281
  end
282
+
283
+ def log_error_throttled(scope, message, error:)
284
+ fingerprint = [error.class, error.message]
285
+ emit, suppressed = @error_log_throttle.check(scope, fingerprint)
286
+ return unless emit
287
+
288
+ if suppressed > 0
289
+ log.error message, error_class: error.class, error: error, suppressed_log_count: suppressed
290
+ else
291
+ log.error message, error_class: error.class, error: error
292
+ end
293
+ end
274
294
  end
275
295
  end
@@ -7,6 +7,8 @@ module Fluent::Plugin
7
7
  include Fluent::Plugin::PrometheusLabelParser
8
8
  include Fluent::Plugin::Prometheus
9
9
 
10
+ helpers :event_emitter
11
+
10
12
  def initialize
11
13
  super
12
14
  @registry = ::Prometheus::Client.registry
@@ -19,7 +21,7 @@ module Fluent::Plugin
19
21
  def configure(conf)
20
22
  super
21
23
  labels = parse_labels_elements(conf)
22
- @metrics = Fluent::Plugin::Prometheus.parse_metrics_elements(conf, @registry, labels)
24
+ @metrics = Fluent::Plugin::Prometheus.parse_metrics_elements(conf, @registry, labels, metric_options)
23
25
  end
24
26
 
25
27
  def process(tag, es)
@@ -0,0 +1,45 @@
1
+ require 'fluent/clock'
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module Prometheus
6
+ # Suppresses the repeated log for the same key within the interval.
7
+ # in_prometheus and filter/out_prometheus use it, each with its own
8
+ # instance. The key decides what is throttled: an error scope or a
9
+ # metric. When a fingerprint is given, a log whose fingerprint differs
10
+ # from the last one is not suppressed.
11
+ class LogThrottle
12
+ Entry = Struct.new(:time, :fingerprint, :suppressed)
13
+
14
+ def initialize(interval)
15
+ @interval = interval
16
+ @mutex = Mutex.new
17
+ # one entry per key, so this does not grow without a limit
18
+ @entries = {}
19
+ end
20
+
21
+ # Returns [emit, suppressed_count]. emit is true for the first log of a
22
+ # key, for a new fingerprint, and after the interval has passed.
23
+ # suppressed_count is how many logs were suppressed since the last one
24
+ # was emitted. Without a fingerprint, a key is throttled by the
25
+ # interval alone.
26
+ def check(key, fingerprint = nil)
27
+ return [true, 0] if @interval <= 0
28
+
29
+ @mutex.synchronize do
30
+ now = Fluent::Clock.now
31
+ last = @entries[key]
32
+ if last.nil? || last.fingerprint != fingerprint || (now - last.time) >= @interval
33
+ suppressed = (last && last.fingerprint == fingerprint) ? last.suppressed : 0
34
+ @entries[key] = Entry.new(now, fingerprint, 0)
35
+ [true, suppressed]
36
+ else
37
+ last.suppressed += 1
38
+ [false, 0]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -2,6 +2,10 @@ module Fluent
2
2
  module Plugin
3
3
  module Prometheus
4
4
  class ExpandBuilder
5
+ # ${tag}, ${tag_parts[...]}, ${tag_prefix[...]} and ${tag_suffix[...]}
6
+ # must be built from the tag only.
7
+ TAG_DERIVED_PLACEHOLDER = /\A\$\{tag(_parts|_prefix|_suffix)?(\[[^\]]*\])?\}\z/.freeze
8
+
5
9
  def self.build(placeholder, log:)
6
10
  new(log: log).build(placeholder)
7
11
  end
@@ -12,6 +16,7 @@ module Fluent
12
16
 
13
17
  def build(placeholder_values)
14
18
  placeholders = {}
19
+ tag_placeholders = {}
15
20
  placeholder_values.each do |key, value|
16
21
  case value
17
22
  when Array
@@ -26,13 +31,22 @@ module Fluent
26
31
  end
27
32
  else
28
33
  if key == 'tag'
29
- placeholders.merge!(build_tag(value))
34
+ tag_placeholders = build_tag(value)
30
35
  else
31
36
  placeholders["${#{key}}"] = value
32
37
  end
33
38
  end
34
39
  end
35
40
 
41
+ # A record may have an array named "tag_parts", or a key named
42
+ # "tag_parts[0]". Both make the same placeholder as the tag does.
43
+ # merge! is not enough here, because a record can also use an index
44
+ # which build_tag does not make, such as ${tag_parts[3]} for a tag
45
+ # of 3 parts, or ${tag_prefix[-1]}. So remove them first, then such
46
+ # a placeholder stays unknown.
47
+ placeholders.delete_if { |k, _| TAG_DERIVED_PLACEHOLDER.match?(k) }
48
+ placeholders.merge!(tag_placeholders)
49
+
36
50
  Fluent::Plugin::Prometheus::ExpandBuilder::PlaceholderExpander.new(@log, placeholders)
37
51
  end
38
52