├── .editorconfig
├── .github
└── workflows
│ ├── maintenance-cache-wipe.yml
│ ├── maintenance-workflow-cleanup.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── examples
├── app-perf-dark.png
├── app-perf-light.png
├── controller-dark.png
├── controller-light.png
├── jobs-dark.png
├── jobs-light.png
├── queue-time-dark.png
└── queue-time-light.png
├── gemfiles
├── rack_2.0.gemfile
├── rack_2.1.gemfile
├── rack_2.2.gemfile
├── rack_3.gemfile
├── rails_6.1.gemfile
├── rails_7.0.gemfile
├── rails_7.1.gemfile
├── rails_7.2.gemfile
├── rails_8.0.gemfile
├── sidekiq_6.gemfile
├── sidekiq_7.gemfile
└── sidekiq_8.gemfile
├── lib
├── telegraf.rb
└── telegraf
│ ├── active_job.rb
│ ├── agent.rb
│ ├── grape.rb
│ ├── plugin.rb
│ ├── rack.rb
│ ├── rails.rb
│ ├── railtie.rb
│ ├── serializer.rb
│ ├── sidekiq.rb
│ └── version.rb
├── renovate.json5
├── spec
├── spec_helper.rb
├── telegraf
│ ├── active_job_spec.rb
│ ├── agent_spec.rb
│ ├── rack_spec.rb
│ ├── railtie_spec.rb
│ ├── serializer_spec.rb
│ └── sidekiq_spec.rb
└── telegraf_spec.rb
└── telegraf.gemspec
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.github/workflows/maintenance-cache-wipe.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: maintenance-cache-clear
3 | on:
4 | schedule:
5 | - cron: "0 0 1 * *"
6 | workflow_dispatch:
7 |
8 | jobs:
9 | cache-clear:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: easimon/wipe-cache@v2
14 |
--------------------------------------------------------------------------------
/.github/workflows/maintenance-workflow-cleanup.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: maintenance-workflow-cleanup
3 | on:
4 | schedule:
5 | - cron: "0 0 1 * *"
6 | workflow_dispatch:
7 |
8 | jobs:
9 | delete-workflow-runs:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: Mattraks/delete-workflow-runs@v2
13 | with:
14 | token: ${{ github.token }}
15 | repository: ${{ github.repository }}
16 | retain_days: 180
17 | keep_minimum_runs: 50
18 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # vim: ft=yaml
2 |
3 | name: release
4 |
5 | on:
6 | push:
7 | tags:
8 | - v*
9 |
10 | jobs:
11 | rubygems:
12 | if: github.repository == 'jgraichen/telegraf-ruby'
13 | runs-on: ubuntu-24.04
14 |
15 | permissions:
16 | contents: write
17 | id-token: write
18 |
19 | env:
20 | BUNDLE_JOBS: 4
21 | BUNDLE_RETRY: 10
22 | BUNDLE_WITHOUT: development test
23 |
24 | steps:
25 | - uses: actions/checkout@v4
26 |
27 | - uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: ruby
30 | bundler-cache: true
31 |
32 | - uses: rubygems/release-gem@v1
33 |
34 | - uses: taiki-e/create-gh-release-action@v1
35 | with:
36 | changelog: CHANGELOG.md
37 | draft: true
38 | token: ${{ secrets.GITHUB_TOKEN }}
39 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # vim: ft=yaml
2 | name: test
3 |
4 | on:
5 | push:
6 | pull_request:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | rspec:
11 | name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile }}
12 | runs-on: ubuntu-24.04
13 |
14 | strategy:
15 | matrix:
16 | ruby:
17 | - "3.4"
18 | - "3.3"
19 | - "3.2"
20 | - "3.1"
21 | gemfile:
22 | - rails_8.0.gemfile
23 | - rails_7.2.gemfile
24 | - rails_7.1.gemfile
25 | - rails_7.0.gemfile
26 | - rails_6.1.gemfile
27 | - rack_3.gemfile
28 | - rack_2.2.gemfile
29 | - rack_2.1.gemfile
30 | - rack_2.0.gemfile
31 | - sidekiq_6.gemfile
32 | - sidekiq_7.gemfile
33 | - sidekiq_8.gemfile
34 | exclude:
35 | - ruby: "3.4"
36 | gemfile: rails_7.0.gemfile
37 | - ruby: "3.4"
38 | gemfile: rails_6.1.gemfile
39 | - ruby: "3.4"
40 | gemfile: rack_2.1.gemfile
41 | - ruby: "3.4"
42 | gemfile: rack_2.0.gemfile
43 | - ruby: "3.1"
44 | gemfile: rails_8.0.gemfile
45 | - ruby: "3.1"
46 | gemfile: sidekiq_8.gemfile
47 | fail-fast: false
48 |
49 | env:
50 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}
51 | BUNDLE_WITHOUT: development
52 | BUNDLE_JOBS: 4
53 | BUNDLE_RETRY: 3
54 |
55 | steps:
56 | - uses: actions/checkout@master
57 |
58 | - uses: ruby/setup-ruby@v1
59 | with:
60 | ruby-version: ${{ matrix.ruby }}
61 | bundler-cache: true
62 |
63 | - run: bundle exec rspec --color --format documentation
64 |
65 | rubocop:
66 | name: rubocop
67 | runs-on: ubuntu-24.04
68 |
69 | env:
70 | BUNDLE_WITHOUT: development
71 | BUNDLE_JOBS: 4
72 | BUNDLE_RETRY: 3
73 |
74 | steps:
75 | - uses: actions/checkout@master
76 | - uses: ruby/setup-ruby@v1
77 | with:
78 | ruby-version: '3.4'
79 | bundler-cache: true
80 |
81 | - run: bundle exec rubocop --parallel --color
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /_yardoc/
2 | /.bundle/
3 | /.yardoc
4 | /coverage/
5 | /doc/
6 | /Gemfile.lock
7 | /gemfiles/.bundle/
8 | /gemfiles/*.lock
9 | /log/
10 | /pkg/
11 | /spec/reports/
12 | /tmp/
13 |
14 | # rspec failure tracking
15 | .rspec_status
16 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | # vim: ft=yaml
2 |
3 | inherit_gem:
4 | rubocop-config: default.yml
5 |
6 | AllCops:
7 | TargetRubyVersion: "2.7"
8 | SuggestExtensions: False
9 |
10 | RSpec/SpecFilePathFormat:
11 | Enabled: False
12 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | appraise 'rails-8.0' do
4 | group :test do
5 | gem 'rails', '~> 8.0.0'
6 | end
7 | end
8 |
9 | appraise 'rails-7.2' do
10 | group :test do
11 | gem 'rails', '~> 7.2.0'
12 | end
13 | end
14 |
15 | appraise 'rails-7.1' do
16 | group :test do
17 | gem 'rails', '~> 7.1.0'
18 | end
19 | end
20 |
21 | appraise 'rails-7.0' do
22 | group :test do
23 | gem 'rails', '~> 7.0.0'
24 | end
25 | end
26 |
27 | appraise 'rails-6.1' do
28 | group :test do
29 | gem 'rails', '~> 6.1.0'
30 | end
31 | end
32 |
33 | appraise 'rack-3' do
34 | group :test do
35 | gem 'rack', '~> 3.0'
36 | end
37 | end
38 |
39 | appraise 'rack-2.2' do
40 | group :test do
41 | gem 'rack', '~> 2.2.0'
42 | end
43 | end
44 |
45 | appraise 'rack-2.1' do
46 | group :test do
47 | gem 'rack', '~> 2.1.0'
48 | end
49 | end
50 |
51 | appraise 'rack-2.0' do
52 | group :test do
53 | gem 'rack', '~> 2.0.0'
54 | end
55 | end
56 |
57 | appraise 'sidekiq-6' do
58 | group :test do
59 | gem 'sidekiq', '~> 6.0'
60 | end
61 | end
62 |
63 | appraise 'sidekiq-7' do
64 | group :test do
65 | gem 'sidekiq', '~> 7.0'
66 | end
67 | end
68 |
69 | appraise 'sidekiq-8' do
70 | group :test do
71 | gem 'sidekiq', '~> 8.0'
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Changelog
4 |
5 | All notable changes to this project will be documented in this file.
6 |
7 | ## [Unreleased]
8 |
9 | ## [3.2.0] - 2025-03-08
10 |
11 | ### Added
12 |
13 | - Support for Sidekiq 8
14 |
15 | ## [3.1.0] - 2025-01-31
16 |
17 | ### Added
18 |
19 | - Support for Ruby 3.4, Rails 8.0, Rack 3
20 |
21 | ## [3.0.0] - 2024-03-11
22 |
23 | ### Added
24 |
25 | - New standalone Influx line protocol serializer
26 | - `before_send` filter option for agent and all plugins (#27)
27 |
28 | ### Changed
29 |
30 | - Require Ruby 2.7+
31 |
32 | ## [2.1.1] - 2022-08-23
33 |
34 | ### Fixed
35 |
36 | - Possible RuntimeError: can't add a new key into hash during iteration (@MrSerth)
37 |
38 | ## [2.1.0] - 2022-01-24
39 |
40 | ### Added
41 |
42 | - Support for Rails 7.0 and Ruby 3.1
43 | - Grape API instrumentation (#17)
44 |
45 | ## [2.0.0] - 2021-09-30
46 |
47 | ### Changed
48 |
49 | - The sidekiq middleware does not use keyword arguments as sidekiq does not handle them correctly on Ruby 3.0 (#14)
50 |
51 | ## [1.0.0] - 2021-01-26
52 |
53 | ### Added
54 |
55 | - Global tags (#6)
56 |
57 | ## [0.8.0] - 2020-12-02
58 |
59 | ### Added
60 |
61 | - ActiveJob instrumentation (#10)
62 |
63 | ## [0.7.0] - 2020-05-07
64 |
65 | ### Added
66 |
67 | - Sidekiq middleware (#8)
68 |
69 | ## [0.6.1] - 2020-04-01
70 |
71 | ### Fixed
72 |
73 | - Fix type in instrumentation option (#7)
74 |
75 | ## [0.6.0] - 2020-03-31
76 |
77 | ### Added
78 |
79 | - New Rack middleware and Rails plugin to collect request events (#5)
80 |
81 | ## 0.5.0 - undefined
82 |
83 | ### Changed
84 |
85 | - Remove `influxdb` not unnecessarily restrict users needing a specific influxdb client.
86 |
87 | [Unreleased]: https://github.com/jgraichen/telegraf-ruby/compare/v3.2.0...HEAD
88 | [3.2.0]: https://github.com/jgraichen/telegraf-ruby/compare/v3.1.0...v3.2.0
89 | [3.1.0]: https://github.com/jgraichen/telegraf-ruby/compare/v3.0.0...v3.1.0
90 | [3.0.0]: https://github.com/jgraichen/telegraf-ruby/compare/v2.1.1...v3.0.0
91 | [2.1.1]: https://github.com/jgraichen/telegraf-ruby/compare/v2.1.0...v2.1.1
92 | [2.1.0]: https://github.com/jgraichen/telegraf-ruby/compare/v2.0.0...v2.1.0
93 | [2.0.0]: https://github.com/jgraichen/telegraf-ruby/compare/v1.0.0...v2.0.0
94 | [1.0.0]: https://github.com/jgraichen/telegraf-ruby/compare/v0.8.0...v1.0.0
95 | [0.8.0]: https://github.com/jgraichen/telegraf-ruby/compare/v0.7.0...v0.8.0
96 | [0.7.0]: https://github.com/jgraichen/telegraf-ruby/compare/v0.6.1...v0.7.0
97 | [0.6.1]: https://github.com/jgraichen/telegraf-ruby/compare/v0.6.0...v0.6.1
98 | [0.6.0]: https://github.com/jgraichen/telegraf-ruby/compare/v0.5.0...v0.6.0
99 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 |
5 | # Specify your gem's dependencies in telegraf.gemspec
6 | gemspec
7 |
8 | gem 'rake'
9 | gem 'rake-release', '~> 1.2'
10 |
11 | gem 'rspec', '~> 3.8'
12 | gem 'rspec-github', require: false
13 |
14 | gem 'rubocop-config', github: 'jgraichen/rubocop-config', tag: 'v14'
15 |
16 | group :test do
17 | gem 'rack'
18 | gem 'rails'
19 | gem 'sidekiq'
20 | end
21 |
22 | group :development do
23 | gem 'appraisal'
24 | end
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Telegraf
2 |
3 | [](https://rubygems.org/gems/telegraf)
4 | [](https://github.com/jgraichen/telegraf-ruby/actions)
5 |
6 | Send events to a local [Telegraf](https://github.com/influxdata/telegraf) agent or anything that can receive the InfluxDB line protocol.
7 |
8 | It further includes plugins for Rack, Rails, ActiveJob and Sidekiq to collect request events. See plugin usage details below.
9 |
10 | ## Examples
11 |
12 |
50 |
51 | ## Installation
52 |
53 | ```ruby
54 | gem 'telegraf'
55 | ```
56 |
57 | And then execute:
58 |
59 | ```console
60 | bundle
61 | ```
62 |
63 | Or install it yourself as:
64 |
65 | ```ruby
66 | gem install telegraf
67 | ```
68 |
69 | ## Usage as a library
70 |
71 | Configure telegraf socket listener e.g.:
72 |
73 | ```toml
74 | [[inputs.socket_listener]]
75 | service_address = "udp://localhost:8094"
76 | ```
77 |
78 | ```ruby
79 | telegraf = Telegraf::Agent.new 'udp://localhost:8094'
80 | telegraf = Telegraf::Agent.new # default: 'udp://localhost:8094'
81 |
82 | telegraf.write('demo',
83 | tags: {tag_a: 'A', tag_b: 'B'},
84 | values: {value_a: 1, value_b: 1.5})
85 |
86 | telegraf.write([{
87 | series: 'demo',
88 | tags: {tag_a: 'A', tag_b: 'B'},
89 | values: {value_a: 1, value_b: 1.5}
90 | }])
91 | ```
92 |
93 | There is no buffer or batch handling, nor connection pooling or keep alive. Each `#write` creates a new connection (unless it's a datagram connection).
94 |
95 | There is no exception handling.
96 |
97 | ## Using the Rack and Rails plugins
98 |
99 | This gem includes a Rails plugin and middlewares / adapters for Rack, ActiveJob and Sidekiq, to collect request and background worker events. You need to require them explicitly:
100 |
101 | ### Rack
102 |
103 | ```ruby
104 | require "telegraf/rack"
105 |
106 | agent = ::Telegraf::Agent.new
107 | use ::Telegraf::Rack.new(series: 'rack', agent: agent, tags: {global: 'tag'})
108 | ```
109 |
110 | See middleware [class documentation](lib/telegraf/rack.rb) for more details.
111 |
112 | The Rack middleware supports parsing the `X-Request-Start: t=` header expecting a fractional (UTC) timestamp when the request has been started or first received by e.g. a load balancer. An additional value `queue_ms` with the queue time will be included.
113 |
114 | ### Rails
115 |
116 | The Rails plugin needs to be required, too, but will automatically install additional components (Rack, ActiveJob, Sidekiq and Rails-specific instrumentation).
117 |
118 | ```ruby
119 | # e.g. in application.rb
120 |
121 | # Load rails plugin (!) or add `require: 'telegraf/rails'` to Gemfile
122 | require "telegraf/rails"
123 |
124 | class MyApplication > ::Rails::Application
125 | # Configure receiver
126 | config.telegraf.connect = "udp://localhost:8094"
127 |
128 | # Global tags added to all events. These will override
129 | # any local tag with the same name.
130 | config.telegraf.tags = {}
131 |
132 | # By default the Rack middleware to collect events is installed
133 | config.telegraf.rack.enabled = true
134 | config.telegraf.rack.series = "requests"
135 | config.telegraf.rack.tags = {}
136 |
137 | # These are the default settings when ActiveJob is detected
138 | config.telegraf.active_job.enabled = true
139 | config.telegraf.active_job.series = "active_job"
140 | config.telegraf.active_job.tags = {}
141 |
142 | # These are the default settings when Sidekiq is detected
143 | config.telegraf.sidekiq.enabled = true
144 | config.telegraf.sidekiq.series = "sidekiq"
145 | config.telegraf.sidekiq.tags = {}
146 |
147 | # Additionally the application is instrumented to tag events with
148 | # controller and action as well as to collect app, database and view timings
149 | config.telegraf.instrumentation = true
150 | end
151 | ```
152 |
153 | Received event example:
154 |
155 | ```text
156 | requests,action=index,controller=TestController,instance=TestController#index,method=GET,status=200 db_ms=0.0,view_ms=2.6217450003969134,action_ms=2.702335,app_ms=4.603561000294576,send_ms=0.09295000018028077,request_ms=4.699011000411701,queue_ms=0.00003000028323014
157 | ```
158 |
159 | See the various classes' documentation for more details on the collected tags and values:
160 |
161 | - [Rack middleware](lib/telegraf/rack.rb)
162 | - [Rails plugin](lib/telegraf/railtie.rb)
163 | - [ActiveJob plugin](lib/telegraf/active_job.rb)
164 | - [Sidekiq middleware](lib/telegraf/sidekiq.rb)
165 |
166 | ### ActiveJob
167 |
168 | ```ruby
169 | require "telegraf/active_job"
170 |
171 | agent = ::Telegraf::Agent.new
172 | ActiveSupport::Notifications.subscribe(
173 | 'perform.active_job',
174 | Telegraf::ActiveJob.new(agent: agent, series: 'active_job', tags: {global: 'tag'})
175 | )
176 | ```
177 |
178 | See plugin [class documentation](lib/telegraf/active_job.rb) for more details.
179 |
180 | ### Sidekiq
181 |
182 | ```ruby
183 | require "telegraf/sidekiq"
184 |
185 | agent = ::Telegraf::Agent.new
186 | Sidekiq.configure_server do |config|
187 | config.server_middleware do |chain|
188 | chain.add ::Telegraf::Sidekiq::Middleware, agent, series: 'sidekiq', tags: {global: 'tag'}
189 | end
190 | end
191 | ```
192 |
193 | See middleware [class documentation](lib/telegraf/sidekiq.rb) for more details.
194 |
195 | ## License
196 |
197 | Copyright (C) 2017-2024 Jan Graichen
198 |
199 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
200 |
201 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
202 |
203 | You should have received a copy of the GNU Lesser General Public License along with this program. If not, see .
204 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rake/release/task'
4 | require 'rspec/core/rake_task'
5 |
6 | Rake::Release::Task.new do |spec|
7 | spec.sign_tag = true
8 | end
9 |
10 | RSpec::Core::RakeTask.new(:spec)
11 |
12 | task default: :spec
13 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'telegraf'
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require 'irb'
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/examples/app-perf-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/app-perf-dark.png
--------------------------------------------------------------------------------
/examples/app-perf-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/app-perf-light.png
--------------------------------------------------------------------------------
/examples/controller-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/controller-dark.png
--------------------------------------------------------------------------------
/examples/controller-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/controller-light.png
--------------------------------------------------------------------------------
/examples/jobs-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/jobs-dark.png
--------------------------------------------------------------------------------
/examples/jobs-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/jobs-light.png
--------------------------------------------------------------------------------
/examples/queue-time-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/queue-time-dark.png
--------------------------------------------------------------------------------
/examples/queue-time-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jgraichen/telegraf-ruby/956d195cb95564db3bbe246b85480eec4ced3240/examples/queue-time-light.png
--------------------------------------------------------------------------------
/gemfiles/rack_2.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack", "~> 2.0.0"
13 | gem "rails"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rack_2.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack", "~> 2.1.0"
13 | gem "rails"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rack_2.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack", "~> 2.2.0"
13 | gem "rails"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rack_3.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack", "~> 3.0"
13 | gem "rails"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rails_6.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails", "~> 6.1.0"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails", "~> 7.0.0"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails", "~> 7.1.0"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails", "~> 7.2.0"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/rails_8.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails", "~> 8.0.0"
14 | gem "sidekiq"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_6.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails"
14 | gem "sidekiq", "~> 6.0"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_7.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails"
14 | gem "sidekiq", "~> 7.0"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_8.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rake"
6 | gem "rake-release", "~> 1.2"
7 | gem "rspec", "~> 3.8"
8 | gem "rspec-github", require: false
9 | gem "rubocop-config", github: "jgraichen/rubocop-config", tag: "v13"
10 |
11 | group :test do
12 | gem "rack"
13 | gem "rails"
14 | gem "sidekiq", "~> 8.0"
15 | end
16 |
17 | group :development do
18 | gem "appraisal"
19 | end
20 |
21 | gemspec path: "../"
22 |
--------------------------------------------------------------------------------
/lib/telegraf.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'telegraf/version'
4 |
5 | module Telegraf
6 | require 'telegraf/agent'
7 | require 'telegraf/plugin'
8 | end
9 |
--------------------------------------------------------------------------------
/lib/telegraf/active_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Telegraf
4 | # Telegraf::ActiveJob
5 | #
6 | # This class collects ActiveJob queue metrics and sends them to telegraf.
7 | #
8 | #
9 | # Tags:
10 | #
11 | # * `queue`:
12 | # The queue this job landed on.
13 | #
14 | # * `job`:
15 | # The name of the job class that was executed.
16 | #
17 | # * `errors`:
18 | # Whether or not this job errored.
19 | #
20 | #
21 | # Values:
22 | #
23 | # * `app_ms`:
24 | # Total job processing time.
25 | #
26 | class ActiveJob
27 | include Plugin
28 |
29 | def initialize(series: 'active_job', **kwargs)
30 | super
31 | end
32 |
33 | def call(_name, start, finish, _id, payload)
34 | job = payload[:job]
35 |
36 | point = Point.new(
37 | tags: {
38 | **@tags,
39 | job: job.class.name,
40 | queue: job.queue_name,
41 | errors: payload.key?(:exception_object),
42 | },
43 | values: {
44 | app_ms: ((finish - start) * 1000.0), # milliseconds
45 | },
46 | )
47 |
48 | _write(point, before_send_kwargs: {payload: payload})
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/telegraf/agent.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'uri'
4 | require 'telegraf/serializer'
5 |
6 | module Telegraf
7 | class Agent
8 | DEFAULT_CONNECTION = 'udp://localhost:8094'
9 |
10 | attr_reader :uri, :logger, :tags
11 |
12 | def initialize(uri = nil, logger: nil, tags: {}, before_send: nil)
13 | @uri = URI.parse(uri || DEFAULT_CONNECTION)
14 | @tags = tags
15 | @logger = logger
16 | @serializer = Serializer.new
17 | @before_send = before_send
18 | end
19 |
20 | def write(*args, **kwargs)
21 | write!(*args, **kwargs)
22 | rescue StandardError => e
23 | logger&.error('telegraf') do
24 | e.to_s + e.backtrace.join("\n")
25 | end
26 | end
27 |
28 | def write!(data, series: nil, tags: nil, values: nil)
29 | tags = tags.merge(@tags) unless @tags.empty?
30 |
31 | if values
32 | data = [{series: series || data.to_s, tags: tags, values: values.dup}]
33 | end
34 |
35 | if @before_send
36 | data = @before_send.call(data)
37 | return unless data
38 | end
39 |
40 | socket = connect @uri
41 | socket.write(@serializer.dump_all(data))
42 | ensure
43 | socket&.close
44 | end
45 |
46 | private
47 |
48 | def connect(uri)
49 | case uri.scheme.downcase
50 | when 'unix'
51 | Socket.new(:UNIX, :STREAM).tap do |socket|
52 | socket.connect(Socket.pack_sockaddr_un(uri.path))
53 | end
54 | when 'unixgram'
55 | Socket.new(:UNIX, :DGRAM).tap do |socket|
56 | socket.connect(Socket.pack_sockaddr_un(uri.path))
57 | end
58 | when 'tcp', 'tcp4', 'tcp6'
59 | TCPSocket.new uri.host, uri.port
60 | when 'udp', 'udp4', 'udp6'
61 | UDPSocket.new.tap do |socket|
62 | socket.connect uri.host, uri.port
63 | end
64 | else
65 | raise "Unknown connection type: #{uri.scheme}"
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/telegraf/grape.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Telegraf
4 | # Telegraf::Grape
5 | #
6 | # This class extends requests metrics with details for Grape API endpoints.
7 | #
8 | #
9 | # Tags:
10 | #
11 | # * `controller`:
12 | # The Grape endpoint class.
13 | #
14 | # * `instance`:
15 | # The Grape endpoint class.
16 | #
17 | # * `format`:
18 | # Grape's internal identifier for the response format.
19 | #
20 | class Grape
21 | def call(_name, _start, _finish, _id, payload)
22 | point = payload[:env][::Telegraf::Rack::FIELD_NAME]
23 | return unless point
24 |
25 | endpoint = payload[:endpoint]
26 | return unless endpoint
27 |
28 | point.tags[:controller] = endpoint.options[:for].to_s
29 | point.tags[:instance] = point.tags[:controller]
30 | point.tags[:format] = payload[:env]['api.format']
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/telegraf/plugin.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rack'
4 |
5 | module Telegraf
6 | module Plugin
7 | # Warning: `:values` member overrides `Struct#values` and it may be
8 | # unexpected, but nothing we can change here as this is an import public API
9 | # right now.
10 | #
11 | # rubocop:disable Lint/StructNewOverride
12 | Point = Struct.new(:tags, :values, keyword_init: true) do
13 | def initialize(tags: {}, values: {})
14 | super
15 | end
16 | end
17 | # rubocop:enable Lint/StructNewOverride
18 |
19 | def initialize(agent:, series:, tags: {}, before_send: nil, **)
20 | @agent = agent
21 |
22 | @tags = tags.freeze
23 | @series = String(series).freeze
24 | @before_send = before_send
25 | end
26 |
27 | def _write(point, before_send_kwargs: {})
28 | if @before_send
29 | point = @before_send.call(point, **before_send_kwargs)
30 | return unless point
31 | end
32 |
33 | @agent.write(@series, tags: point.tags, values: point.values)
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/telegraf/rack.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rack'
4 |
5 | module Telegraf
6 | # Telegraf::Rack
7 | #
8 | # This rack middleware collects request metrics and sends them to the telegraf
9 | # agent. A `Point` data structure is added to the Rack environment to assign
10 | # custom tags and values. This point can be accessed using the environment key
11 | # defined in `::Telegraf::Rack::FIELD_NAME`.
12 | #
13 | # Example:
14 | #
15 | # if (point = request.env[::Telegraf::Rack::FIELD_NAME])
16 | # point.tags[:tag] = 'tag'
17 | # point.values[:value] = 10
18 | # end
19 | #
20 | #
21 | # Tags:
22 | #
23 | # * `status`:
24 | # Response status unless request errored
25 | #
26 | #
27 | # Values:
28 | #
29 | # * `request_ms`:
30 | # Total request processing time including response sending.
31 | #
32 | # * `app_ms`:
33 | # Total application processing time.
34 | #
35 | # * `send_ms`:
36 | # Time took to send the response body.
37 | #
38 | # * `queue_ms`:
39 | # Queue time calculated from a `X-Request-Start` header if present. The
40 | # header is expected to be formatted like this `t=` and
41 | # contain a floating point timestamp in seconds.
42 | #
43 | class Rack
44 | include ::Telegraf::Plugin
45 |
46 | FIELD_NAME = 'telegraf.rack.point'
47 | HEADER_REGEX = /t=(\d+(\.\d+)?)/.freeze
48 |
49 | def initialize(app, agent:, series: 'rack', tags: {}, logger: nil, before_send: nil) # rubocop:disable Metrics/ParameterLists
50 | super(agent: agent, series: series, tags: tags, before_send: before_send)
51 |
52 | @app = app
53 | @logger = logger
54 | end
55 |
56 | def call(env)
57 | if (request_start = extract_request_start(env))
58 | queue_ms = (::Time.now.utc - request_start) * 1000 # milliseconds
59 | end
60 |
61 | rack_start = ::Rack::Utils.clock_time
62 | point = env[FIELD_NAME] = Point.new(tags: @tags.dup)
63 | point.values[:queue_ms] = queue_ms if queue_ms
64 |
65 | begin
66 | begin
67 | status, headers, body = @app.call(env)
68 | ensure
69 | point.tags[:status] ||= status || -1
70 | point.values[:app_ms] =
71 | (::Rack::Utils.clock_time - rack_start) * 1000 # milliseconds
72 | end
73 |
74 | send_start = ::Rack::Utils.clock_time
75 | proxy = ::Rack::BodyProxy.new(body) do
76 | point.values[:send_ms] =
77 | (::Rack::Utils.clock_time - send_start) * 1000 # milliseconds
78 |
79 | finish(env, point, rack_start)
80 | end
81 |
82 | [status, headers, proxy]
83 | ensure
84 | finish(env, point, rack_start) unless proxy
85 | end
86 | end
87 |
88 | private
89 |
90 | def finish(env, point, rack_start)
91 | point.values[:request_ms] =
92 | (::Rack::Utils.clock_time - rack_start) * 1000 # milliseconds
93 |
94 | _write(point, before_send_kwargs: {request: ::Rack::Request.new(env)})
95 | rescue StandardError => e
96 | (@logger || env[::Rack::RACK_LOGGER])&.error(e)
97 | end
98 |
99 | def extract_request_start(env)
100 | return unless env.key?('HTTP_X_REQUEST_START')
101 |
102 | if (m = HEADER_REGEX.match(env['HTTP_X_REQUEST_START']))
103 | ::Time.at(m[1].to_f).utc
104 | end
105 | rescue FloatDomainError
106 | # Ignore obscure floats in Time.at (e.g. infinity)
107 | false
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/lib/telegraf/rails.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'telegraf/railtie'
4 |
--------------------------------------------------------------------------------
/lib/telegraf/railtie.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails'
4 | require 'telegraf'
5 | require 'telegraf/active_job'
6 | require 'telegraf/grape'
7 | require 'telegraf/rack'
8 | require 'telegraf/sidekiq'
9 |
10 | module Telegraf
11 | # Telegraf::Railtie
12 | #
13 | # This Rails plugin installs the rack middleware and adds intrumentation to
14 | # enrich the data point with additional tags an values.
15 | #
16 | # These include the following tags:
17 | #
18 | # * `action`
19 | # The controller action, e.g. `index`.
20 | #
21 | # * `controller`
22 | # The controller class name, e.g. `API::UsersController`.
23 | #
24 | # * `instance`
25 | # A combination of the controller class and the action, e.g.
26 | # `API::UsersController#index`.
27 | #
28 | # * `method`
29 | # The request method, e.g. `GET`.
30 | #
31 | # Additional collected values are:
32 | #
33 | # * `db_ms`
34 | # Time spend with database operations in milliseconds.
35 | #
36 | # * `view_ms`
37 | # Time spend with rendering views in milliseconds.
38 | #
39 | # * `action_ms`
40 | # Total time spend in a Rails action in milliseconds.
41 | #
42 | # These additional tags and values are collection from the
43 | # `process_action.action_controller` events usings Rails instrumentation.
44 | #
45 | class Railtie < ::Rails::Railtie
46 | config.telegraf = ::ActiveSupport::OrderedOptions.new
47 |
48 | # Connect URI or tuple
49 | config.telegraf.connect = ::Telegraf::Agent::DEFAULT_CONNECTION
50 | config.telegraf.tags = {}
51 | config.telegraf.before_send = nil
52 |
53 | # Install Rack middlewares
54 | config.telegraf.rack = ::ActiveSupport::OrderedOptions.new
55 | config.telegraf.rack.enabled = true
56 | config.telegraf.rack.series = 'requests'
57 | config.telegraf.rack.tags = {}
58 | config.telegraf.rack.before_send = nil
59 |
60 | # Install request instrumentation
61 | config.telegraf.instrumentation = true
62 |
63 | # Install Grape instrumentation
64 | config.telegraf.grape = ::ActiveSupport::OrderedOptions.new
65 | config.telegraf.grape.enabled = defined?(::Grape)
66 |
67 | # Install ActiveJob instrumentation
68 | config.telegraf.active_job = ::ActiveSupport::OrderedOptions.new
69 | config.telegraf.active_job.enabled = defined?(::ActiveJob)
70 | config.telegraf.active_job.series = 'active_job'
71 | config.telegraf.active_job.tags = {}
72 | config.telegraf.active_job.before_send = nil
73 |
74 | # Install Sidekiq middleware
75 | config.telegraf.sidekiq = ::ActiveSupport::OrderedOptions.new
76 | config.telegraf.sidekiq.enabled = defined?(::Sidekiq)
77 | config.telegraf.sidekiq.series = 'sidekiq'
78 | config.telegraf.sidekiq.tags = {}
79 | config.telegraf.sidekiq.before_send = nil
80 |
81 | initializer 'telegraf.agent' do |app|
82 | app.config.telegraf.agent ||=
83 | ::Telegraf::Agent.new(
84 | app.config.telegraf.connect,
85 | before_send: app.config.telegraf.before_send,
86 | logger: Rails.logger,
87 | tags: app.config.telegraf.tags,
88 | )
89 | end
90 |
91 | initializer 'telegraf.rack' do |app|
92 | next unless app.config.telegraf.rack.enabled
93 |
94 | app.config.middleware.insert(
95 | 0,
96 | Telegraf::Rack,
97 | agent: app.config.telegraf.agent,
98 | before_send: app.config.telegraf.rack.before_send,
99 | logger: Rails.logger,
100 | series: app.config.telegraf.rack.series,
101 | tags: app.config.telegraf.rack.tags,
102 | )
103 | end
104 |
105 | initializer 'telegraf.instrumentation' do |app|
106 | next unless app.config.telegraf.instrumentation
107 |
108 | ActiveSupport::Notifications.subscribe(
109 | 'process_action.action_controller',
110 | ) do |_name, start, finish, _id, payload|
111 | point = payload[:headers].env[::Telegraf::Rack::FIELD_NAME]
112 | next unless point
113 |
114 | point.tags[:action] = payload[:action]
115 | point.tags[:controller] = payload[:controller]
116 | point.tags[:instance] = "#{payload[:controller]}##{payload[:action]}"
117 | point.tags[:method] = payload[:method]
118 |
119 | point.values[:db_ms] = payload[:db_runtime].to_f
120 | point.values[:view_ms] = payload[:view_runtime].to_f
121 | point.values[:action_ms] = ((finish - start) * 1000.0) # milliseconds
122 | end
123 | end
124 |
125 | initializer 'telegraf.grape' do |app|
126 | next unless app.config.telegraf.grape.enabled
127 |
128 | ActiveSupport::Notifications.subscribe(
129 | 'endpoint_run.grape',
130 | Telegraf::Grape.new,
131 | )
132 | end
133 |
134 | initializer 'telegraf.active_job' do |app|
135 | next unless app.config.telegraf.active_job.enabled
136 |
137 | ActiveSupport::Notifications.subscribe(
138 | 'perform.active_job',
139 | Telegraf::ActiveJob.new(
140 | agent: app.config.telegraf.agent,
141 | before_send: app.config.telegraf.active_job.before_send,
142 | series: app.config.telegraf.active_job.series,
143 | tags: app.config.telegraf.active_job.tags,
144 | ),
145 | )
146 | end
147 |
148 | initializer 'telegraf.sidekiq' do |app|
149 | next unless app.config.telegraf.sidekiq.enabled
150 |
151 | ::Sidekiq.configure_server do |config|
152 | config.server_middleware do |chain|
153 | chain.add Telegraf::Sidekiq::Middleware,
154 | app.config.telegraf.agent,
155 | {
156 | before_send: app.config.telegraf.sidekiq.before_send,
157 | series: app.config.telegraf.sidekiq.series,
158 | tags: app.config.telegraf.sidekiq.tags,
159 | }
160 | end
161 | end
162 | end
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/lib/telegraf/serializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Telegraf
4 | class Serializer
5 | QUOTE_SERIES = /[ ,]/.freeze
6 | QUOTE_TAG_KEY = /[ ,=]/.freeze
7 | QUOTE_TAG_VALUE = /[ ,=]/.freeze
8 | QUOTE_FIELD_KEY = /[ ,="]/.freeze
9 | QUOTE_FIELD_VALUE = /[\\"]/.freeze
10 | QUOTE_REPLACE = /[\t\r\n]+/.freeze
11 |
12 | def dump_all(points)
13 | points
14 | .each
15 | .filter_map {|point| dump(point) }
16 | .join("\n")
17 | end
18 |
19 | def dump(point)
20 | series = quote(point[:series], QUOTE_SERIES)
21 | return '' if series.empty?
22 |
23 | values = point.fetch(:values).filter_map do |key, value|
24 | k = quote(key.to_s, QUOTE_FIELD_KEY)
25 | v = encode_value(value)
26 | next if k.empty? || v.nil?
27 |
28 | "#{k}=#{v}"
29 | end
30 | return '' if values.empty?
31 |
32 | tags = point[:tags]&.filter_map do |key, value|
33 | k = quote(key.to_s, QUOTE_TAG_KEY)
34 | v = quote(value.to_s, QUOTE_TAG_VALUE)
35 | next if k.empty? || v.empty?
36 |
37 | "#{k}=#{v}"
38 | end
39 |
40 | StringIO.new.tap do |io|
41 | io << series
42 | io << ',' << tags.sort.join(',') if !tags.nil? && tags.any?
43 | io << ' ' << values.sort.join(',')
44 | end.string
45 | end
46 |
47 | private
48 |
49 | def encode_value(val)
50 | if val.nil?
51 | nil
52 | elsif val.is_a?(Integer)
53 | "#{val}i"
54 | elsif val.is_a?(Numeric)
55 | if val.nan? || val.infinite?
56 | nil
57 | else
58 | val.to_s
59 | end
60 | else
61 | "\"#{quote(val.to_s, QUOTE_FIELD_VALUE)}\""
62 | end
63 | end
64 |
65 | def quote(str, rule)
66 | str
67 | .encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: '')
68 | .gsub(QUOTE_REPLACE, ' ')
69 | .gsub(rule) {|c| "\\#{c}" }
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/telegraf/sidekiq.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rack'
4 |
5 | module Telegraf
6 | module Sidekiq
7 | # Telegraf::Sidekiq::Middleware
8 | #
9 | # This Sidekiq middleware collects queue metrics and sends them to telegraf.
10 | #
11 | #
12 | # Tags:
13 | #
14 | # * `type`:
15 | # One of "job" or "scheduled_job".
16 | #
17 | # * `queue`:
18 | # The queue this job landed on.
19 | #
20 | # * `worker`:
21 | # The name of the worker class that was executed.
22 | #
23 | # * `errors`:
24 | # Whether or not this job errored.
25 | #
26 | # * `retry`:
27 | # Whether or not this execution was a retry of a previously failed one.
28 | #
29 | #
30 | # Values:
31 | #
32 | # * `app_ms`:
33 | # Total worker processing time.
34 | #
35 | # * `queue_ms`:
36 | # How long did this job wait in the queue before being processed?
37 | # Only present for "normal" (async) jobs (with tag `type` of "job").
38 | #
39 | class Middleware
40 | include ::Telegraf::Plugin
41 |
42 | def initialize(agent, options = {})
43 | options[:series] ||= 'sidekiq'
44 |
45 | super(
46 | **options,
47 | agent: agent
48 | )
49 | end
50 |
51 | def call(worker, job, queue)
52 | job_start = ::Time.now.utc
53 |
54 | point = Point.new(
55 | tags: {
56 | **@tags,
57 | type: 'job',
58 | errors: true,
59 | retry: job.key?('retried_at'),
60 | queue: queue,
61 | worker: worker.class.name,
62 | },
63 | values: {
64 | retry_count: job['retry_count'],
65 | }.compact,
66 | )
67 |
68 | # The "enqueued_at" key is not present for scheduled jobs.
69 | # See https://github.com/mperham/sidekiq/wiki/Job-Format.
70 | if job.key?('enqueued_at')
71 | enqueued_at = Time.at(
72 | if defined?(::Sidekiq::MAJOR) && ::Sidekiq::MAJOR >= 8
73 | job['enqueued_at'].to_f / 1000
74 | else
75 | job['enqueued_at'].to_f
76 | end,
77 | ).utc
78 |
79 | point.values[:queue_ms] = (job_start - enqueued_at) * 1000 # milliseconds
80 | end
81 |
82 | # The "at" key is only present for scheduled jobs.
83 | point.tags[:type] = 'scheduled_job' if job.key?('at')
84 |
85 | begin
86 | yield
87 |
88 | # If we get here, this was a successful execution
89 | point.tags[:errors] = false
90 | ensure
91 | job_stop = ::Time.now.utc
92 |
93 | point.values[:app_ms] = (job_stop - job_start) * 1000 # milliseconds
94 |
95 | _write(point, before_send_kwargs: {worker: worker, job: job, queue: queue})
96 | end
97 | end
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/telegraf/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Telegraf
4 | module VERSION
5 | MAJOR = 3
6 | MINOR = 2
7 | PATCH = 0
8 | STAGE = nil
9 | STRING = [MAJOR, MINOR, PATCH, STAGE].compact.join('.').freeze
10 |
11 | def self.to_s
12 | STRING
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/renovate.json5:
--------------------------------------------------------------------------------
1 | {
2 | $schema: "https://docs.renovatebot.com/renovate-schema.json",
3 | extends: [
4 | "local>jgraichen/renovate-config"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/setup'
4 | require 'telegraf'
5 |
6 | require 'socket'
7 | require 'tmpdir'
8 |
9 | module Support
10 | module Tmpdir
11 | extend RSpec::SharedContext
12 |
13 | attr_reader :tmpdir
14 |
15 | around do |example|
16 | Dir.mktmpdir do |dir|
17 | @tmpdir = dir
18 | example.run
19 | end
20 | end
21 | end
22 |
23 | module Socket
24 | extend RSpec::SharedContext
25 |
26 | Point = Struct.new(:series, :tags, :values, :timestamp) # rubocop:disable Lint/StructNewOverride
27 |
28 | let(:socket) { nil }
29 | let(:last_points) { socket_parse }
30 | let(:last_point) { last_points.first }
31 |
32 | around do |example|
33 | socket
34 | example.run
35 | ensure
36 | socket&.close
37 | end
38 |
39 | def socket_read
40 | if socket.respond_to?(:accept_nonblock)
41 | begin
42 | socket.accept_nonblock.read_nonblock(4096)
43 | rescue Errno::ENOTSUP # unixgram
44 | socket.read_nonblock(4096)
45 | end
46 | else
47 | socket.read_nonblock(4096)
48 | end
49 | end
50 |
51 | def socket_parse
52 | [].tap do |data|
53 | loop do
54 | socket_read.lines.each {|line| data << _parse(line) }
55 | rescue IO::EAGAINWaitReadable
56 | break
57 | end
58 | end
59 | end
60 |
61 | REGEXP = /^(?\w+)(?:,(?.*))? (?.*)(? \d+)?$/.freeze
62 |
63 | private
64 |
65 | def _parse(line)
66 | if (m = REGEXP.match(line))
67 | return Point.new(
68 | m['series'],
69 | _parse_fields(m['tags']),
70 | _parse_fields(m['values']),
71 | m['ts']&.strip&.to_i,
72 | )
73 | end
74 |
75 | raise "Cannot parse: #{line}"
76 | end
77 |
78 | def _parse_fields(str)
79 | return {} unless str
80 |
81 | str.split(',').to_h do |s|
82 | s.split('=')
83 | end
84 | end
85 | end
86 | end
87 |
88 | RSpec.configure do |config|
89 | # Enable flags like --only-failures and --next-failure
90 | config.example_status_persistence_file_path = '.rspec_status'
91 |
92 | # Disable RSpec exposing methods globally on `Module` and `main`
93 | config.disable_monkey_patching!
94 |
95 | config.expect_with :rspec do |c|
96 | c.syntax = :expect
97 | end
98 |
99 | config.include Support::Tmpdir
100 | config.include Support::Socket
101 |
102 | # Use the GitHub Annotations formatter for CI
103 | if ENV['GITHUB_ACTIONS'] == 'true'
104 | require 'rspec/github'
105 | config.add_formatter RSpec::Github::Formatter
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/spec/telegraf/active_job_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | require 'telegraf/active_job'
6 | require 'tmpdir'
7 |
8 | require 'logger'
9 | require 'active_job'
10 |
11 | class HardJob < ActiveJob::Base
12 | def perform(_message)
13 | # noop
14 | end
15 | end
16 |
17 | class FailJob < ActiveJob::Base
18 | def perform(message)
19 | raise message
20 | end
21 | end
22 |
23 | RSpec.describe Telegraf::ActiveJob do
24 | subject(:work!) { HardJob.perform_now 'test' }
25 |
26 | let(:socket) { UNIXServer.new "#{tmpdir}/sock" }
27 | let(:agent) { Telegraf::Agent.new "unix:#{tmpdir}/sock" }
28 | let(:args) { {} }
29 |
30 | before do
31 | ActiveSupport::Notifications.subscribe(
32 | 'perform.active_job',
33 | Telegraf::ActiveJob.new(agent: agent, **args),
34 | )
35 |
36 | ActiveJob::Base.logger = Logger.new(IO::NULL)
37 | end
38 |
39 | context 'successful async execution' do
40 | it 'job=HardJob,queue=default,errors=false app_ms' do
41 | work!
42 |
43 | expect(last_point.series).to eq 'active_job'
44 | expect(last_point.tags).to include 'job' => 'HardJob', 'queue' => 'default', 'errors' => 'false'
45 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/
46 | end
47 | end
48 |
49 | context 'with error' do
50 | subject(:work!) { FailJob.perform_now 'test' }
51 |
52 | it 'job=HardJob,queue=default,errors=true app_ms' do
53 | work! rescue nil
54 |
55 | expect(last_point.series).to eq 'active_job'
56 | expect(last_point.tags).to include 'job' => 'FailJob', 'queue' => 'default', 'errors' => 'true'
57 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/
58 | end
59 | end
60 |
61 | context 'with custom series name' do
62 | let(:args) { {series: 'background'} }
63 |
64 | it 'uses the custom series' do
65 | work!
66 |
67 | expect(last_point.series).to eq 'background'
68 | expect(last_point.tags).to include 'job' => 'HardJob', 'queue' => 'default', 'errors' => 'false'
69 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/
70 | end
71 | end
72 |
73 | context 'with extra tags' do
74 | let(:args) { {tags: {my: 'tag'}} }
75 |
76 | it 'adds the tag to the standard tags' do
77 | work!
78 |
79 | expect(last_point.series).to eq 'active_job'
80 | expect(last_point.tags).to include 'job' => 'HardJob', 'queue' => 'default', 'errors' => 'false', 'my' => 'tag'
81 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/telegraf/agent_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | require 'telegraf'
6 |
7 | RSpec.describe Telegraf::Agent do
8 | subject(:agent) { Telegraf::Agent.new("unix:#{tmpdir}/sock", **kwargs) }
9 |
10 | let(:kwargs) { {} }
11 | let(:socket) { UNIXServer.new "#{tmpdir}/sock" }
12 |
13 | describe '#write' do
14 | context 'with tags' do
15 | let(:kwargs) { {tags: {app: 'test', tagged: 'yes'}} }
16 |
17 | it 'merges tags into event' do
18 | # Write app tag here too to ensure it is overwritten by global tags
19 | agent.write!('series', tags: {app: 'no', field: 'yes'}, values: {a: 1})
20 | expect(socket_read).to eq 'series,app=test,field=yes,tagged=yes a=1i'
21 | end
22 | end
23 | end
24 |
25 | describe '@before_send' do
26 | let(:kwargs) { {before_send: before_send} }
27 | let(:before_send) do
28 | lambda {|data|
29 | data.delete_if {|line| line[:tags].key?(:drop) }
30 | data
31 | }
32 | end
33 |
34 | it 'drops all points when all points match' do
35 | agent.write!('series', tags: {drop: 1}, values: {a: 1})
36 | expect { socket_read }.to raise_error(EOFError)
37 | end
38 |
39 | it 'drops matching points' do
40 | agent.write!('series', tags: {drop: 1}, values: {a: 1})
41 | agent.write!('series', tags: {field: 'yes'}, values: {a: 1})
42 |
43 | expect { socket_read }.to raise_error(EOFError)
44 | expect(socket_read).to eq 'series,field=yes a=1i'
45 | end
46 |
47 | it 'drops matching points in bulk mode' do
48 | agent.write!([
49 | {series: 'series', tags: {drop: 1}, values: {a: 1}},
50 | {series: 'series', tags: {field: 'yes'}, values: {a: 1}},
51 | ])
52 |
53 | expect(socket_read).to eq 'series,field=yes a=1i'
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/telegraf/rack_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | require 'rack/mock'
6 | require 'telegraf/rack'
7 | require 'tmpdir'
8 |
9 | require 'logger'
10 |
11 | RSpec.describe Telegraf::Rack do
12 | subject(:mock) do
13 | Rack::MockRequest.new(described_class.new(app, agent: agent, **args))
14 | end
15 |
16 | let(:socket) { UNIXServer.new "#{tmpdir}/sock" }
17 | let(:agent) { Telegraf::Agent.new "unix:#{tmpdir}/sock" }
18 | let(:args) { {} }
19 |
20 | let(:app) { ->(_env) { [200, {}, []] } }
21 |
22 | context 'with successful request' do
23 | it 'status=200 app_ms,send_ms,request_ms' do
24 | mock.request
25 | expect(socket_read).to match(/\Arack,status=200 app_ms=\d+\.\d+,request_ms=\d+\.\d+,send_ms=\d+\.\d+\z/)
26 | end
27 | end
28 |
29 | context 'with a 404 response' do
30 | let(:app) { ->(_env) { [404, {}, []] } }
31 |
32 | it 'includes status=404' do
33 | mock.request
34 | expect(last_points.size).to eq 1
35 | expect(last_point.tags).to eq 'status' => '404'
36 | end
37 | end
38 |
39 | context 'with error' do
40 | let(:app) { ->(_env) { raise 'fail' } }
41 |
42 | it 'status=-1 app_ms,request_ms' do
43 | mock.request rescue nil
44 | expect(last_points.size).to eq 1
45 | expect(last_point.tags).to eq 'status' => '-1'
46 | expect(last_point.values.keys).to match_array %w[app_ms request_ms]
47 | end
48 | end
49 |
50 | context 'with X-Request-Start' do
51 | it 'includes queue_ms value' do
52 | mock.request('GET', '/', {'HTTP_X_REQUEST_START' => "t=#{Time.now.utc.to_f}"})
53 |
54 | expect(last_points.size).to eq 1
55 | expect(last_point.values.keys).to include 'queue_ms'
56 | expect(last_point.values['queue_ms']).to match(/\A\d+\.\d+\z/)
57 | end
58 | end
59 |
60 | context 'with extra tags and values' do
61 | let(:app) do
62 | lambda do |env|
63 | env['telegraf.rack.point'].tags[:my] = 'tag'
64 | env[Telegraf::Rack::FIELD_NAME].values[:val] = 100
65 |
66 | [200, {}, []]
67 | end
68 | end
69 |
70 | it 'status=200 app_ms,send_ms,request_ms' do
71 | mock.request
72 |
73 | expect(last_points.size).to eq 1
74 | expect(last_point.tags).to include 'my' => 'tag'
75 | expect(last_point.values).to include 'val' => '100i'
76 | end
77 | end
78 |
79 | context 'with write error' do
80 | before { allow(agent).to receive(:write).and_raise('write failed') }
81 |
82 | let(:io) { StringIO.new }
83 |
84 | it 'logs error to rack logger' do
85 | mock.request('GET', '/', {'rack.logger' => Logger.new(io)})
86 |
87 | expect(io.string).to match(/ERROR .* write failed \(RuntimeError\)/)
88 | end
89 |
90 | context 'with explicitly given logger' do
91 | let(:args) { {logger: Logger.new(io)} }
92 |
93 | it 'logs error to given logger' do
94 | rackio = StringIO.new
95 | mock.request('GET', '/', {'rack.logger' => Logger.new(rackio)})
96 |
97 | expect(io.string).to match(/ERROR .* write failed \(RuntimeError\)/)
98 | expect(rackio.string).to be_empty
99 | end
100 | end
101 |
102 | context 'without logger' do
103 | it 'nothing happens' do
104 | expect do
105 | mock.request('GET', '/', {'rack.logger' => nil})
106 | end.not_to raise_error
107 | end
108 | end
109 | end
110 |
111 | context 'with before_send filter' do
112 | let(:args) { {before_send: before_send} }
113 |
114 | context 'dropping rack paths' do
115 | let(:before_send) do
116 | lambda {|point, request:, **|
117 | return if %r{^/healthcheck}.match?(request.path)
118 |
119 | point.values[:path] = request.path
120 | point
121 | }
122 | end
123 |
124 | it 'drops matching points' do
125 | mock.request('GET', '/', {})
126 | mock.request('GET', '/healthcheck', {})
127 |
128 | expect(last_points.size).to eq 1
129 | expect(last_point.values).to include('path' => '"/"')
130 | end
131 | end
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/spec/telegraf/railtie_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 | require 'telegraf/rails'
5 |
6 | require 'action_controller'
7 |
8 | ENV['RAILS_ENV'] ||= 'production'
9 |
10 | class TestController < ActionController::Base
11 | def index
12 | render plain: 'test'
13 | end
14 | end
15 |
16 | RSpec.describe Telegraf::Railtie do
17 | subject(:app) do
18 | Class.new(Rails::Application) do
19 | config.eager_load = true
20 | config.secret_key_base = 'secret'
21 | config.action_dispatch.show_exceptions = false
22 |
23 | routes.append do
24 | get '/' => 'test#index'
25 | end
26 | end
27 | end
28 |
29 | around do |example|
30 | defaults = config.telegraf.deep_dup
31 | begin
32 | example.run
33 | ensure
34 | config.telegraf = defaults
35 | end
36 | end
37 |
38 | let(:config) { app.config }
39 | let(:application) do
40 | # Rails removes the support of multiple instances, which includes freezing some setting values.
41 | # This is the workaround to avoid FrozenError. Related issue: https://github.com/rails/rails/issues/42319
42 | ActiveSupport::Dependencies.autoload_once_paths = []
43 | ActiveSupport::Dependencies.autoload_paths = []
44 |
45 | app.tap(&:initialize!)
46 | end
47 |
48 | describe '' do
49 | describe 'telegraf.connect' do
50 | it 'defaults to ::Telegraf::Agent::DEFAULT_CONNECTION' do
51 | expect(config.telegraf.connect).to eq Telegraf::Agent::DEFAULT_CONNECTION
52 | end
53 | end
54 |
55 | describe 'telegraf.tags' do
56 | subject { config.telegraf.tags }
57 |
58 | it { is_expected.to eq({}) }
59 | end
60 |
61 | describe 'telegraf.rack.enabled' do
62 | subject { config.telegraf.rack.enabled }
63 |
64 | it { is_expected.to be true }
65 | end
66 |
67 | describe 'telegraf.rack.series' do
68 | subject { config.telegraf.rack.series }
69 |
70 | it { is_expected.to eq 'requests' }
71 | end
72 |
73 | describe 'telegraf.rack.tags' do
74 | subject { config.telegraf.rack.tags }
75 |
76 | it { is_expected.to eq({}) }
77 | end
78 | end
79 |
80 | describe '' do
81 | it 'creates a telegraf agent' do
82 | config.telegraf.connect = 'tcp://localhost:1234'
83 | config.telegraf.tags = {app: 'name'}
84 |
85 | application.config.telegraf.agent.tap do |agent|
86 | expect(agent).to be_a Telegraf::Agent
87 | expect(agent.uri).to eq URI.parse('tcp://localhost:1234')
88 | expect(agent.tags).to eq({app: 'name'})
89 | end
90 | end
91 |
92 | it 'installs middleware in first place' do
93 | middleware = application.config.middleware.first
94 | expect(middleware.klass).to eq Telegraf::Rack
95 |
96 | kwargs = middleware.args.first
97 | expect(kwargs[:agent]).to eq application.config.telegraf.agent
98 | expect(kwargs[:series]).to eq 'requests'
99 | expect(kwargs[:tags]).to eq({})
100 | end
101 |
102 | context 'with rack disabled' do
103 | before { config.telegraf.rack.enabled = false }
104 |
105 | it 'does not install middleware' do
106 | expect(application.config.middleware.to_a).not_to include Telegraf::Rack
107 | end
108 | end
109 | end
110 |
111 | describe '' do
112 | let(:mock) { Rack::MockRequest.new(application) }
113 | let(:socket) { UDPSocket.new.tap {|s| s.bind('localhost', 8094) } }
114 |
115 | it 'assigns extra tags and values' do
116 | expect(application.config.middleware).to include Telegraf::Rack
117 |
118 | response = mock.request
119 | expect(response.status).to eq 200
120 | expect(response.body).to eq 'test'
121 |
122 | parsed = socket_parse
123 | expect(parsed.size).to eq 1
124 | expect(parsed[0].series).to eq 'requests'
125 | expect(parsed[0].tags).to eq({
126 | 'action' => 'index',
127 | 'controller' => 'TestController',
128 | 'instance' => 'TestController#index',
129 | 'method' => 'GET',
130 | 'status' => '200',
131 | })
132 | expect(parsed[0].values.keys).to match_array %w[action_ms app_ms db_ms request_ms send_ms view_ms]
133 | end
134 |
135 | context 'with global tags' do
136 | before { config.telegraf.rack.tags = {my: 'tag'} }
137 |
138 | it 'include global tags' do
139 | mock.request
140 | expect(socket_parse[0].tags).to include 'my' => 'tag'
141 | end
142 | end
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/spec/telegraf/serializer_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | require 'telegraf'
6 |
7 | RSpec.describe Telegraf::Serializer do
8 | subject(:serializer) { Telegraf::Serializer.new }
9 |
10 | describe '#dump' do
11 | it 'escapes whitespace in series' do
12 | expect(
13 | serializer.dump({
14 | series: 'my series',
15 | values: {a: 1},
16 | }),
17 | ).to eq 'my\ series a=1i'
18 | end
19 |
20 | it 'replaces LF with space' do
21 | expect(
22 | serializer.dump({
23 | series: 'series',
24 | tags: {app: "no\nyes"},
25 | values: {a: 1},
26 | }),
27 | ).to eq 'series,app=no\\ yes a=1i'
28 | end
29 |
30 | it 'replaces TAB with space' do
31 | expect(
32 | serializer.dump({
33 | series: 'series',
34 | tags: {app: "no\tyes"},
35 | values: {a: 1},
36 | }),
37 | ).to eq 'series,app=no\\ yes a=1i'
38 | end
39 |
40 | it 'replaces CR with space' do
41 | expect(
42 | serializer.dump({
43 | series: 'series',
44 | tags: {app: "no\ryes"},
45 | values: {a: 1},
46 | }),
47 | ).to eq 'series,app=no\\ yes a=1i'
48 | end
49 |
50 | it 'replaces CRLF with space' do
51 | expect(
52 | serializer.dump({
53 | series: 'series',
54 | tags: {app: "no\r\nyes"},
55 | values: {a: 1},
56 | }),
57 | ).to eq 'series,app=no\\ yes a=1i'
58 | end
59 |
60 | it 'escapes quotes in field values' do
61 | expect(
62 | serializer.dump({
63 | series: 'series',
64 | values: {a: 'string "data"'},
65 | }),
66 | ).to eq 'series a="string \"data\""'
67 | end
68 |
69 | it 'strips invalid UTF-8 from series' do
70 | expect(
71 | serializer.dump({
72 | series: "series\xC9",
73 | values: {a: 1},
74 | }),
75 | ).to eq 'series a=1i'
76 | end
77 |
78 | it 'skips series with only invalid UTF-8' do
79 | expect(
80 | serializer.dump({
81 | series: "\xC9",
82 | values: {a: 1},
83 | }),
84 | ).to eq ''
85 | end
86 |
87 | it 'ignores nil value' do
88 | expect(
89 | serializer.dump({
90 | series: 'series',
91 | values: {a: 1, b: nil},
92 | }),
93 | ).to eq 'series a=1i'
94 | end
95 |
96 | it 'ignores nil values' do
97 | expect(
98 | serializer.dump({
99 | series: 'series',
100 | values: {a: nil},
101 | }),
102 | ).to eq ''
103 | end
104 |
105 | it 'ignores value name with invalid encoding' do
106 | expect(
107 | serializer.dump({
108 | series: 'series',
109 | values: {a: 1, '\xC9': nil},
110 | }),
111 | ).to eq 'series a=1i'
112 | end
113 |
114 | it 'ignores value names with invalid encoding' do
115 | expect(
116 | serializer.dump({
117 | series: 'series',
118 | values: {'\xC9': nil},
119 | }),
120 | ).to eq ''
121 | end
122 |
123 | it 'ignores nil tag' do
124 | expect(
125 | serializer.dump({
126 | series: 'series',
127 | tags: {a: 'test', b: nil},
128 | values: {a: 1},
129 | }),
130 | ).to eq 'series,a=test a=1i'
131 | end
132 |
133 | it 'ignores nil tags' do
134 | expect(
135 | serializer.dump({
136 | series: 'series',
137 | tags: {a: nil},
138 | values: {a: 1},
139 | }),
140 | ).to eq 'series a=1i'
141 | end
142 |
143 | it 'ignores NaN values' do
144 | expect(
145 | serializer.dump({
146 | series: 'series',
147 | values: {a: 1, b: 0 / 0.0},
148 | }),
149 | ).to eq 'series a=1i'
150 | end
151 |
152 | it 'ignores Infinity values' do
153 | expect(
154 | serializer.dump({
155 | series: 'series',
156 | values: {a: 1, b: 1 / 0.0},
157 | }),
158 | ).to eq 'series a=1i'
159 | end
160 | end
161 | end
162 |
--------------------------------------------------------------------------------
/spec/telegraf/sidekiq_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | require 'telegraf/sidekiq'
6 | require 'tmpdir'
7 |
8 | require 'sidekiq'
9 | require 'sidekiq/testing'
10 |
11 | class HardWorker
12 | include Sidekiq::Worker
13 |
14 | def perform(_message)
15 | # noop
16 | end
17 | end
18 |
19 | class FailWorker
20 | include Sidekiq::Worker
21 |
22 | def perform(message)
23 | raise message
24 | end
25 | end
26 |
27 | RSpec.describe Telegraf::Sidekiq::Middleware do
28 | subject(:work!) do
29 | queue!
30 | Sidekiq::Worker.drain_all
31 | end
32 |
33 | let(:queue!) { HardWorker.perform_async('test') }
34 | let(:socket) { UNIXServer.new "#{tmpdir}/sock" }
35 | let(:agent) { Telegraf::Agent.new "unix:#{tmpdir}/sock" }
36 | let(:args) { {} }
37 |
38 | before do
39 | Sidekiq::Testing.server_middleware do |chain|
40 | chain.add Telegraf::Sidekiq::Middleware, agent, args
41 | end
42 |
43 | Sidekiq::Worker.clear_all
44 | end
45 |
46 | context 'successful async execution' do
47 | it 'type=job,errors=false app_ms,queue_ms' do
48 | work!
49 |
50 | expect(last_point.series).to eq 'sidekiq'
51 | expect(last_point.tags).to include(
52 | 'type' => 'job',
53 | 'worker' => 'HardWorker',
54 | 'queue' => 'default',
55 | 'errors' => 'false',
56 | 'retry' => 'false',
57 | )
58 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/, 'queue_ms' => /^\d+\.\d+$/
59 | end
60 | end
61 |
62 | context 'successful scheduled execution' do
63 | let(:queue!) { HardWorker.perform_at(Time.now.utc + 20, 'test') }
64 |
65 | it 'type=scheduled_job,errors=false app_ms' do
66 | work!
67 |
68 | expect(last_point.series).to eq 'sidekiq'
69 | expect(last_point.tags).to include(
70 | 'type' => 'scheduled_job',
71 | 'worker' => 'HardWorker',
72 | 'queue' => 'default',
73 | 'errors' => 'false',
74 | 'retry' => 'false',
75 | )
76 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/
77 | end
78 | end
79 |
80 | context 'with error' do
81 | let(:queue!) { FailWorker.perform_async('test') }
82 |
83 | it 'type=job,errors=true app_ms,queue_ms' do
84 | begin
85 | work!
86 | rescue StandardError
87 | nil
88 | end
89 |
90 | expect(last_point.series).to eq 'sidekiq'
91 | expect(last_point.tags).to include(
92 | 'type' => 'job',
93 | 'worker' => 'FailWorker',
94 | 'queue' => 'default',
95 | 'errors' => 'true',
96 | 'retry' => 'false',
97 | )
98 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/, 'queue_ms' => /^\d+\.\d+$/
99 | end
100 | end
101 |
102 | context 'with custom series name' do
103 | let(:args) { {series: 'background'} }
104 |
105 | it 'uses the custom series' do
106 | work!
107 |
108 | expect(last_point.series).to eq 'background'
109 | expect(last_point.tags).to include(
110 | 'type' => 'job',
111 | 'worker' => 'HardWorker',
112 | 'queue' => 'default',
113 | 'errors' => 'false',
114 | 'retry' => 'false',
115 | )
116 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/, 'queue_ms' => /^\d+\.\d+$/
117 | end
118 | end
119 |
120 | context 'with extra tags' do
121 | let(:args) { {tags: {my: 'tag'}} }
122 |
123 | it 'adds the tag to the standard tags' do
124 | work!
125 |
126 | expect(last_point.series).to eq 'sidekiq'
127 | expect(last_point.tags).to include(
128 | 'type' => 'job',
129 | 'worker' => 'HardWorker',
130 | 'queue' => 'default',
131 | 'errors' => 'false',
132 | 'retry' => 'false',
133 | 'my' => 'tag',
134 | )
135 | expect(last_point.values).to match 'app_ms' => /^\d+\.\d+$/, 'queue_ms' => /^\d+\.\d+$/
136 | end
137 | end
138 |
139 | context 'with before_send filter' do
140 | let(:args) { {before_send: before_send} }
141 |
142 | context 'excluding specific worker' do
143 | let(:before_send) do
144 | lambda {|point, worker:, **|
145 | return if worker.instance_of?(HardWorker)
146 |
147 | point
148 | }
149 | end
150 |
151 | it 'drops matching points' do
152 | work!
153 | expect(last_points.size).to eq 0
154 | end
155 | end
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/spec/telegraf_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | require 'tmpdir'
6 |
7 | RSpec.describe Telegraf do
8 | it 'has a version number' do
9 | expect(Telegraf::VERSION).not_to be_nil
10 | end
11 |
12 | context 'with UNIX socket' do
13 | let(:agent) { Telegraf::Agent.new "unix:#{tmpdir}/sock" }
14 | let(:socket) { UNIXServer.new "#{tmpdir}/sock" }
15 |
16 | it 'writes multiple points' do
17 | agent.write(
18 | [
19 | {series: 'demo', tags: {a: 1, b: 2}, values: {a: 1, b: 2.1}},
20 | {series: 'demo', tags: {a: '1', b: 2}, values: {a: 6, b: 2.5}},
21 | ],
22 | )
23 |
24 | expect(socket_read).to eq "demo,a=1,b=2 a=1i,b=2.1\ndemo,a=1,b=2 a=6i,b=2.5"
25 | end
26 |
27 | it 'writes single points' do
28 | agent.write('demo', tags: {a: 1, b: 2}, values: {a: 1, b: 2.1})
29 | expect(socket_read).to eq 'demo,a=1,b=2 a=1i,b=2.1'
30 | end
31 | end
32 |
33 | context 'with UNIXGRAM socket' do
34 | let(:agent) { Telegraf::Agent.new "unixgram:#{tmpdir}/sock" }
35 | let(:socket) do
36 | Socket.new(:UNIX, :DGRAM).tap do |socket|
37 | socket.bind Socket.pack_sockaddr_un "#{tmpdir}/sock"
38 | end
39 | end
40 |
41 | it 'write points' do
42 | agent.write('demo', tags: {a: 1, b: 2}, values: {a: 1, b: 2.1})
43 | expect(socket_read).to eq 'demo,a=1,b=2 a=1i,b=2.1'
44 | end
45 | end
46 |
47 | context 'with TCP socket' do
48 | let(:agent) { Telegraf::Agent.new 'tcp://localhost:8094' }
49 | let(:socket) { TCPServer.new 'localhost', 8094 }
50 |
51 | it 'write points' do
52 | agent.write('demo', tags: {a: 1, b: 2}, values: {a: 1, b: 2.1})
53 | expect(socket_read).to eq 'demo,a=1,b=2 a=1i,b=2.1'
54 | end
55 | end
56 |
57 | context 'with UDP socket' do
58 | let(:agent) { Telegraf::Agent.new 'udp://localhost:8094' }
59 | let(:socket) { UDPSocket.new.tap {|s| s.bind 'localhost', 8094 } }
60 |
61 | it 'write points' do
62 | agent.write('demo', tags: {a: 1, b: 2}, values: {a: 1, b: 2.1})
63 | expect(socket_read).to eq 'demo,a=1,b=2 a=1i,b=2.1'
64 | end
65 | end
66 |
67 | context 'with default' do
68 | let(:agent) { Telegraf::Agent.new 'udp://localhost:8094' }
69 | let(:socket) { UDPSocket.new.tap {|s| s.bind 'localhost', 8094 } }
70 |
71 | it 'writes points to UDP on localhost:8094' do
72 | agent.write('demo', tags: {a: 1, b: 2}, values: {a: 1, b: 2.1})
73 | expect(socket_read).to eq 'demo,a=1,b=2 a=1i,b=2.1'
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/telegraf.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | lib = File.expand_path('lib', __dir__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require 'telegraf/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'telegraf'
9 | spec.version = Telegraf::VERSION
10 | spec.authors = ['Jan Graichen']
11 | spec.email = ['jgraichen@altimos.de']
12 |
13 | spec.summary = 'Metric Reporter to local telegraf agent'
14 | spec.description = 'Metric Reporter to local telegraf agent'
15 | spec.homepage = 'https://github.com/jgraichen/telegraf-ruby'
16 | spec.license = 'LGPLv3'
17 |
18 | spec.metadata = {
19 | 'rubygems_mfa_required' => 'true',
20 | }
21 |
22 | spec.files = `git ls-files -z`.split("\x0").reject do |f|
23 | f.match(%r{^(test|spec|features)/})
24 | end
25 |
26 | spec.bindir = 'exe'
27 | spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
28 | spec.require_paths = ['lib']
29 |
30 | spec.required_ruby_version = '>= 2.7'
31 | end
32 |
--------------------------------------------------------------------------------