├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.markdown ├── Rakefile ├── SECURITY.md ├── lib ├── riemann.rb └── riemann │ ├── attribute.rb │ ├── auto_state.rb │ ├── client.rb │ ├── client │ ├── ssl_socket.rb │ ├── tcp.rb │ ├── tcp_socket.rb │ └── udp.rb │ ├── event.rb │ ├── message.rb │ ├── metric_thread.rb │ ├── query.rb │ ├── state.rb │ └── version.rb ├── riemann-client.gemspec └── spec ├── client_spec.rb ├── riemann.config ├── shared_examples.rb └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Open PR for gem updates 9 | - package-ecosystem: "bundler" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "daily" 13 | 14 | # Open PR for GitHub Actions updates 15 | - package-ecosystem: "github-actions" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: '2.7' 21 | bundler-cache: true 22 | - name: Run rubocop 23 | run: bundle exec rubocop 24 | test: 25 | needs: lint 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | ruby-version: 30 | - '2.6' 31 | - '2.7' 32 | - '3.0' 33 | - '3.1' 34 | - '3.2' 35 | - '3.3' 36 | - '3.4' 37 | steps: 38 | - uses: actions/checkout@v4 39 | - name: Setup Ruby 40 | uses: ruby/setup-ruby@v1 41 | with: 42 | ruby-version: ${{ matrix.ruby-version }} 43 | bundler-cache: true 44 | - name: Install riemann 45 | run: | 46 | wget --quiet https://github.com/riemann/riemann/releases/download/0.3.8/riemann_0.3.8_all.deb 47 | sudo dpkg -i riemann_0.3.8_all.deb 48 | 49 | sudo systemctl stop riemann 50 | 51 | sudo openssl genrsa -out /etc/riemann/riemann_server.key 4096 52 | sudo openssl pkcs8 -topk8 -nocrypt -in /etc/riemann/riemann_server.key -out /etc/riemann/riemann_server.pkcs8 53 | sudo openssl req -x509 -new -nodes -key /etc/riemann/riemann_server.key -days 7 -out /etc/riemann/riemann_server.crt -subj '/CN=localhost' 54 | sudo chmod +r /etc/riemann/riemann_server.pkcs8 55 | sudo cp -v spec/riemann.config /etc/riemann/ 56 | 57 | sudo systemctl start riemann 58 | 59 | while ! nc -z localhost 5555; do sleep 1; done 60 | - name: Run the test suite 61 | run: bundle exec rspec 62 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '33 17 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | ._* 3 | *~ 4 | .DS_Store 5 | .*.swp 6 | *.log 7 | .bundle/ 8 | bin/ 9 | vendor/ 10 | Gemfile.lock 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format doc 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | TargetRubyVersion: 2.6 4 | require: 5 | - rubocop-rspec 6 | Metrics/AbcSize: 7 | Enabled: false 8 | Metrics/BlockLength: 9 | Enabled: false 10 | Metrics/ClassLength: 11 | Enabled: false 12 | Metrics/CyclomaticComplexity: 13 | Enabled: false 14 | Metrics/MethodLength: 15 | Enabled: false 16 | Metrics/PerceivedComplexity: 17 | Enabled: false 18 | RSpec/IndexedLet: 19 | Enabled: false 20 | Style/Documentation: 21 | Enabled: false 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.2.1](https://github.com/riemann/riemann-ruby-client/tree/v1.2.1) (2023-07-30) 4 | 5 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/v1.2.0...v1.2.1) 6 | 7 | **Fixed bugs:** 8 | 9 | - Fix sending large batch of events over TLS [\#51](https://github.com/riemann/riemann-ruby-client/pull/51) ([smortex](https://github.com/smortex)) 10 | 11 | ## [v1.2.0](https://github.com/riemann/riemann-ruby-client/tree/v1.2.0) (2023-06-28) 12 | 13 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/v1.1.0...v1.2.0) 14 | 15 | **Implemented enhancements:** 16 | 17 | - Allow connection to Riemann using TLS 1.3 [\#49](https://github.com/riemann/riemann-ruby-client/pull/49) ([smortex](https://github.com/smortex)) 18 | 19 | ## [v1.1.0](https://github.com/riemann/riemann-ruby-client/tree/v1.1.0) (2023-01-23) 20 | 21 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/v1.0.1...v1.1.0) 22 | 23 | **Implemented enhancements:** 24 | 25 | - Add support for sending events in bulk [\#44](https://github.com/riemann/riemann-ruby-client/pull/44) ([smortex](https://github.com/smortex)) 26 | 27 | **Fixed bugs:** 28 | 29 | - Fix UDP fallback to TCP on large messages [\#46](https://github.com/riemann/riemann-ruby-client/pull/46) ([smortex](https://github.com/smortex)) 30 | 31 | **Merged pull requests:** 32 | 33 | - Modernize unit tests [\#45](https://github.com/riemann/riemann-ruby-client/pull/45) ([smortex](https://github.com/smortex)) 34 | - Switch from Bacon to RSpec [\#43](https://github.com/riemann/riemann-ruby-client/pull/43) ([smortex](https://github.com/smortex)) 35 | - Create codeql-analysis.yml [\#40](https://github.com/riemann/riemann-ruby-client/pull/40) ([jamtur01](https://github.com/jamtur01)) 36 | 37 | ## [v1.0.1](https://github.com/riemann/riemann-ruby-client/tree/v1.0.1) (2022-06-25) 38 | 39 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/v1.0.0...v1.0.1) 40 | 41 | **Merged pull requests:** 42 | 43 | - Setup Rubocop and lower required Ruby version [\#37](https://github.com/riemann/riemann-ruby-client/pull/37) ([smortex](https://github.com/smortex)) 44 | 45 | ## [v1.0.0](https://github.com/riemann/riemann-ruby-client/tree/v1.0.0) (2022-06-16) 46 | 47 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/0.2.6...v1.0.0) 48 | 49 | **Implemented enhancements:** 50 | 51 | - Add support for micro-seconds resolution [\#34](https://github.com/riemann/riemann-ruby-client/pull/34) ([smortex](https://github.com/smortex)) 52 | - Add support for TLS [\#33](https://github.com/riemann/riemann-ruby-client/pull/33) ([smortex](https://github.com/smortex)) 53 | - Add support for IPv6 addresses [\#30](https://github.com/riemann/riemann-ruby-client/pull/30) ([dch](https://github.com/dch)) 54 | 55 | **Merged pull requests:** 56 | 57 | - Fix race conditions in CI [\#35](https://github.com/riemann/riemann-ruby-client/pull/35) ([smortex](https://github.com/smortex)) 58 | - Modernize and setup CI [\#32](https://github.com/riemann/riemann-ruby-client/pull/32) ([smortex](https://github.com/smortex)) 59 | - Bump beefcake dependency [\#29](https://github.com/riemann/riemann-ruby-client/pull/29) ([dch](https://github.com/dch)) 60 | 61 | ## [0.2.6](https://github.com/riemann/riemann-ruby-client/tree/0.2.6) (2015-11-18) 62 | 63 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/0.2.5...0.2.6) 64 | 65 | **Merged pull requests:** 66 | 67 | - Client should yield self [\#22](https://github.com/riemann/riemann-ruby-client/pull/22) ([agile](https://github.com/agile)) 68 | - Allow TCP sockets to work on Windows [\#21](https://github.com/riemann/riemann-ruby-client/pull/21) ([sgran](https://github.com/sgran)) 69 | - README hash syntax fix [\#20](https://github.com/riemann/riemann-ruby-client/pull/20) ([squarism](https://github.com/squarism)) 70 | 71 | ## [0.2.5](https://github.com/riemann/riemann-ruby-client/tree/0.2.5) (2015-02-05) 72 | 73 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/0.2.4...0.2.5) 74 | 75 | ## [0.2.4](https://github.com/riemann/riemann-ruby-client/tree/0.2.4) (2015-02-03) 76 | 77 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/0.2.2...0.2.4) 78 | 79 | **Merged pull requests:** 80 | 81 | - Tightening beefcake requirement [\#19](https://github.com/riemann/riemann-ruby-client/pull/19) ([aphyr](https://github.com/aphyr)) 82 | - Fix for \#17, plus test and connection refactor [\#18](https://github.com/riemann/riemann-ruby-client/pull/18) ([RKelln](https://github.com/RKelln)) 83 | - Ensure that we close the connection if we got an error back [\#16](https://github.com/riemann/riemann-ruby-client/pull/16) ([eric](https://github.com/eric)) 84 | - String\#clear doesn't exist in 1.8 [\#15](https://github.com/riemann/riemann-ruby-client/pull/15) ([eric](https://github.com/eric)) 85 | - Tcp client with timeouts [\#14](https://github.com/riemann/riemann-ruby-client/pull/14) ([eric](https://github.com/eric)) 86 | 87 | ## [0.2.2](https://github.com/riemann/riemann-ruby-client/tree/0.2.2) (2013-05-28) 88 | 89 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/0.2.0...0.2.2) 90 | 91 | **Merged pull requests:** 92 | 93 | - Update README with timeout information [\#11](https://github.com/riemann/riemann-ruby-client/pull/11) ([gsandie](https://github.com/gsandie)) 94 | - Add tcp socket timeouts [\#10](https://github.com/riemann/riemann-ruby-client/pull/10) ([gsandie](https://github.com/gsandie)) 95 | - Socket can not be opened in method connected? [\#9](https://github.com/riemann/riemann-ruby-client/pull/9) ([vadv](https://github.com/vadv)) 96 | 97 | ## [0.2.0](https://github.com/riemann/riemann-ruby-client/tree/0.2.0) (2013-04-02) 98 | 99 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/version-0.0.7...0.2.0) 100 | 101 | **Merged pull requests:** 102 | 103 | - Get and set attributes using hash-style accessors [\#8](https://github.com/riemann/riemann-ruby-client/pull/8) ([jegt](https://github.com/jegt)) 104 | - Add extra attributes added to the Event constructor to the event as Attribute instances [\#7](https://github.com/riemann/riemann-ruby-client/pull/7) ([jegt](https://github.com/jegt)) 105 | - Change attribute name to attribute key [\#6](https://github.com/riemann/riemann-ruby-client/pull/6) ([b](https://github.com/b)) 106 | - Arbitrary attributes on events [\#5](https://github.com/riemann/riemann-ruby-client/pull/5) ([b](https://github.com/b)) 107 | 108 | ## [version-0.0.7](https://github.com/riemann/riemann-ruby-client/tree/version-0.0.7) (2012-04-16) 109 | 110 | [Full Changelog](https://github.com/riemann/riemann-ruby-client/compare/fe25a3b01681612defc39250006748069e06a172...version-0.0.7) 111 | 112 | **Merged pull requests:** 113 | 114 | - Add support for ruby 1.8 [\#1](https://github.com/riemann/riemann-ruby-client/pull/1) ([eric](https://github.com/eric)) 115 | 116 | 117 | 118 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* 119 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'github_changelog_generator' 8 | gem 'rspec' 9 | gem 'rubocop' 10 | gem 'rubocop-rspec' 11 | gem 'timecop' 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Kyle Kingsbury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Riemann Ruby Client 2 | 3 | [![CI](https://github.com/riemann/riemann-ruby-client/actions/workflows/ci.yml/badge.svg)](https://github.com/riemann/riemann-ruby-client/actions/workflows/ci.yml) 4 | 5 | ## Installing 6 | 7 | ```shell 8 | gem install riemann-client 9 | ``` 10 | 11 | ## Use 12 | 13 | ```ruby 14 | require 'riemann/client' 15 | 16 | # Create a client. Host, port and timeout are optional. 17 | c = Riemann::Client.new host: 'localhost', port: 5555, timeout: 5 18 | 19 | # Send a simple event 20 | c << {service: 'testing', metric: 2.5} 21 | 22 | # Or a more complex one 23 | c << { 24 | host: 'web3', 25 | service: 'api latency', 26 | state: 'warn', 27 | metric: 63.5, 28 | description: "63.5 milliseconds per request", 29 | time: Time.now.to_i - 10 30 | } 31 | 32 | # :host defaults to gethostname(). :time defaults to current unix time. You 33 | # can explicitly override host... 34 | 35 | c << {host: nil, service: 'the cloud', state: 'nebulous'} 36 | 37 | # Get all the states from the server 38 | c['true'] 39 | 40 | # Or specific states matching a query 41 | c['host =~ "%.dc1" and (state = "critical" or state = "warning")'] 42 | 43 | ``` 44 | 45 | ## Transports 46 | 47 | 48 | Riemann::Client sends small events over UDP by default, and uses TCP for 49 | queries and large events. UDP sends are essentially "shouting into the void". 50 | They will not block your application and are roughly an order of magnitude 51 | faster than TCP, but you will not know if the server is down or encountered an 52 | error. You can specify what transport to use by selecting a subclient: 53 | 54 | ``` ruby 55 | c.udp << { :state => "ok" } # => nil 56 | c.tcp << { :state => "ok" } # => # 57 | c.tcp["true"] # => [#, ...] 58 | c.udp["true"] # => raise Riemann::Client::Unsupported 59 | ``` 60 | 61 | ## Client state management 62 | 63 | Riemann::Client provides some classes to make managing state updates easier. 64 | 65 | Riemann::MetricThread starts a thread to poll a metric periodically, which can 66 | be used to flush an accumulated value to ustate at regular intervals. 67 | 68 | Riemann::AutoState bundles a state and a client together. Any changes to the 69 | AutoState automatically send the new state to the client. 70 | 71 | ## License 72 | 73 | The MIT License 74 | 75 | Copyright (c) 2011-2024 Kyle Kingsbury 76 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'riemann' 4 | 5 | require 'bundler/gem_tasks' 6 | 7 | require 'github_changelog_generator/task' 8 | 9 | GitHubChangelogGenerator::RakeTask.new :changelog do |config| 10 | config.user = 'riemann' 11 | config.project = 'riemann-ruby-client' 12 | config.exclude_labels = ['skip-changelog'] 13 | config.future_release = "v#{Riemann::VERSION}" 14 | config.add_issues_wo_labels = false 15 | end 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Riemann Security and Disclosure Information 2 | This page describes Riemann security and disclosure information. 3 | 4 | ## Supported Versions 5 | 6 | The currently supported version of Riemann for security-patching purposes is always the latest version. 7 | 8 | ## Security Announcements 9 | 10 | Will be made on the [Riemann mailing list](https://groups.google.com/g/riemann-users?pli=1). 11 | 12 | ## Report a Vulnerability 13 | 14 | We're extremely grateful for security researchers and users that report vulnerabilities to Riemann. All reports are thoroughly investigated by the maintainers. 15 | 16 | To make a report, you should email the private security@riemann.io list with the details. 17 | 18 | ## When Should I Report a Vulnerability? 19 | 20 | * You think you discovered a potential security vulnerability in Riemann. 21 | * You are unsure how a vulnerability affects Riemann. 22 | * You think you discovered a vulnerability in another project that Riemann depends on 23 | 24 | For projects with their own vulnerability reporting and disclosure process, please report it directly there. 25 | 26 | ## When Should I NOT Report a Vulnerability? 27 | 28 | * You need help tuning Riemann components for security 29 | * You need help applying security related updates 30 | * Your issue is not security related 31 | 32 | ## Security Vulnerability Response 33 | 34 | Each report is acknowledged and analyzed within 5 working days. 35 | 36 | Any vulnerability information shared stays within Riemann project and will not be disseminated to other projects unless it is necessary to get the issue fixed. 37 | 38 | As the security issue moves from triage, to identified fix, to release planning we will keep the reporter updated. 39 | 40 | ## Public Disclosure Timing 41 | 42 | A public disclosure date is negotiated by the Riemann maintainers nd the bug submitter. We prefer to fully disclose the bug as soon as possible once a user mitigation is available. It is reasonable to delay disclosure when the bug or the fix is not yet fully understood, the solution is not well-tested, or for vendor coordination. The timeframe for disclosure is from immediate (especially if it's already publicly known) to a few weeks. For a vulnerability with a straightforward mitigation, we expect report date to disclosure date to be on the order of 7 days. The Riemann maintainers hold the final say when setting a disclosure date. 43 | -------------------------------------------------------------------------------- /lib/riemann.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | require 'beefcake' 5 | require 'timeout' 6 | require 'riemann/version' 7 | require 'riemann/state' 8 | require 'riemann/attribute' 9 | require 'riemann/event' 10 | require 'riemann/query' 11 | require 'riemann/message' 12 | end 13 | -------------------------------------------------------------------------------- /lib/riemann/attribute.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class Attribute 5 | include Beefcake::Message 6 | 7 | required :key, :string, 1 8 | optional :value, :string, 2 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/riemann/auto_state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class AutoState 5 | # Binds together a state hash and a Client. Any change made here 6 | # sends the state to the client. Useful when updates to a state are made 7 | # decoherently, e.g. across many methods. Combine with MetricThread (or 8 | # just Thread.new { loop { autostate.flush; sleep n } }) to ensure regular 9 | # updates. 10 | # 11 | # example: 12 | # 13 | # class Job 14 | # def initialize 15 | # @state = AutoState.new 16 | # @state.service = 'job' 17 | # @state.state = 'starting up' 18 | # 19 | # run 20 | # end 21 | # 22 | # def run 23 | # loop do 24 | # begin 25 | # a 26 | # b 27 | # rescue Exception => e 28 | # @state.once( 29 | # state: 'error', 30 | # description: e.to_s 31 | # ) 32 | # end 33 | # end 34 | # end 35 | # 36 | # def a 37 | # @state.state = 'heavy lifting a' 38 | # ... 39 | # end 40 | # 41 | # def b 42 | # @state.state = 'heavy lifting b' 43 | # ... 44 | # end 45 | 46 | def initialize(client = Client.new, state = {}) 47 | @client = client 48 | @state = state 49 | end 50 | 51 | def description=(description) 52 | @state[:description] = description 53 | flush 54 | end 55 | 56 | def description 57 | @state[:description] 58 | end 59 | 60 | # Send state to client 61 | def flush 62 | @state[:time] = Time.now.to_i 63 | @client << @state 64 | end 65 | 66 | def host=(host) 67 | @state[:host] = host 68 | flush 69 | end 70 | 71 | def host 72 | @state[:host] 73 | end 74 | 75 | def metric=(metric) 76 | @state[:metric] = metric 77 | flush 78 | end 79 | alias metric_f= metric= 80 | 81 | def metric 82 | @state[:metric] 83 | end 84 | alias metric_f metric 85 | 86 | # Performs multiple updates, followed by flush. 87 | # Example: merge state: critical, metric_f: 10235.3 88 | def merge(opts) 89 | @state.merge! opts 90 | flush 91 | end 92 | alias << merge 93 | 94 | # Issues an immediate update of the state with tag "once" 95 | # set, but does not update the local state. Useful for transient errors. 96 | # Opts are merged with the state. 97 | def once(opts) 98 | o = @state.merge opts 99 | o[:time] = Time.now.to_i 100 | o[:tags] = begin 101 | (o[:tags] | ['once']) 102 | rescue StandardError 103 | ['once'] 104 | end 105 | @client << o 106 | end 107 | 108 | def state=(state) 109 | @state[:state] = state 110 | flush 111 | end 112 | 113 | def state 114 | @state[:state] 115 | end 116 | 117 | def service=(service) 118 | @state[:service] = service 119 | flush 120 | end 121 | 122 | def service 123 | @state[:service] 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/riemann/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'riemann' 4 | 5 | module Riemann 6 | class Client 7 | class Error < RuntimeError; end 8 | class InvalidResponse < Error; end 9 | class ServerError < Error; end 10 | class Unsupported < Error; end 11 | class TooBig < Unsupported; end 12 | 13 | require 'socket' 14 | require 'time' 15 | 16 | HOST = '127.0.0.1' 17 | PORT = 5555 18 | TIMEOUT = 5 19 | 20 | require 'riemann/client/tcp' 21 | require 'riemann/client/udp' 22 | 23 | attr_reader :tcp, :udp 24 | 25 | def initialize(opts = {}) 26 | @options = opts.dup 27 | @options[:host] ||= HOST 28 | @options[:port] ||= PORT 29 | @options[:timeout] ||= TIMEOUT 30 | 31 | @udp = UDP.new(@options) 32 | @tcp = TCP.new(@options) 33 | return unless block_given? 34 | 35 | begin 36 | yield self 37 | ensure 38 | close 39 | end 40 | end 41 | 42 | def host 43 | @options[:host] 44 | end 45 | 46 | def port 47 | @options[:port] 48 | end 49 | 50 | def timeout 51 | @options[:timeout] 52 | end 53 | 54 | # Send a state 55 | def <<(event) 56 | # Create state 57 | case event 58 | when Riemann::State, Riemann::Event, Hash 59 | # Noop 60 | else 61 | raise(ArgumentError, "Unsupported event class: #{event.class.name}") 62 | end 63 | 64 | bulk_send([event]) 65 | end 66 | 67 | def bulk_send(events) 68 | raise ArgumentError unless events.is_a?(Array) 69 | 70 | message = Riemann::Message.new(events: normalize_events(events)) 71 | 72 | send_maybe_recv(message) 73 | end 74 | 75 | def normalize_events(events) 76 | events.map do |event| 77 | case event 78 | when Riemann::State, Riemann::Event 79 | event 80 | when Hash 81 | e = if event.include?(:host) 82 | event 83 | else 84 | event.dup.merge(host: Socket.gethostname) 85 | end 86 | Riemann::Event.new(e) 87 | else 88 | raise(ArgumentError, "Unsupported event class: #{event.class.name}") 89 | end 90 | end 91 | end 92 | 93 | # Returns an array of states matching query. 94 | def [](query) 95 | response = query(query) 96 | (response.events || []) | 97 | (response.states || []) 98 | end 99 | 100 | def connect 101 | # NOTE: connections are made automatically on send 102 | warn 'Riemann client#connect is deprecated' 103 | end 104 | 105 | # Close both UDP and TCP sockets. 106 | def close 107 | @udp.close 108 | @tcp.close 109 | end 110 | 111 | def connected? 112 | tcp.connected? and udp.connected? 113 | end 114 | 115 | # Ask for states 116 | def query(string = 'true') 117 | send_recv Riemann::Message.new(query: Riemann::Query.new(string: string)) 118 | end 119 | 120 | def send_recv(message) 121 | @tcp.send_recv(message) 122 | end 123 | 124 | def send_maybe_recv(message) 125 | @udp.send_maybe_recv(message) 126 | rescue TooBig 127 | @tcp.send_maybe_recv(message) 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/riemann/client/ssl_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'openssl' 4 | require_relative 'tcp_socket' 5 | 6 | module Riemann 7 | class Client 8 | # Socket: A specialized socket that has been configure 9 | class SSLSocket < TcpSocket 10 | def initialize(options = {}) 11 | super(options) 12 | @key_file = options[:key_file] 13 | @cert_file = options[:cert_file] 14 | @ca_file = options[:ca_file] 15 | @ssl_verify = options[:ssl_verify] 16 | end 17 | 18 | def ssl_context 19 | @ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |ctx| 20 | ctx.key = OpenSSL::PKey::RSA.new(File.read(@key_file)) 21 | ctx.cert = OpenSSL::X509::Certificate.new(File.read(@cert_file)) 22 | ctx.ca_file = @ca_file if @ca_file 23 | ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION 24 | ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER if @ssl_verify 25 | end 26 | end 27 | 28 | # Internal: Connect to the give address within the timeout. 29 | # 30 | # Make an attempt to connect to a single address within the given timeout. 31 | # 32 | # Return the ::Socket when it is connected, or raise an Error if no 33 | # connection was possible. 34 | def connect_nonblock(addr, timeout) 35 | sock = super(addr, timeout) 36 | ssl_socket = OpenSSL::SSL::SSLSocket.new(sock, ssl_context) 37 | ssl_socket.sync = true 38 | 39 | begin 40 | ssl_socket.connect_nonblock 41 | rescue IO::WaitReadable 42 | unless IO.select([ssl_socket], nil, nil, timeout) 43 | raise Timeout, "Could not read from #{host}:#{port} in #{timeout} seconds" 44 | end 45 | 46 | retry 47 | rescue IO::WaitWritable 48 | unless IO.select(nil, [ssl_socket], nil, timeout) 49 | raise Timeout, "Could not write to #{host}:#{port} in #{timeout} seconds" 50 | end 51 | 52 | retry 53 | end 54 | ssl_socket 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/riemann/client/tcp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'monitor' 4 | require 'riemann/client/tcp_socket' 5 | require 'riemann/client/ssl_socket' 6 | 7 | module Riemann 8 | class Client 9 | class TCP < Client 10 | attr_accessor :host, :port 11 | 12 | # Public: Set a socket factory -- an object responding 13 | # to #call(options) that returns a Socket object 14 | class << self 15 | attr_writer :socket_factory 16 | end 17 | 18 | # Public: Return a socket factory 19 | def self.socket_factory 20 | @socket_factory ||= proc { |options| 21 | if options[:ssl] 22 | SSLSocket.connect(options) 23 | else 24 | TcpSocket.connect(options) 25 | end 26 | } 27 | end 28 | 29 | def initialize(options = {}) # rubocop:disable Lint/MissingSuper 30 | @options = options 31 | @locket = Monitor.new 32 | @socket = nil 33 | @pid = nil 34 | end 35 | 36 | def socket 37 | @locket.synchronize do 38 | close if @pid && @pid != Process.pid 39 | 40 | return @socket if connected? 41 | 42 | @socket = self.class.socket_factory.call(@options) 43 | @pid = Process.pid 44 | 45 | return @socket 46 | end 47 | end 48 | 49 | def close 50 | @locket.synchronize do 51 | @socket.close if connected? 52 | @socket = nil 53 | end 54 | end 55 | 56 | def connected? 57 | @locket.synchronize do 58 | !@socket.nil? && !@socket.closed? 59 | end 60 | end 61 | 62 | # Read a message from a stream 63 | def read_message(socket) 64 | unless (buffer = socket.read(4)) && (buffer.size == 4) 65 | raise InvalidResponse, 'unexpected EOF' 66 | end 67 | 68 | length = buffer.unpack1('N') 69 | begin 70 | str = socket.read length 71 | message = Riemann::Message.decode str 72 | rescue StandardError 73 | puts "Message was #{str.inspect}" 74 | raise 75 | end 76 | 77 | unless message.ok 78 | puts 'Failed' 79 | raise ServerError, message.error 80 | end 81 | 82 | message 83 | end 84 | 85 | def send_recv(message) 86 | with_connection do |socket| 87 | socket.write(message.encode_with_length) 88 | read_message(socket) 89 | end 90 | end 91 | 92 | alias send_maybe_recv send_recv 93 | 94 | # Yields a connection in the block. 95 | def with_connection 96 | tries = 0 97 | 98 | @locket.synchronize do 99 | tries += 1 100 | yield(socket) 101 | rescue IOError, Errno::EPIPE, Errno::ECONNREFUSED, InvalidResponse, Timeout::Error, 102 | Riemann::Client::TcpSocket::Error 103 | close 104 | raise if tries > 3 105 | 106 | retry 107 | rescue StandardError 108 | close 109 | raise 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/riemann/client/tcp_socket.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'socket' 4 | require 'fcntl' 5 | 6 | module Riemann 7 | class Client 8 | # Socket: A specialized socket that has been configure 9 | class TcpSocket 10 | class Error < Riemann::Client::Error; end 11 | class Timeout < Error; end 12 | 13 | # Internal: 14 | # The timeout for reading in seconds. Defaults to 2 15 | attr_accessor :read_timeout 16 | 17 | # Internal: 18 | # The timeout for connecting in seconds. Defaults to 2 19 | attr_reader :connect_timeout 20 | 21 | # Internal: 22 | # The timeout for writing in seconds. Defaults to 2 23 | attr_reader :write_timeout 24 | 25 | # Internal: 26 | # The host this socket is connected to 27 | attr_reader :host 28 | 29 | # Internal: 30 | # The port this socket is connected to 31 | attr_reader :port 32 | 33 | # Internal 34 | # 35 | # Used for setting TCP_KEEPIDLE: overrides tcp_keepalive_time for a single 36 | # socket. 37 | # 38 | # http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html 39 | # 40 | # tcp_keepalive_time: 41 | # 42 | # The interval between the last data packet sent (simple ACKs are not 43 | # considered data) and the first keepalive probe; after the connection is 44 | # marked to need keepalive, this counter is not used any further. 45 | attr_reader :keepalive_idle 46 | 47 | # Internal 48 | # 49 | # Used for setting TCP_KEEPINTVL: overrides tcp_keepalive_intvl for a single 50 | # socket. 51 | # 52 | # http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html 53 | # 54 | # tcp_keepalive_intvl: 55 | # 56 | # The interval between subsequential keepalive probes, regardless of what 57 | # the connection has exchanged in the meantime. 58 | attr_reader :keepalive_interval 59 | 60 | # Internal 61 | # 62 | # Used for setting TCP_KEEPCNT: overrides tcp_keepalive_probes for a single 63 | # socket. 64 | # 65 | # http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html 66 | # 67 | # tcp_keepalive_probes: 68 | # 69 | # The number of unacknowledged probes to send before considering the 70 | # connection dead and notifying the application layer. 71 | attr_reader :keepalive_count 72 | 73 | # Internal: Create and connect to the given location. 74 | # 75 | # options, same as Constructor 76 | # 77 | # Returns an instance of KJess::Socket 78 | def self.connect(options = {}) 79 | s = new(options) 80 | s.connect 81 | s 82 | end 83 | 84 | # Internal: Creates a new KJess::Socket 85 | def initialize(options = {}) 86 | @host = options[:host] 87 | @port = options[:port] 88 | 89 | @connect_timeout = options[:connect_timeout] || options[:timeout] || 2 90 | @read_timeout = options[:read_timeout] || options[:timeout] || 2 91 | @write_timeout = options[:write_timeout] || options[:timeout] || 2 92 | 93 | @keepalive_active = options.fetch(:keepalive_active, true) 94 | @keepalive_idle = options[:keepalive_idle] || 60 95 | @keepalive_interval = options[:keepalive_interval] || 30 96 | @keepalive_count = options[:keepalive_count] || 5 97 | 98 | @socket = nil 99 | end 100 | 101 | # Internal: Return whether or not the keepalive_active flag is set. 102 | def keepalive_active? 103 | @keepalive_active 104 | end 105 | 106 | # Internal: Low level socket allocation and option configuration 107 | # 108 | # Using the options from the initializer, a new ::Socket is created that 109 | # is: 110 | # 111 | # TCP, autoclosing on exit, nagle's algorithm is disabled and has 112 | # TCP Keepalive options set if keepalive is supported. 113 | # 114 | # Returns a new ::Socket instance for 115 | 116 | def socket_factory(type) 117 | sock = ::Socket.new(type, ::Socket::SOCK_STREAM, 0) 118 | 119 | # close file descriptors if we exec 120 | if Fcntl.constants.include?(:F_SETFD) && Fcntl.constants.include?(:FD_CLOEXEC) 121 | sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) 122 | end 123 | # Disable Nagle's algorithm 124 | sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) 125 | 126 | if using_keepalive? 127 | sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) 128 | sock.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPIDLE, keepalive_idle) 129 | sock.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPINTVL, keepalive_interval) 130 | sock.setsockopt(::Socket::SOL_TCP, ::Socket::TCP_KEEPCNT, keepalive_count) 131 | end 132 | 133 | sock 134 | end 135 | 136 | # Internal: Return the connected raw Socket. 137 | # 138 | # If the socket is closed or non-existent it will create and connect again. 139 | # 140 | # Returns a ::Socket 141 | def socket 142 | return @socket unless closed? 143 | 144 | @socket ||= connect 145 | end 146 | 147 | # Internal: Closes the internal ::Socket 148 | # 149 | # Returns nothing 150 | def close 151 | @socket.close unless closed? 152 | @socket = nil 153 | end 154 | 155 | # Internal: Return true the socket is closed. 156 | def closed? 157 | return true if @socket.nil? 158 | return true if @socket.closed? 159 | 160 | false 161 | end 162 | 163 | # Internal: 164 | # 165 | # Connect to the remote host in a non-blocking fashion. 166 | # 167 | # Raise Error if there is a failure connecting. 168 | # 169 | # Return the ::Socket on success 170 | def connect 171 | # Calculate our timeout deadline 172 | deadline = Time.now.to_f + connect_timeout 173 | 174 | # Lookup destination address, we only want TCP. 175 | addrs = ::Socket.getaddrinfo(host, port, nil, ::Socket::SOCK_STREAM) 176 | errors = [] 177 | conn_error = -> { raise errors.first } 178 | sock = nil 179 | 180 | # Sort it so we get AF_INET, IPv4 181 | addrs.sort.find(conn_error) do |addr| 182 | sock = connect_or_error(addr, deadline, errors) 183 | end 184 | sock 185 | end 186 | 187 | # Internal: Connect to the destination or raise an error. 188 | # 189 | # Connect to the address or capture the error of the connection 190 | # 191 | # addr - An address returned from Socket.getaddrinfo() 192 | # deadline - the after which we should raise a timeout error 193 | # errors - a collection of errors to append an error too should we have one. 194 | # 195 | # Make an attempt to connect to the given address. If it is successful, 196 | # return the socket. 197 | # 198 | # Should the connection fail, append the exception to the errors array and 199 | # return false. 200 | # 201 | def connect_or_error(addr, deadline, errors) 202 | timeout = deadline - Time.now.to_f 203 | raise Timeout, "Could not connect to #{host}:#{port}" if timeout <= 0 204 | 205 | connect_nonblock(addr, timeout) 206 | rescue Error => e 207 | errors << e 208 | false 209 | end 210 | 211 | # Internal: Connect to the give address within the timeout. 212 | # 213 | # Make an attempt to connect to a single address within the given timeout. 214 | # 215 | # Return the ::Socket when it is connected, or raise an Error if no 216 | # connection was possible. 217 | def connect_nonblock(addr, timeout) 218 | sockaddr = ::Socket.pack_sockaddr_in(addr[1], addr[3]) 219 | sock = socket_factory(addr[4]) 220 | sock.connect_nonblock(sockaddr) 221 | sock 222 | rescue Errno::EINPROGRESS 223 | if IO.select(nil, [sock], nil, timeout).nil? 224 | begin 225 | sock.close 226 | rescue StandardError 227 | nil 228 | end 229 | raise Timeout, "Could not connect to #{host}:#{port} within #{timeout} seconds" 230 | end 231 | connect_nonblock_finalize(sock, sockaddr) 232 | rescue StandardError => e 233 | begin 234 | sock.close 235 | rescue StandardError 236 | nil 237 | end 238 | raise Error, "Could not connect to #{host}:#{port}: #{e.class}: #{e.message}", e.backtrace 239 | end 240 | 241 | # Internal: Make sure that a non-blocking connect has truely connected. 242 | # 243 | # Ensure that the given socket is actually connected to the given adddress. 244 | # 245 | # Returning the socket if it is and raising an Error if it isn't. 246 | def connect_nonblock_finalize(sock, sockaddr) 247 | sock.connect_nonblock(sockaddr) 248 | sock 249 | rescue Errno::EISCONN 250 | sock 251 | rescue StandardError => e 252 | begin 253 | sock.close 254 | rescue StandardError 255 | nil 256 | end 257 | raise Error, "Could not connect to #{host}:#{port}: #{e.class}: #{e.message}", e.backtrace 258 | end 259 | 260 | # Internal: say if we are using TCP Keep Alive or not 261 | # 262 | # We will return true if the initialization options :keepalive_active is 263 | # set to true, and if all the constants that are necessary to use TCP keep 264 | # alive are defined. 265 | # 266 | # It may be the case that on some operating systems that the constants are 267 | # not defined, so in that case we do not want to attempt to use tcp keep 268 | # alive if we are unable to do so in any case. 269 | # 270 | # Returns true or false 271 | def using_keepalive? 272 | using = false 273 | if keepalive_active? 274 | using = %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? do |c| 275 | ::Socket.const_defined? c 276 | end 277 | end 278 | using 279 | end 280 | 281 | # Reads length bytes from the socket 282 | # 283 | # length - the number of bytes to read from the socket 284 | # outbuf - an optional buffer to store the bytes in 285 | # 286 | # Returns the bytes read if no outbuf is specified 287 | def read(length, outbuf = nil) 288 | if outbuf 289 | outbuf.replace('') 290 | buf = outbuf 291 | else 292 | buf = String.new 293 | end 294 | 295 | while buf.length < length 296 | unless (rb = readpartial(length - buf.length)) 297 | break 298 | end 299 | 300 | buf << rb 301 | end 302 | 303 | buf 304 | end 305 | 306 | # Internal: Read up to a maxlen of data from the socket and store it in outbuf 307 | # 308 | # maxlen - the maximum number of bytes to read from the socket 309 | # outbuf - the buffer in which to store the bytes. 310 | # 311 | # Returns the bytes read 312 | def readpartial(maxlen, outbuf = nil) 313 | socket.read_nonblock(maxlen, outbuf) 314 | rescue Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNRESET, IO::WaitReadable 315 | unless wait_readable(read_timeout) 316 | raise Timeout, "Could not read from #{host}:#{port} in #{read_timeout} seconds" 317 | end 318 | 319 | retry 320 | end 321 | 322 | # Internal: Write the given data to the socket 323 | # 324 | # buf - the data to write to the socket. 325 | # 326 | # Raises an error if it is unable to write the data to the socket within the 327 | # write_timeout. 328 | # 329 | # returns nothing 330 | def write(buf) 331 | until buf.nil? || buf.empty? 332 | written = socket.write_nonblock(buf) 333 | buf = buf[written, buf.length] 334 | end 335 | rescue Errno::EWOULDBLOCK, Errno::EINTR, Errno::EAGAIN, Errno::ECONNRESET, IO::WaitWritable 336 | unless wait_writable(write_timeout) 337 | raise Timeout, "Could not write to #{host}:#{port} in #{write_timeout} seconds" 338 | end 339 | 340 | retry 341 | rescue IO::WaitReadable 342 | # Also rescued for SSL renegotiation in OpenSSL::SSL::SSLSocket according to 343 | # https://ruby-doc.org/core-2.7.1/IO.html#method-c-select 344 | unless wait_readable(read_timeout) 345 | raise Timeout, "Could not write to #{host}:#{port} in #{write_timeout} seconds" 346 | end 347 | 348 | retry 349 | end 350 | 351 | def wait_writable(timeout = nil) 352 | IO.select(nil, [@socket], nil, timeout || write_timeout) 353 | end 354 | 355 | def wait_readable(timeout = nil) 356 | IO.select([@socket], nil, nil, timeout || read_timeout) 357 | end 358 | end 359 | end 360 | end 361 | -------------------------------------------------------------------------------- /lib/riemann/client/udp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class Client 5 | class UDP < Client 6 | MAX_SIZE = 16_384 7 | 8 | attr_accessor :host, :port, :max_size 9 | 10 | def initialize(opts = {}) # rubocop:disable Lint/MissingSuper 11 | @host = opts[:host] || HOST 12 | @port = opts[:port] || PORT 13 | @max_size = opts[:max_size] || MAX_SIZE 14 | @socket = nil 15 | end 16 | 17 | def socket 18 | return @socket if connected? 19 | 20 | @socket = UDPSocket.new 21 | end 22 | 23 | def close 24 | @socket.close if connected? 25 | @socket = nil 26 | end 27 | 28 | def connected? 29 | @socket && !@socket.closed? 30 | end 31 | 32 | # Read a message from a stream 33 | def read_message(_socket) 34 | raise Unsupported 35 | end 36 | 37 | def send_recv(_message) 38 | raise Unsupported 39 | end 40 | 41 | def send_maybe_recv(message) 42 | encoded_string = message.encode.to_s 43 | raise TooBig unless encoded_string.length < @max_size 44 | 45 | socket.send(encoded_string, 0, @host, @port) 46 | nil 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/riemann/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class Event 5 | require 'set' 6 | include Beefcake::Message 7 | 8 | optional :time, :int64, 1 9 | optional :state, :string, 2 10 | optional :service, :string, 3 11 | optional :host, :string, 4 12 | optional :description, :string, 5 13 | repeated :tags, :string, 7 14 | optional :ttl, :float, 8 15 | repeated :attributes, Attribute, 9 16 | optional :time_micros, :int64, 10 17 | 18 | optional :metric_sint64, :sint64, 13 19 | optional :metric_d, :double, 14 20 | optional :metric_f, :float, 15 21 | 22 | # Fields which don't really exist in protobufs, but which are reserved 23 | # and can't be used as attributes. 24 | VIRTUAL_FIELDS = Set.new([:metric]) 25 | # Fields which are specially encoded in the Event protobuf--that is, they 26 | # can't be used as attributes. 27 | RESERVED_FIELDS = fields.map do |_i, field| 28 | field.name.to_sym 29 | end.reduce(VIRTUAL_FIELDS) do |set, field| # rubocop:disable Style/MultilineBlockChain 30 | set << field 31 | end 32 | 33 | def self.now 34 | (Time.now.to_f * 1_000_000).to_i 35 | end 36 | 37 | # Average a set of states together. Chooses the mean metric, the mode 38 | # state, mode service, and the mean time. If init is provided, its values 39 | # override (where present) the computed ones. 40 | def self.average(states, init = Event.new) 41 | init = case init 42 | when Event 43 | init.dup 44 | else 45 | Event.new init 46 | end 47 | 48 | # Metric 49 | init.metric_f ||= states.inject(0.0) do |a, state| 50 | a + (state.metric || 0) 51 | end / states.size 52 | init.metric_f = 0.0 if init.metric_f.nan? 53 | 54 | # Event 55 | init.state ||= mode states.map(&:state) 56 | init.service ||= mode states.map(&:service) 57 | 58 | # Time 59 | init.time_micros = begin 60 | times = states.map(&:time_micros).compact 61 | (times.inject(:+) / times.size).to_i 62 | rescue ZeroDivisionError 63 | nil 64 | end 65 | init.time_micros ||= now 66 | 67 | init 68 | end 69 | 70 | # Sum a set of states together. Adds metrics, takes the mode state, mode 71 | # service and the mean time. If init is provided, its values override 72 | # (where present) the computed ones. 73 | def self.sum(states, init = Event.new) 74 | init = case init 75 | when Event 76 | init.dup 77 | else 78 | Event.new init 79 | end 80 | 81 | # Metric 82 | init.metric_f ||= states.inject(0.0) do |a, state| 83 | a + (state.metric || 0) 84 | end 85 | init.metric_f = 0.0 if init.metric_f.nan? 86 | 87 | # Event 88 | init.state ||= mode states.map(&:state) 89 | init.service ||= mode states.map(&:service) 90 | 91 | # Time 92 | init.time_micros = begin 93 | times = states.map(&:time_micros).compact 94 | (times.inject(:+) / times.size).to_i 95 | rescue ZeroDivisionError 96 | nil 97 | end 98 | init.time_micros ||= now 99 | 100 | init 101 | end 102 | 103 | # Finds the maximum of a set of states. Metric is the maximum. Event is the 104 | # highest, as defined by Dash.config.state_order. Time is the mean. 105 | def self.max(states, init = Event.new) 106 | init = case init 107 | when Event 108 | init.dup 109 | else 110 | Event.new init 111 | end 112 | 113 | # Metric 114 | init.metric_f ||= states.inject(0.0) do |a, state| 115 | a + (state.metric || 0) 116 | end 117 | init.metric = 0.0 if init.metric.nan? 118 | 119 | # Event 120 | init.state ||= states.inject(nil) do |max, state| 121 | state.state if Dash.config[:state_order][state.state] > Dash.config[:state_order][max] 122 | end 123 | 124 | # Time 125 | init.time_micros = begin 126 | times = states.map(&:time_micros).compact 127 | (times.inject(:+) / times.size).to_i 128 | rescue ZeroDivisionError 129 | nil 130 | end 131 | init.time_micros ||= now 132 | 133 | init 134 | end 135 | 136 | def self.mode(array) 137 | array.each_with_object(Hash.new(0)) do |e, counts| 138 | counts[e] += 1 139 | end.max_by { |_e, count| count }.first # rubocop:disable Style/MultilineBlockChain 140 | rescue StandardError 141 | nil 142 | end 143 | 144 | # Partition a list of states by a field 145 | # Returns a hash of field_value => state 146 | def self.partition(states, field) 147 | states.each_with_object({}) do |state, p| 148 | k = state.send field 149 | if p.include? k 150 | p[k] << state 151 | else 152 | p[k] = [state] 153 | end 154 | end 155 | end 156 | 157 | # Sorts states by a field. nil values first. 158 | def self.sort(states, field) 159 | states.sort do |a, b| 160 | a = a.send field 161 | b = b.send field 162 | if a.nil? 163 | -1 164 | elsif b.nil? 165 | 1 166 | else 167 | a <=> b 168 | end 169 | end 170 | end 171 | 172 | def initialize(hash = nil) 173 | if hash 174 | super hash 175 | self.metric = hash[:metric] if hash[:metric] 176 | 177 | # Add extra attributes to the event as Attribute instances with values 178 | # converted to String 179 | self.attributes = hash.map do |key, _value| 180 | unless RESERVED_FIELDS.include? key.to_sym 181 | Attribute.new(key: key.to_s, 182 | value: (hash[key] || hash[key.to_sym]).to_s) 183 | end 184 | end.compact 185 | else 186 | super() 187 | end 188 | 189 | @time_micros ||= self.class.now unless @time 190 | end 191 | 192 | def metric 193 | metric_d || 194 | metric_sint64 || 195 | metric_f 196 | end 197 | 198 | def metric=(value) 199 | if value.is_a?(Integer) && (-(2**63)...2**63).include?(value) 200 | # Long 201 | self.metric_sint64 = value 202 | else 203 | self.metric_d = value.to_f 204 | end 205 | self.metric_f = value.to_f 206 | end 207 | 208 | # Look up attributes 209 | def [](key) 210 | if RESERVED_FIELDS.include? key.to_sym 211 | super 212 | else 213 | attributes.find { |a| a.key.to_s == key.to_s }.value 214 | end 215 | end 216 | 217 | # Set attributes 218 | def []=(key, value) 219 | if RESERVED_FIELDS.include? key.to_sym 220 | super 221 | else 222 | attr = attributes.find { |a| a.key == key.to_s } 223 | if attr 224 | attr.value = value.to_s 225 | else 226 | attributes << Attribute.new(key: key.to_s, value: value.to_s) 227 | end 228 | end 229 | end 230 | end 231 | end 232 | -------------------------------------------------------------------------------- /lib/riemann/message.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class Message 5 | include Beefcake::Message 6 | 7 | optional :ok, :bool, 2 8 | optional :error, :string, 3 9 | repeated :states, State, 4 10 | optional :query, Query, 5 11 | repeated :events, Event, 6 12 | 13 | def encode_with_length 14 | encoded_string = encode.to_s 15 | [encoded_string.length].pack('N') << encoded_string 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/riemann/metric_thread.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class MetricThread 5 | # A metric thread is simple: it wraps some metric object which responds to <<, 6 | # and every interval seconds, calls #flush which replaces the object and calls 7 | # a user specified function. 8 | 9 | INTERVAL = 10 10 | 11 | attr_accessor :interval, :metric 12 | 13 | # client = Riemann::Client.new 14 | # m = MetricThread.new Mtrc::Rate do |rate| 15 | # client << rate 16 | # end 17 | # 18 | # loop do 19 | # sleep rand 20 | # m << rand 21 | # end 22 | def initialize(klass, *klass_args, &block) 23 | @klass = klass 24 | @klass_args = klass_args 25 | @block = block 26 | @interval = INTERVAL 27 | 28 | @metric = new_metric 29 | 30 | start 31 | end 32 | 33 | def <<(value) 34 | @metric.<<(value) 35 | end 36 | 37 | def new_metric 38 | @klass.new(*@klass_args) 39 | end 40 | 41 | def flush 42 | old = @metric 43 | @metric = new_metric 44 | @block[old] 45 | end 46 | 47 | def start 48 | raise 'already running' if @runner 49 | 50 | @running = true 51 | @runner = Thread.new do 52 | while @running 53 | sleep @interval 54 | begin 55 | flush 56 | rescue StandardError 57 | # ignore 58 | end 59 | end 60 | @runner = nil 61 | end 62 | end 63 | 64 | def stop 65 | stop! 66 | @runner.join 67 | end 68 | 69 | def stop! 70 | @running = false 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/riemann/query.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class Query 5 | include Beefcake::Message 6 | 7 | optional :string, :string, 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/riemann/state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | class State 5 | include Beefcake::Message 6 | 7 | optional :time, :int64, 1 8 | optional :state, :string, 2 9 | optional :service, :string, 3 10 | optional :host, :string, 4 11 | optional :description, :string, 5 12 | optional :once, :bool, 6 13 | repeated :tags, :string, 7 14 | optional :ttl, :float, 8 15 | optional :metric_f, :float, 15 16 | 17 | def initialize 18 | super 19 | 20 | @time ||= Time.now.to_i 21 | end 22 | 23 | def metric 24 | @metric || metric_f 25 | end 26 | 27 | attr_writer :metric 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/riemann/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Riemann 4 | VERSION = '1.2.1' 5 | end 6 | -------------------------------------------------------------------------------- /riemann-client.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/riemann/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'riemann-client' 7 | spec.version = Riemann::VERSION 8 | spec.author = 'Kyle Kingsbury' 9 | spec.email = 'aphyr@aphyr.com' 10 | spec.summary = 'Client for the distributed event system Riemann.' 11 | spec.homepage = 'https://github.com/aphyr/riemann-ruby-client' 12 | spec.license = 'MIT' 13 | spec.platform = Gem::Platform::RUBY 14 | 15 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | 19 | spec.required_ruby_version = '>= 2.6.0' 20 | 21 | spec.add_dependency 'beefcake', '>= 1.0.0 ' 22 | spec.add_dependency 'mtrc', '>= 0.0.4' 23 | end 24 | -------------------------------------------------------------------------------- /spec/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'riemann' 4 | require 'riemann/client' 5 | 6 | require 'spec_helper' 7 | require 'shared_examples' 8 | 9 | RSpec.describe 'Riemann::Client' do 10 | let(:client) do 11 | Riemann::Client.new(host: 'localhost', port: 5555) 12 | end 13 | 14 | let(:expected_rate) { 100 } 15 | 16 | context('with TLS transport') do 17 | let(:client) do 18 | Riemann::Client.new(host: 'localhost', port: 5554, ssl: true, 19 | key_file: '/etc/riemann/riemann_server.pkcs8', 20 | cert_file: '/etc/riemann/riemann_server.crt', 21 | ca_file: '/etc/riemann/riemann_server.crt', 22 | ssl_verify: true) 23 | end 24 | let(:client_with_transport) { client.tcp } 25 | 26 | it_behaves_like 'a riemann client' 27 | it_behaves_like 'a riemann client that acknowledge messages' 28 | end 29 | 30 | context 'with TCP transport' do 31 | let(:client_with_transport) { client.tcp } 32 | 33 | it_behaves_like 'a riemann client' 34 | it_behaves_like 'a riemann client that acknowledge messages' 35 | end 36 | 37 | context('with UDP transport') do 38 | let(:client_with_transport) { client.udp } 39 | let(:expected_rate) { 1000 } 40 | 41 | it_behaves_like 'a riemann client' 42 | it_behaves_like 'a riemann client that does not acknowledge messages' 43 | 44 | context 'when sending a message too large for UDP transport' do 45 | let(:large_message) do 46 | { 47 | data: 'X' * (Riemann::Client::UDP::MAX_SIZE + 10) 48 | } 49 | end 50 | 51 | before do 52 | allow(client.udp).to receive(:send_maybe_recv).and_call_original 53 | allow(client.tcp).to receive(:send_maybe_recv).and_call_original 54 | client << large_message 55 | end 56 | 57 | it 'has tried to send the message using UDP' do 58 | expect(client.udp).to have_received(:send_maybe_recv) 59 | end 60 | 61 | it 'has retried to send the message using TCP' do 62 | expect(client.tcp).to have_received(:send_maybe_recv) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/riemann.config: -------------------------------------------------------------------------------- 1 | ; -*- mode: clojure; -*- 2 | ; vim: filetype=clojure 3 | 4 | (logging/init {:file "/var/log/riemann/riemann.log"}) 5 | 6 | ; Listen on the local interface over TCP (5555), UDP (5555), websockets 7 | ; (5556) and TLS (5554) 8 | (let [host "127.0.0.1"] 9 | (tcp-server {:host host}) 10 | (udp-server {:host host}) 11 | (ws-server {:host host}) 12 | (tcp-server {:host host :port 5554 :tls? true :key "/etc/riemann/riemann_server.pkcs8" :cert "/etc/riemann/riemann_server.crt" :ca-cert "/etc/riemann/riemann_server.crt"})) 13 | 14 | ; Expire old events from the index every 5 seconds. 15 | (periodically-expire 5) 16 | 17 | (let [index (index)] 18 | ; Inbound events will be passed to these streams: 19 | (streams 20 | (default :ttl 60 21 | ; Index all events immediately. 22 | ;index 23 | 24 | ; Index all events after a delay. 25 | (batch 1000 1/10 26 | (sflatten index)) 27 | 28 | ; Log expired events. 29 | (expired 30 | (fn [event] (info "expired" event)))))) 31 | -------------------------------------------------------------------------------- /spec/shared_examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'riemann' 4 | require 'riemann/client' 5 | require 'set' 6 | require 'timecop' 7 | 8 | INACTIVITY_TIME = 5 9 | 10 | class Sequence 11 | include Singleton 12 | 13 | def initialize 14 | @nextval = 0 15 | end 16 | 17 | def nextval 18 | @nextval += 1 19 | end 20 | 21 | def current 22 | @nextval 23 | end 24 | end 25 | 26 | def next_message_id 27 | Sequence.instance.nextval 28 | "#{Process.pid}-#{Sequence.instance.current}" 29 | end 30 | 31 | def wait_for_message_with_id(message_id) 32 | wait_for { client[%(message_id = "#{message_id}")].first } 33 | end 34 | 35 | def wait_for(&block) 36 | tries = 0 37 | while tries < 30 38 | tries += 1 39 | begin 40 | res = block.call 41 | return res if res 42 | rescue NoMethodError 43 | # If a query returns no result (#query retruns nil or #[] returns []), 44 | # calling #first on it will raise a NoMethodError. We can ignore it for 45 | # these tests. 46 | end 47 | sleep(0.1) 48 | end 49 | 50 | raise 'wait_for condition never realized' 51 | end 52 | 53 | RSpec.shared_examples 'a riemann client' do 54 | it 'is not connected before sending' do 55 | expect(client).not_to be_connected 56 | end 57 | 58 | context 'when given a block that raises' do 59 | let(:client) do 60 | res = nil 61 | begin 62 | Riemann::Client.new(host: 'localhost', port: 5555) do |c| 63 | res = c 64 | raise 'The Boom' 65 | end 66 | rescue StandardError 67 | # swallow the exception 68 | end 69 | res 70 | end 71 | 72 | it 'in not connected' do 73 | expect(client).not_to be_connected 74 | end 75 | end 76 | 77 | it 'is connected after sending' do 78 | client_with_transport << { state: 'ok', service: 'connected check' } 79 | expect(client_with_transport).to be_connected 80 | # NOTE: only single transport connected at this point, client.connected? is still false until all transports used 81 | end 82 | 83 | describe '#<<' do 84 | subject { wait_for_message_with_id(message_id) } 85 | 86 | let(:message_id) { next_message_id } 87 | 88 | before do 89 | client_with_transport << { 90 | state: 'ok', 91 | service: 'test', 92 | description: 'desc', 93 | metric_f: 1.0, 94 | message_id: message_id 95 | } 96 | end 97 | 98 | it 'finds the send message' do 99 | expect(subject.state).to eq('ok') 100 | end 101 | end 102 | 103 | context 'when sending metrics' do 104 | [ 105 | 0, 106 | -3, 107 | 5, 108 | -(2**63), 109 | 2**63 - 1, 110 | 0.0, 111 | 12.0, 112 | 1.2300000190734863 113 | ].each do |metric| 114 | context "with metric=#{metric}" do 115 | before do 116 | client_with_transport << { 117 | service: 'metric-test', 118 | metric: metric, 119 | message_id: message_id 120 | } 121 | end 122 | 123 | let(:message_id) { next_message_id } 124 | 125 | it 'return the exact value that was sent' do 126 | e = wait_for_message_with_id(message_id) 127 | expect(e.metric).to eq(metric) 128 | end 129 | end 130 | end 131 | end 132 | 133 | context 'when sending custom attributes' do 134 | subject { wait_for_message_with_id(message_id) } 135 | 136 | before do 137 | event = Riemann::Event.new( 138 | service: 'custom', 139 | state: 'ok', 140 | cats: 'meow', 141 | env: 'prod', 142 | message_id: message_id 143 | ) 144 | event[:sneak] = 'attack' 145 | client_with_transport << event 146 | end 147 | 148 | let(:message_id) { next_message_id } 149 | 150 | it 'has the expected service' do 151 | expect(subject.service).to eq('custom') 152 | end 153 | 154 | it 'has the expected state' do 155 | expect(subject.state).to eq('ok') 156 | end 157 | 158 | it 'has the expected cats' do 159 | expect(subject[:cats]).to eq('meow') 160 | end 161 | 162 | it 'has the expected env' do 163 | expect(subject[:env]).to eq('prod') 164 | end 165 | 166 | it 'has the expected sneak' do 167 | expect(subject[:sneak]).to eq('attack') 168 | end 169 | end 170 | 171 | context 'when passing time' do 172 | subject { wait_for_message_with_id(message_id) } 173 | 174 | before do 175 | Timecop.freeze 176 | client_with_transport << { 177 | state: 'ok', 178 | service: 'test', 179 | time: t, 180 | message_id: message_id 181 | } 182 | end 183 | 184 | after do 185 | Timecop.return 186 | end 187 | 188 | let(:message_id) { next_message_id } 189 | let(:t) { (Time.now - 10).to_i } 190 | 191 | it 'has the expected time' do 192 | expect(subject.time).to eq(t) 193 | end 194 | 195 | it 'has the expected time_micros' do 196 | expect(subject.time_micros).to eq(t * 1_000_000) 197 | end 198 | end 199 | 200 | context 'when passing time_micros' do 201 | subject { wait_for_message_with_id(message_id) } 202 | 203 | before do 204 | Timecop.freeze 205 | client_with_transport << { 206 | state: 'ok', 207 | service: 'test', 208 | time_micros: t, 209 | message_id: message_id 210 | } 211 | end 212 | 213 | after do 214 | Timecop.return 215 | end 216 | 217 | let(:message_id) { next_message_id } 218 | let(:t) { ((Time.now - 10).to_f * 1_000_000).to_i } 219 | 220 | it 'has the expected time' do 221 | expect(subject.time).to eq((Time.now - 10).to_i) 222 | end 223 | 224 | it 'has the expected time_micros' do 225 | expect(subject.time_micros).to eq(t) 226 | end 227 | end 228 | 229 | context 'when passing no time nor time_micros' do 230 | let(:message_id) { next_message_id } 231 | 232 | let(:time_before) { (Time.now.to_f * 1_000_000).to_i } 233 | let(:event) do 234 | client_with_transport << { 235 | state: 'ok', 236 | service: 'timeless test', 237 | message_id: message_id 238 | } 239 | end 240 | let(:time_after) { (Time.now.to_f * 1_000_000).to_i } 241 | 242 | it 'has the expected time_micros' do 243 | time_before 244 | event 245 | time_after 246 | 247 | e = wait_for_message_with_id(message_id) 248 | 249 | expect([time_before, e.time_micros, time_after].sort).to eq([time_before, e.time_micros, time_after]) 250 | end 251 | end 252 | 253 | describe '#query' do 254 | before do 255 | message_id1 = next_message_id 256 | message_id2 = next_message_id 257 | message_id3 = next_message_id 258 | 259 | client_with_transport << { state: 'critical', service: '1', message_id: message_id1 } 260 | client_with_transport << { state: 'warning', service: '2', message_id: message_id2 } 261 | client_with_transport << { state: 'critical', service: '3', message_id: message_id3 } 262 | 263 | wait_for_message_with_id(message_id3) 264 | end 265 | 266 | let(:rate) do 267 | t1 = Time.now 268 | total = 1000 269 | total.times do |_i| 270 | client.query('state = "critical"') 271 | end 272 | t2 = Time.now 273 | 274 | total / (t2 - t1) 275 | end 276 | 277 | it 'returns all events without parameters' do 278 | expect(client.query.events 279 | .map(&:service).to_set).to include(%w[1 2 3].to_set) 280 | end 281 | 282 | it 'returns matched events with parameters' do 283 | expect(client.query('state = "critical" and (service = "1" or service = "2" or service = "3")').events 284 | .map(&:service).to_set).to eq(%w[1 3].to_set) 285 | end 286 | 287 | it 'query quickly' do 288 | puts "\n #{format('%.2f', rate)} queries/sec (#{format('%.2f', (1000 / rate))}ms per query)" 289 | expect(rate).to be > 100 290 | end 291 | end 292 | 293 | it '[]' do 294 | message_id = next_message_id 295 | 296 | # expect(client['state = "critical"']).to be_empty 297 | client_with_transport << { state: 'critical', message_id: message_id } 298 | e = wait_for_message_with_id(message_id) 299 | expect(e.state).to eq('critical') 300 | end 301 | 302 | describe '#bulk_send' do 303 | let(:message_id1) { next_message_id } 304 | let(:message_id2) { next_message_id } 305 | let(:event1) { wait_for_message_with_id(message_id1) } 306 | let(:event2) { wait_for_message_with_id(message_id2) } 307 | 308 | before do 309 | client_with_transport.bulk_send( 310 | [ 311 | { 312 | state: 'ok', 313 | service: 'foo', 314 | message_id: message_id1 315 | }, 316 | { 317 | state: 'warning', 318 | service: 'bar', 319 | message_id: message_id2 320 | } 321 | ] 322 | ) 323 | end 324 | 325 | it 'has send the first event' do 326 | expect(event2.state).to eq('warning') 327 | end 328 | 329 | it 'has send the second event' do 330 | expect(event1.state).to eq('ok') 331 | end 332 | end 333 | 334 | context 'when using multiple threads' do 335 | let!(:rate) do 336 | concurrency = 10 337 | per_thread = 200 338 | total = concurrency * per_thread 339 | 340 | t1 = Time.now 341 | concurrency.times.map do 342 | Thread.new do 343 | per_thread.times do 344 | client_with_transport << { 345 | state: 'ok', 346 | service: 'test', 347 | description: 'desc', 348 | metric_f: 1.0, 349 | message_id: next_message_id 350 | } 351 | end 352 | end 353 | end.each(&:join) 354 | t2 = Time.now 355 | 356 | total / (t2 - t1) 357 | end 358 | 359 | it 'is threadsafe' do 360 | puts "\n #{format('%.2f', rate)} inserts/sec (#{format('%.2f', (1000 / rate))}ms per insert)" 361 | expect(rate).to be > expected_rate 362 | end 363 | end 364 | end 365 | 366 | RSpec.shared_examples 'a riemann client that acknowledge messages' do 367 | describe '#<<' do 368 | subject do 369 | client_with_transport << { 370 | state: 'ok', 371 | service: 'test', 372 | description: 'desc', 373 | metric_f: 1.0 374 | } 375 | end 376 | 377 | it 'acknowledge the message' do 378 | expect(subject.ok).to be_truthy 379 | end 380 | end 381 | 382 | context 'when inactive' do 383 | let(:message_id1) { next_message_id } 384 | let(:message1) do 385 | { 386 | state: 'warning', 387 | service: 'survive TCP inactivity', 388 | message_id: message_id1 389 | } 390 | end 391 | 392 | let(:message_id2) { next_message_id } 393 | let(:message2) do 394 | { 395 | state: 'ok', 396 | service: 'survive TCP inactivity', 397 | message_id: message_id2 398 | } 399 | end 400 | 401 | before do 402 | client_with_transport << message1 403 | wait_for_message_with_id(message_id1) 404 | end 405 | 406 | it 'survive inactivity' do 407 | sleep INACTIVITY_TIME 408 | 409 | expect((client_with_transport << message2).ok).to be_truthy 410 | wait_for_message_with_id(message_id2) 411 | end 412 | end 413 | 414 | context 'when the connection is closed' do 415 | let(:message_id1) { next_message_id } 416 | let(:message1) do 417 | { 418 | state: 'warning', 419 | service: 'survive TCP local close', 420 | message_id: message_id1 421 | } 422 | end 423 | 424 | let(:message_id2) { next_message_id } 425 | let(:message2) do 426 | { 427 | state: 'ok', 428 | service: 'survive TCP local close', 429 | message_id: message_id2 430 | } 431 | end 432 | 433 | before do 434 | client_with_transport << message1 435 | wait_for_message_with_id(message_id1) 436 | end 437 | 438 | it 'survive local close' do 439 | client.close 440 | 441 | expect((client_with_transport << message2).ok).to be_truthy 442 | wait_for_message_with_id(message_id2) 443 | end 444 | end 445 | end 446 | 447 | RSpec.shared_examples 'a riemann client that does not acknowledge messages' do 448 | describe '#<<' do 449 | subject do 450 | client_with_transport << { 451 | state: 'ok', 452 | service: 'test', 453 | description: 'desc', 454 | metric_f: 1.0 455 | } 456 | end 457 | 458 | it 'does not acknowledge the message' do 459 | expect(subject).to be_nil 460 | end 461 | end 462 | 463 | context 'when inactive' do 464 | let(:message_id1) { next_message_id } 465 | let(:message1) do 466 | { 467 | state: 'warning', 468 | service: 'survive UDP inactivity', 469 | message_id: message_id1 470 | } 471 | end 472 | 473 | let(:message_id2) { next_message_id } 474 | let(:message2) do 475 | { 476 | state: 'ok', 477 | service: 'survive UDP inactivity', 478 | message_id: message_id2 479 | } 480 | end 481 | 482 | before do 483 | client_with_transport << message1 484 | wait_for_message_with_id(message_id1) 485 | end 486 | 487 | it 'survive inactivity' do 488 | sleep INACTIVITY_TIME 489 | 490 | client_with_transport << message2 491 | expect { wait_for_message_with_id(message_id2) }.not_to raise_exception 492 | end 493 | end 494 | 495 | context 'when the connection is closed' do 496 | let(:message_id1) { next_message_id } 497 | let(:message1) do 498 | { 499 | state: 'warning', 500 | service: 'survive UDP local close', 501 | message_id: message_id1 502 | } 503 | end 504 | 505 | let(:message_id2) { next_message_id } 506 | let(:message2) do 507 | { 508 | state: 'ok', 509 | service: 'survive UDP local close', 510 | message_id: message_id2 511 | } 512 | end 513 | 514 | before do 515 | client_with_transport << message1 516 | wait_for_message_with_id(message_id1) 517 | end 518 | 519 | it 'survive local close' do 520 | client.close 521 | 522 | client_with_transport << message2 523 | expect { wait_for_message_with_id(message_id2) }.not_to raise_exception 524 | end 525 | end 526 | 527 | it 'raise Riemann::Client::Unsupported exception on #[]' do 528 | expect { client_with_transport['service = "test"'] }.to raise_error(Riemann::Client::Unsupported) 529 | end 530 | 531 | it 'raise Riemann::Client::Unsupported exception on #query' do 532 | expect { client_with_transport.query('service = "test"') }.to raise_error(Riemann::Client::Unsupported) 533 | end 534 | end 535 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | # Limits the available syntax to the non-monkey patched syntax that is 5 | # recommended. For more details, see: 6 | # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode 7 | # config.disable_monkey_patching! 8 | 9 | # This setting enables warnings. It's recommended, but in some cases may 10 | # be too noisy due to issues in dependencies. 11 | config.warnings = true 12 | 13 | # Run specs in random order to surface order dependencies. If you find an 14 | # order dependency and want to debug it, you can fix the order by providing 15 | # the seed, which is printed after each run. 16 | # --seed 1234 17 | config.order = :random 18 | 19 | # Seed global randomization in this process using the `--seed` CLI option. 20 | # Setting this allows you to use `--seed` to deterministically reproduce 21 | # test failures related to randomization by passing the same `--seed` value 22 | # as the one that triggered the failure. 23 | Kernel.srand config.seed 24 | 25 | # RSpec tries to be friendly to us by detecting deadlocks but this breaks CI 26 | # :-( 27 | # 28 | # Some tests want to start multiple thread in a let block, and this 29 | # thread-safety mechanism makes it impossible and raise an exception while 30 | # our code is working correctly. 31 | # 32 | # This issue seems the same as: 33 | # https://github.com/rspec/rspec-core/issues/2064 34 | # 35 | # The feature we disable was introduced in: 36 | # https://github.com/rspec/rspec-core/commit/ffe00a1d4e369e312881e6b2c091c8b6fb7e6087 37 | config.threadsafe = false 38 | end 39 | --------------------------------------------------------------------------------