Skip to content
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 19 additions & 6 deletions lib/diffdash/ast/visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion lib/diffdash/cli/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = []
Expand All @@ -33,6 +34,11 @@ def execute
end
end

if @version
puts "diffdash #{VERSION}"
return 0
end

if @help && @subcommand.nil?
print_help
return 0
Expand Down Expand Up @@ -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):
Expand Down
60 changes: 60 additions & 0 deletions spec/diffdash/ast/visitor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions spec/diffdash/cli/runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading