From d49d142d0ab2bb456d23d02eed63140cdc9d7132 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 25 Jan 2026 11:43:17 +0000 Subject: [PATCH] Add Datadog metric support and --version CLI flag Datadog Support: - Add Datadog and DogStatsD to METRIC_RECEIVERS - Properly differentiate factory-pattern (Prometheus) vs direct-action (StatsD/Datadog) clients - Support Datadog.increment, Datadog.gauge, Datadog.timing, etc. - Add distribution method support for histograms CLI --version flag: - Add --version option to print version and exit - Update help text to include --version Tests: - Add tests for Datadog.increment, DogStatsD.increment - Add tests for Datadog.gauge, Datadog.timing - Add tests for --version flag Documentation: - Update README with Datadog/DogStatsD in metrics table - Add --version to CLI options Co-authored-by: r.buddie --- README.md | 5 +++ lib/diffdash/ast/visitor.rb | 25 +++++++++---- lib/diffdash/cli/runner.rb | 9 ++++- spec/diffdash/ast/visitor_spec.rb | 60 +++++++++++++++++++++++++++++++ spec/diffdash/cli/runner_spec.rb | 14 ++++++++ 5 files changed, 106 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 80d588b..e845bd4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ diffdash [command] [options] **Options:** - `--dry-run` - Generate JSON only, don't upload to Grafana - `--verbose` - Show detailed progress and dynamic metric warnings +- `--version` - Show version number - `--help` - Show help ## Environment Variables @@ -137,6 +138,10 @@ In dry-run mode: | StatsD | `gauge`, `set` | gauge | | StatsD | `timing`, `time` | histogram | | Statsd | (same as StatsD) | | +| Datadog | `increment`, `incr` | counter | +| Datadog | `gauge`, `set` | gauge | +| Datadog | `timing`, `time` | histogram | +| DogStatsD | (same as Datadog) | | | Hesiod | `emit` | counter | ### Dynamic Metrics Warning diff --git a/lib/diffdash/ast/visitor.rb b/lib/diffdash/ast/visitor.rb index b0fec8c..4f8ba40 100644 --- a/lib/diffdash/ast/visitor.rb +++ b/lib/diffdash/ast/visitor.rb @@ -31,7 +31,7 @@ class Visitor LOG_GENERIC_METHODS = %i[add log].freeze # Generic logging methods that take severity as first arg # Metric client patterns - METRIC_RECEIVERS = %i[Prometheus StatsD Statsd Hesiod].freeze + METRIC_RECEIVERS = %i[Prometheus StatsD Statsd Hesiod Datadog DogStatsD].freeze COUNTER_METHODS = %i[counter increment incr].freeze GAUGE_METHODS = %i[gauge set].freeze HISTOGRAM_METHODS = %i[histogram observe timing time].freeze @@ -179,19 +179,32 @@ def log_call?(receiver, method_name) false end - # Methods that create metric objects (not action methods) + # Methods that create metric objects (Prometheus factory pattern) METRIC_FACTORY_METHODS = %i[counter gauge histogram summary].freeze # Methods that perform metric actions METRIC_ACTION_METHODS = %i[increment incr decrement decr set observe time timing].freeze + # StatsD-style action methods (called directly, not via factory pattern) + STATSD_ACTION_METHODS = %i[increment incr decrement decr gauge set timing time histogram distribution emit].freeze + # Receivers that use factory pattern (Prometheus.counter(:name).increment) + FACTORY_PATTERN_RECEIVERS = %i[Prometheus].freeze + # Receivers that use direct action pattern (StatsD.increment("name")) + DIRECT_ACTION_RECEIVERS = %i[StatsD Statsd Datadog DogStatsD Hesiod].freeze def metric_call?(receiver, method_name, args) return false unless receiver - # Direct calls with action method: StatsD.increment("metric") - # Only match if method_name is an action, not a factory + # Direct calls: StatsD.increment("metric"), Datadog.gauge("metric", val) if receiver.type == :const const_name = extract_const_name(receiver)&.to_sym - return METRIC_RECEIVERS.include?(const_name) && !METRIC_FACTORY_METHODS.include?(method_name) + return false unless METRIC_RECEIVERS.include?(const_name) + + # For direct-action receivers (StatsD, Datadog, etc.), accept their action methods + if DIRECT_ACTION_RECEIVERS.include?(const_name) + return STATSD_ACTION_METHODS.include?(method_name) + end + + # For factory-pattern receivers (Prometheus), reject factory methods as direct calls + return !METRIC_FACTORY_METHODS.include?(method_name) end # Chained calls: Prometheus.counter(:name).increment @@ -367,7 +380,7 @@ def extract_metric_name(args) def infer_metric_type(method_name) return :counter if COUNTER_METHODS.include?(method_name) return :gauge if GAUGE_METHODS.include?(method_name) - return :histogram if HISTOGRAM_METHODS.include?(method_name) + return :histogram if HISTOGRAM_METHODS.include?(method_name) || method_name == :distribution return :summary if SUMMARY_METHODS.include?(method_name) :counter # Default diff --git a/lib/diffdash/cli/runner.rb b/lib/diffdash/cli/runner.rb index 923a457..31aeb1b 100644 --- a/lib/diffdash/cli/runner.rb +++ b/lib/diffdash/cli/runner.rb @@ -4,7 +4,7 @@ module Diffdash module CLI # Thin CLI glue. Orchestrates engine + output adapters. class Runner - VALID_OPTIONS = %w[--dry-run --verbose -v --help -h].freeze + VALID_OPTIONS = %w[--dry-run --verbose -v --help -h --version].freeze VALID_SUBCOMMANDS = %w[folders rspec].freeze def self.run(args) @@ -16,6 +16,7 @@ def initialize(args) @config = Config.new @dry_run = ENV["DIFFDASH_DRY_RUN"] == "true" || args.include?("--dry-run") @help = args.include?("--help") || args.include?("-h") + @version = args.include?("--version") @verbose = args.include?("--verbose") || args.include?("-v") @subcommand = extract_subcommand(args) @dynamic_metrics = [] @@ -33,6 +34,11 @@ def execute end end + if @version + puts "diffdash #{VERSION}" + return 0 + end + if @help && @subcommand.nil? print_help return 0 @@ -307,6 +313,7 @@ def print_help Options: --dry-run Generate JSON only, skip Grafana connection --verbose Print detailed progress information + --version Show version number --help Show this help message Environment Variables (set in .env file): diff --git a/spec/diffdash/ast/visitor_spec.rb b/spec/diffdash/ast/visitor_spec.rb index b4fe1ca..4039039 100644 --- a/spec/diffdash/ast/visitor_spec.rb +++ b/spec/diffdash/ast/visitor_spec.rb @@ -342,6 +342,66 @@ def bar expect(visitor.metric_calls.size).to eq(1) end + it "detects Datadog.increment calls" do + source = <<~RUBY + class Foo + def bar + Datadog.increment("payments.processed") + end + end + RUBY + parse_and_visit(source) + + expect(visitor.metric_calls.size).to eq(1) + expect(visitor.metric_calls.first[:name]).to eq("payments.processed") + expect(visitor.metric_calls.first[:metric_type]).to eq(:counter) + end + + it "detects DogStatsD.increment calls" do + source = <<~RUBY + class Foo + def bar + DogStatsD.increment("events.count") + end + end + RUBY + parse_and_visit(source) + + expect(visitor.metric_calls.size).to eq(1) + expect(visitor.metric_calls.first[:name]).to eq("events.count") + expect(visitor.metric_calls.first[:metric_type]).to eq(:counter) + end + + it "detects Datadog.gauge calls" do + source = <<~RUBY + class Foo + def bar + Datadog.gauge("queue.size", 42) + end + end + RUBY + parse_and_visit(source) + + expect(visitor.metric_calls.size).to eq(1) + expect(visitor.metric_calls.first[:name]).to eq("queue.size") + expect(visitor.metric_calls.first[:metric_type]).to eq(:gauge) + end + + it "detects Datadog.timing calls" do + source = <<~RUBY + class Foo + def bar + Datadog.timing("request.duration", 150) + end + end + RUBY + parse_and_visit(source) + + expect(visitor.metric_calls.size).to eq(1) + expect(visitor.metric_calls.first[:name]).to eq("request.duration") + expect(visitor.metric_calls.first[:metric_type]).to eq(:histogram) + end + it "detects Prometheus.counter calls" do source = <<~RUBY class Foo diff --git a/spec/diffdash/cli/runner_spec.rb b/spec/diffdash/cli/runner_spec.rb index 97c6baa..627c737 100644 --- a/spec/diffdash/cli/runner_spec.rb +++ b/spec/diffdash/cli/runner_spec.rb @@ -56,4 +56,18 @@ def render(_bundle) expect(errors.size).to eq(1) expect(results.values.map { |r| r[:payload] }).to include({ ok: true }) end + + describe "--version flag" do + it "prints version and exits with 0" do + runner = described_class.new(["--version"]) + + expect { runner.execute }.to output("diffdash #{Diffdash::VERSION}\n").to_stdout + end + + it "returns exit code 0" do + runner = described_class.new(["--version"]) + + expect(runner.execute).to eq(0) + end + end end