Comprehensive documentation for Diffdash - PR-scoped observability dashboard generator.
- Installation
- Configuration
- CLI Reference
- Output Adapters
- Signal Detection
- GitHub Actions
- Local Development
- Troubleshooting
gem install diffdash# Gemfile
source "https://rubygems.pkg.github.com/rossme" do
gem "diffdash"
endgem build diffdash.gemspec
gem install diffdash-*.gemCreate diffdash.yml in your repository root:
# Output adapters (grafana, datadog, kibana, json)
outputs:
- grafana
# Grafana settings
grafana:
url: https://myorg.grafana.net
folder_id: 42
# Datadog settings (if using datadog output)
datadog:
site: datadoghq.com # or datadoghq.eu, etc.
# Kibana settings (if using kibana output)
kibana:
url: https://my-deployment.kb.us-east-1.aws.elastic.cloud
space_id: default
index_pattern: logs-*
# General settings
default_env: production
app_name: my-service
# File filtering
ignore_paths:
- vendor/
- lib/legacy/
include_paths: # Optional whitelist
- app/
- lib/
excluded_suffixes:
- _spec.rb
- _test.rb
excluded_directories:
- spec
- test
- config
# Signal filtering
signals:
# How to handle interpolated logs (include, warn, exclude)
interpolated_logs: includeControl how interpolated logs are handled in dashboards:
signals:
interpolated_logs: exclude # Options: include, warn, exclude| Value | Behavior |
|---|---|
include |
Include all logs in dashboard (default) |
warn |
Include all, but show CLI warning suggesting structured logging |
exclude |
Exclude interpolated logs from dashboard |
Environment variable: DIFFDASH_INTERPOLATED_LOGS
# Exclude interpolated logs via env var
DIFFDASH_INTERPOLATED_LOGS=exclude diffdash grafanaEnvironment variables always override config file values.
| Variable | Required | Description |
|---|---|---|
DIFFDASH_GRAFANA_URL |
Yes* | Grafana instance URL |
DIFFDASH_GRAFANA_TOKEN |
Yes | Service Account token (Editor role) |
DIFFDASH_GRAFANA_FOLDER_ID |
No | Target folder ID |
| Variable | Required | Description |
|---|---|---|
DIFFDASH_DATADOG_API_KEY |
Yes | Datadog API key |
DIFFDASH_DATADOG_APP_KEY |
Yes | Datadog Application key |
DIFFDASH_DATADOG_SITE |
No | Datadog site (default: datadoghq.com) |
| Variable | Required | Description |
|---|---|---|
DIFFDASH_KIBANA_URL |
Yes | Kibana instance URL |
DIFFDASH_KIBANA_API_KEY |
Yes** | Kibana API key |
DIFFDASH_KIBANA_USERNAME |
Yes** | Kibana username (if not using API key) |
DIFFDASH_KIBANA_PASSWORD |
Yes** | Kibana password (if not using API key) |
DIFFDASH_KIBANA_SPACE_ID |
No | Kibana space ID (default: default) |
DIFFDASH_KIBANA_INDEX_PATTERN |
No | Index pattern (default: logs-*) |
*Either API key OR username/password required
| Variable | Description |
|---|---|
DIFFDASH_OUTPUTS |
Comma-separated outputs (default: grafana) |
DIFFDASH_DRY_RUN |
Set to true for dry-run mode |
DIFFDASH_DEFAULT_ENV |
Default environment filter |
DIFFDASH_APP_NAME |
Override app name |
- Environment variables (highest priority)
--configflag specified file- Config file in current directory
- Config file in git root
- Default values (lowest priority)
API tokens are only loaded from environment variables — never from config files. This prevents accidental commits of secrets.
diffdash [output] [options]| Output | Description |
|---|---|
grafana |
Generate and upload Grafana dashboard |
datadog |
Generate and upload Datadog dashboard |
kibana |
Generate and upload Kibana dashboard |
json |
Output raw signal data as JSON |
| (none) | Use outputs from config or DIFFDASH_OUTPUTS |
| Command | Description |
|---|---|
lint |
Check for observability best practices |
grafana folders |
List available Grafana folders |
kibana folders |
List available Kibana spaces |
| Option | Description |
|---|---|
--config FILE |
Path to configuration file |
--dry-run |
Generate without uploading |
--list-signals |
Show detected signals only |
--verbose |
Detailed output |
--version |
Show version |
--help |
Show help |
# Generate Grafana dashboard
diffdash grafana
# Generate Kibana dashboard with verbose output
diffdash kibana --verbose
# Generate Datadog dashboard without uploading
diffdash datadog --dry-run
# List available folders/spaces
diffdash grafana folders
diffdash kibana folders
# Check for observability best practices
diffdash lint
diffdash lint --verbose
# See detected signals without uploading
diffdash --list-signals
# Use multiple outputs (via env var)
DIFFDASH_OUTPUTS=grafana,json diffdashDiffdash supports multiple observability backends:
Generates Grafana dashboard JSON with:
- Log panels using Loki queries
- Metric panels using PromQL
- Template variables for
app,env,datasource - PR deployment annotations
Requirements:
- Grafana Service Account token with Editor role
- Loki datasource for logs
- Prometheus datasource for metrics
Generates Datadog dashboard JSON with:
- Log stream widgets
- Timeseries widgets for metrics
- Template variables
Requirements:
- Datadog API key and Application key
Generates Kibana Saved Objects (NDJSON) with:
- Saved searches showing log entries
- Metric visualizations
- Index pattern configuration
Requirements:
- Kibana API key or username/password
- Elasticsearch with your log data
Note: For Elastic Cloud Serverless, set DIFFDASH_KIBANA_INDEX_PATTERN to match your data stream (e.g., logs-myapp-default).
Outputs raw signal data as JSON to stdout. Useful for debugging or piping to other tools.
logger.info("message")
logger.debug("message")
logger.warn("message")
logger.error("message")
logger.fatal("message")
Rails.logger.info("message")
@logger.info("message")| Client | Methods | Metric Type |
|---|---|---|
| Prometheus | counter().increment |
counter |
| Prometheus | gauge().set |
gauge |
| Prometheus | histogram().observe |
histogram |
| StatsD | increment, incr |
counter |
| StatsD | gauge, set |
gauge |
| StatsD | timing, time |
histogram |
| Datadog | increment, incr |
counter |
| Datadog | gauge, set |
gauge |
| Hesiod | emit |
counter |
| Hesiod | gauge |
gauge |
Diffdash automatically resolves metric constants defined in centralized files:
# app/services/metrics.rb
module Metrics
RequestTotal = Hesiod.register_counter("request_total")
QueueDepth = Hesiod.register_gauge("queue_depth")
end
# app/jobs/worker.rb
class Worker
def perform
Metrics::RequestTotal.increment # ✅ Resolved to "request_total"
Metrics::QueueDepth.set(5) # ✅ Resolved to "queue_depth"
end
endDiffdash scans these common locations for metric definitions:
app/services/metrics.rblib/metrics.rbapp/lib/metrics.rbconfig/initializers/metrics.rb
Plain strings/symbols — exact match:
logger.info("user_created")
# Grafana: |= "user_created"
# Kibana: message:"user_created"Interpolated strings — static parts extracted:
logger.info("Loaded widget #{id}")
# Grafana: |= "Loaded widget "
# Kibana: message:"Loaded widget "Signals are extracted from:
- The changed class/module (depth 0)
- Parent classes (up to 5 levels)
- Included modules
- Prepended modules
The diffdash lint command checks for observability best practices.
Logs with string interpolation are harder to query:
# ⚠️ Interpolated - hard to match in Loki/Kibana
logger.info("User #{user.id} logged in")
# Matches on: "User " and " logged in"
# ✅ Structured - exact match
logger.info("user_logged_in", user_id: user.id)
# Matches on: "user_logged_in"# Check for issues
diffdash lint
# Show details for each issue
diffdash lint --verbose[diffdash] Linting observability patterns...
[diffdash] Analyzing 4 files...
Found 3 logs with string interpolation.
Consider structured logging for better observability matching.
Example:
Before: logger.info("User #{user.id} logged in")
After: logger.info("user_logged_in", user_id: user.id)
Run 'diffdash lint --verbose' for details.
During dashboard generation, a warning is shown if interpolated logs are found:
[diffdash] ⚠ Found 3 interpolated logs (run 'diffdash lint' for suggestions)
name: Diffdash Dashboard
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- "**/*.rb"
- "!spec/**"
- "!test/**"
push:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
generate-dashboard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
- name: Set branch name
run: |
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
git checkout -B "$BRANCH"
- name: Install diffdash
run: gem install diffdash
- name: Generate dashboard
env:
DIFFDASH_GRAFANA_URL: ${{ secrets.DIFFDASH_GRAFANA_URL }}
DIFFDASH_GRAFANA_TOKEN: ${{ secrets.DIFFDASH_GRAFANA_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
run: diffdash --verbose| Secret | Description |
|---|---|
DIFFDASH_GRAFANA_URL |
Grafana instance URL |
DIFFDASH_GRAFANA_TOKEN |
Grafana Service Account token |
GITHUB_TOKEN |
Auto-provided for PR comments |
- Install Promtail for log shipping:
docker run -d \
--name promtail \
-v $(pwd)/log:/host/log \
-v $(pwd)/promtail.yml:/etc/promtail/config.yml \
grafana/promtail:2.9.0 \
-config.file=/etc/promtail/config.yml- Create
promtail.yml:
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: https://logs-prod-xxx.grafana.net/loki/api/v1/push
basic_auth:
username: <your-user-id>
password: <your-api-key>
scrape_configs:
- job_name: myapp
static_configs:
- targets: [localhost]
labels:
app: myapp
env: local
__path__: /host/log/*.log- Run Diffdash:
bundle exec diffdash --verbose- Set up Elastic Agent to ship logs to your Elasticsearch cluster
- Configure environment variables:
export DIFFDASH_KIBANA_URL=https://my-deployment.kb.region.aws.elastic.cloud
export DIFFDASH_KIBANA_API_KEY=your-api-key
export DIFFDASH_KIBANA_INDEX_PATTERN=logs-myapp-default
export DIFFDASH_OUTPUTS=kibana- Run Diffdash:
bundle exec diffdash --verboseFor a complete example with logs, metrics, and CI integration, see: diffdash-test-app
- Check that you have changed Ruby files in your branch
- Ensure files aren't excluded by
ignore_pathsorexcluded_directories - Use
--list-signalsto debug detection
- Verify
DIFFDASH_GRAFANA_TOKENis set correctly - Ensure the Service Account has Editor role
- Check the token hasn't expired
- Verify
DIFFDASH_KIBANA_INDEX_PATTERNmatches your actual data stream - Check the time range in Kibana includes recent data
- Ensure logs are being shipped to Elasticsearch
Logs with interpolation are detected but harder to query:
# ⚠️ Interpolated - harder to match
logger.info("User #{user.id} logged in")
# ✅ Structured - exact match
logger.info("user_logged_in", user_id: user.id)Run diffdash lint --verbose to see all interpolated logs.
Metrics with runtime-determined names cannot be analyzed:
# ❌ Dynamic - cannot be detected
StatsD.increment("#{entity.type}.processed")
# ✅ Static - will be detected
StatsD.increment("entity.processed", tags: { type: entity.type })Hard limits prevent noisy dashboards:
| Signal Type | Max Count |
|---|---|
| Logs | 10 |
| Metrics | 10 |
| Total Panels | 12 |
Excess signals are truncated with a warning.
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Run linter
bundle exec rubocop
# Build gem
gem build diffdash.gemspecMIT