diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5874f5..35c8968 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: build -on: push +on: [push, create] jobs: build_matrix: @@ -47,3 +47,25 @@ jobs: - name: Dummy for branch status checks run: | echo "build complete" + + release: + needs: build + if: contains(github.ref, 'tags') && github.event_name == 'create' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + - name: Prepare Gemfury + run: | + mkdir -p ~/.gem + echo -e "---\n:fury_push_token: ${{ secrets.GEMFURY_TOKEN }}" > ~/.gem/credentials + chmod 0600 ~/.gem/credentials + + - name: Build Gem + run: | + gem build *.gemspec + + - name: Publish Gem + run: | + gem push *.gem --key fury_push_token \ + --host https://push.fury.io/babbel diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bbf826c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2020-12-18 +### Added +- Core Plugin +- Telemetry generation +- IO Target with JSON formatter +- Datadog Statsd Target diff --git a/README.md b/README.md index cd92073..e289c65 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # Puma::Plugin::Telemetry -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/puma/plugin/telemetry`. To experiment with that code, run `bin/console` for an interactive prompt. - -TODO: Delete this and the text above, and describe your gem +Puma plugin adding ability to publish various metrics to your prefered targets. ## Install @@ -22,17 +20,87 @@ Or install it yourself as: ## Usage -TODO: Write usage instructions here +In your puma configuration file (i.e. `config/puma.rb` or `config/puma/.rb`): + +```ruby +plugin "telemetry" + +Puma::Plugin::Telemetry.configure do |config| + config.enabled = true + + # << here rest of the configuration, examples below +end +``` + +### Basic + +Output telemetry as JSON to STDOUT + +```ruby + config.add_target :io +``` + +### Datadog statsd target + +Given gem provides built in target for Datadog Statsd client, that uses batch operation to publish metrics. + +**NOTE** Be sure to have `dogstatsd` gem installed. + +```ruby + config.add_target :dogstatsd, client: Datadog::Statsd.new +``` + +You can provide all the tags, namespaces, and other configuration options as always to `Datadog::Statsd.new` method. + +### All available options + +For detailed documentation checkout [`Puma::Plugin::Telemetry::Config`](./lib/puma/plugin/telemetry/config.rb) class. + +```ruby +Puma::Plugin::Telemetry.configure do |config| + config.enabled = true + config.initial_delay = 10 + config.frequency = 30 + config.puma_telemetry = %w[workers.requests_count queue.backlog queue.capacity] + config.add_target :io, formatter: :json, io: StringIO.new + config.add_target :dogstatsd, client: Datadog::Statsd.new(tags: { env: ENV["RAILS_ENV"] }) +end +``` + +### Custom Targets + +Target is a simple object that implements `call` methods that accepts `telemetry` hash object. This means it can be super simple `proc` or some sofisticated class calling some external API. + +Just be mindful that if the API takes long to call, it will slow down frequency with which telemetry will get reported. + +```ruby + # Example logfmt to stdout target + config.add_target proc { |telemetry| puts telemetry.map { |k, v| "#{k}=#{v.inspect}" }.join(" ") } +``` ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +To install this gem onto your local machine, run `bundle exec rake install`. + +## Release + +All gem releases are manual, in order to create a new release follow: + +1. Create new PR (this could be included in feature PR, if it's meant to be released) + - update `VERSION`, we use [Semantic Versioning](https://semver.org/spec/v2.0.0.html) + - update `CHANGELOG` + - merge +2. Draft new release via Github Releases + - use `v#{VERSION}` as a tag, i.e. `v0.1.0` + - add release notes based on the Changelog + - create +3. Gem will get automatically published to given rubygems server ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/puma-plugin-telemetry. +Bug reports and pull requests are welcome on GitHub at https://github.com/lessonnine/puma-plugin-telemetry. ## License diff --git a/Rakefile b/Rakefile index b6ae734..971d985 100644 --- a/Rakefile +++ b/Rakefile @@ -2,7 +2,9 @@ require "bundler/gem_tasks" require "rspec/core/rake_task" +require "rubocop/rake_task" RSpec::Core::RakeTask.new(:spec) +RuboCop::RakeTask.new -task default: :spec +task default: %i[rubocop spec] diff --git a/lib/puma/plugin/telemetry.rb b/lib/puma/plugin/telemetry.rb index 8177dc6..a08a94e 100644 --- a/lib/puma/plugin/telemetry.rb +++ b/lib/puma/plugin/telemetry.rb @@ -4,9 +4,10 @@ require "puma/plugin" require "puma/plugin/telemetry/version" -require "puma/plugin/telemetry/config" require "puma/plugin/telemetry/data" -require "puma/plugin/telemetry/target/datadog_statsd_target" +require "puma/plugin/telemetry/targets/datadog_statsd_target" +require "puma/plugin/telemetry/targets/io_target" +require "puma/plugin/telemetry/config" module Puma class Plugin diff --git a/lib/puma/plugin/telemetry/config.rb b/lib/puma/plugin/telemetry/config.rb index 8a554b1..afc58ea 100644 --- a/lib/puma/plugin/telemetry/config.rb +++ b/lib/puma/plugin/telemetry/config.rb @@ -30,6 +30,11 @@ class Config "queue.capacity" ].freeze + TARGETS = { + dogstatsd: Telemetry::Targets::DatadogStatsdTarget, + io: Telemetry::Targets::IOTarget + }.freeze + # Whenever telemetry should run with puma # - default: false attr_accessor :enabled @@ -64,6 +69,16 @@ def initialize def enabled? !!@enabled end + + def add_target(name_or_target, **args) + return @targets.push(name_or_target) unless name_or_target.is_a?(Symbol) + + target = TARGETS.fetch(name_or_target) do + raise Telemetry::Error, "Unknown Target: #{name_or_target.inspect}, #{args.inspect}" + end + + @targets.push(target.new(**args)) + end end end end diff --git a/lib/puma/plugin/telemetry/target/datadog_statsd_target.rb b/lib/puma/plugin/telemetry/targets/datadog_statsd_target.rb similarity index 79% rename from lib/puma/plugin/telemetry/target/datadog_statsd_target.rb rename to lib/puma/plugin/telemetry/targets/datadog_statsd_target.rb index 77cf433..b438eb9 100644 --- a/lib/puma/plugin/telemetry/target/datadog_statsd_target.rb +++ b/lib/puma/plugin/telemetry/targets/datadog_statsd_target.rb @@ -3,7 +3,7 @@ module Puma class Plugin module Telemetry - module Target + module Targets # Target wrapping Datadog Statsd client. You can configure # all details like _metrics prefix_ and _tags_ in the client # itself. @@ -19,16 +19,13 @@ module Target # version: ENV["CODE_VERSION"] # }) # - # DatadogStatsdTarget.new(client) + # DatadogStatsdTarget.new(client: client) # class DatadogStatsdTarget - def initialize(client) + def initialize(client:) @client = client end - # TODO: Support other metric types, like `counter` backed into - # telemetry. Best example that would use this is `request_count` - # def call(telemetry) client.batch do |statsd| telemetry.each do |metric, value| diff --git a/lib/puma/plugin/telemetry/targets/io_target.rb b/lib/puma/plugin/telemetry/targets/io_target.rb new file mode 100644 index 0000000..2351486 --- /dev/null +++ b/lib/puma/plugin/telemetry/targets/io_target.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "json" + +module Puma + class Plugin + module Telemetry + module Targets + # Simple IO Target, publishing metrics to STDOUT or logs + # + class IOTarget + # JSON formatter for IO, expects `call` method accepting telemetry hash + # + class JSONFormatter + def self.call(telemetry) + ::JSON.dump(telemetry.merge(name: "Puma::Plugin::Telemetry", message: "Publish telemetry")) + end + end + + def initialize(io: $stdout, formatter: :json) + @io = io + @formatter = case formatter + when :json then JSONFormatter + else formatter + end + end + + def call(telemetry) + @io.puts(@formatter.call(telemetry)) + end + end + end + end + end +end diff --git a/spec/fixtures/config.rb b/spec/fixtures/config.rb index b18b01b..a5a7823 100644 --- a/spec/fixtures/config.rb +++ b/spec/fixtures/config.rb @@ -13,8 +13,8 @@ def call(telemetry) end Puma::Plugin::Telemetry.configure do |config| - config.targets << Target.new("01") - config.targets << Target.new("02") + config.add_target Target.new("01") + config.add_target Target.new("02") config.frequency = 0.2 config.enabled = true config.initial_delay = 0 diff --git a/spec/fixtures/puma_telemetry_subset.rb b/spec/fixtures/puma_telemetry_subset.rb index f87f366..998204d 100644 --- a/spec/fixtures/puma_telemetry_subset.rb +++ b/spec/fixtures/puma_telemetry_subset.rb @@ -8,7 +8,7 @@ plugin "telemetry" Puma::Plugin::Telemetry.configure do |config| - config.targets << ->(telemetry) { puts "telemetry=#{telemetry.inspect}" } + config.add_target :io, formatter: :json config.frequency = 0.2 config.enabled = true diff --git a/spec/integration/plugin_spec.rb b/spec/integration/plugin_spec.rb index bc718b9..fb8f2a7 100644 --- a/spec/integration/plugin_spec.rb +++ b/spec/integration/plugin_spec.rb @@ -62,16 +62,12 @@ class Plugin context "when subset of telemetry" do let(:config) { "puma_telemetry_subset" } let(:expected_telemetry) do - { - "queue.backlog" => 0, - "workers.spawned_threads" => 2, - "workers.max_threads" => 4 - } + "{\"queue.backlog\":0,\"workers.spawned_threads\":2,\"workers.max_threads\":4,\"name\":\"Puma::Plugin::Telemetry\",\"message\":\"Publish telemetry\"}\n" # rubocop:disable Layout/LineLength end it "logs only selected telemetry" do - true while (line = @server.next_line) !~ /telemetry=/ - expect(line).to start_with "telemetry=#{expected_telemetry.inspect}" + true while (line = @server.next_line) !~ /Puma::Plugin::Telemetry/ + expect(line).to start_with expected_telemetry end end end diff --git a/spec/puma/plugin/telemetry/config_spec.rb b/spec/puma/plugin/telemetry/config_spec.rb new file mode 100644 index 0000000..2932f17 --- /dev/null +++ b/spec/puma/plugin/telemetry/config_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Puma + class Plugin + module Telemetry + RSpec.describe Config do + subject(:config) { described_class.new } + + describe "#enabled?" do + context "when default" do + it { expect(config.enabled?).to eq false } + end + + context "when enabled" do + before { config.enabled = true } + + it { expect(config.enabled?).to eq true } + end + end + + describe "#add_target" do + context "when built in: IO" do + it "adds new target" do + expect { config.add_target(:io) }.to change(config.targets, :size).by(1) + end + + it "adds new IO Target" do + config.add_target(:io) + expect(config.targets.first).to be_a(Telemetry::Targets::IOTarget) + end + end + + context "when built in: Datadog" do + let(:client) { instance_double("statsd") } + + it "adds new target" do + expect do + config.add_target(:dogstatsd, client: client) + end.to change(config.targets, :size).by(1) + end + + it "adds new Datadog Target" do + config.add_target(:dogstatsd, client: client) + expect(config.targets.first).to be_a(Telemetry::Targets::DatadogStatsdTarget) + end + end + + context "when custom" do + let(:target) { proc { |telemetry| puts telemetry.inspect } } + + it "adds new target" do + expect do + config.add_target(target) + end.to change(config.targets, :size).by(1) + end + + it "adds new Custom Target" do + config.add_target(target) + expect(config.targets.first).to be_a(Proc) + end + end + + context "when multiple targets" do + it "adds new targets" do + expect do + config.add_target(proc { |telemetry| puts telemetry.inspect }) + config.add_target(:io) + config.add_target(:io) + end.to change(config.targets, :size).by(3) + end + end + end + end + end + end +end