{"id":39157165,"url":"https://github.com/rossme/diffdash","last_synced_at":"2026-01-17T21:59:42.608Z","repository":{"id":331936460,"uuid":"1131895452","full_name":"rossme/diffdash","owner":"rossme","description":"PR-scoped observability signal extractor and Grafana dashboard generator","archived":false,"fork":false,"pushed_at":"2026-01-14T23:22:11.000Z","size":179,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-15T04:52:49.156Z","etag":null,"topics":["grafana","grafana-dashboard","grafana-dashboards","grafana-panel","grafana-prometheus","logger","logging","logs","ruby","rubygem","rubygems"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rossme.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-10T22:45:33.000Z","updated_at":"2026-01-14T23:22:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rossme/diffdash","commit_stats":null,"previous_names":["rossme/grafantastic","rossme/diffdash"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/rossme/diffdash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossme%2Fdiffdash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossme%2Fdiffdash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossme%2Fdiffdash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossme%2Fdiffdash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rossme","download_url":"https://codeload.github.com/rossme/diffdash/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossme%2Fdiffdash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28519254,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-17T18:55:29.170Z","status":"ssl_error","status_checked_at":"2026-01-17T18:55:03.375Z","response_time":85,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["grafana","grafana-dashboard","grafana-dashboards","grafana-panel","grafana-prometheus","logger","logging","logs","ruby","rubygem","rubygems"],"created_at":"2026-01-17T21:59:42.532Z","updated_at":"2026-01-17T21:59:42.599Z","avatar_url":"https://github.com/rossme.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Diffdash\n\nPR-scoped observability signal extractor and Grafana dashboard generator.\n\n## Overview\n\nDiffdash statically analyzes Ruby source code changed in a Pull Request and generates a Grafana dashboard JSON containing panels relevant to the observability signals found in that code.\n\n## Installation\n\n```bash\ngem install diffdash\n```\n\nOr from source:\n\n```bash\ngem build diffdash.gemspec\ngem install diffdash-*.gem\n```\n\n## Quick Start\n\n### 1. Add these to your application's `.env` (dotenv) file\n\n```bash\nGRAFANA_URL=https://myorg.grafana.net\nGRAFANA_TOKEN=glsa_xxxxxxxxxxxx\nGRAFANA_FOLDER_ID=42  # optional\n```\n\n### 2. Find your folder ID (optional)\n\n```bash\ndiffdash folders\n```\n\nOutput:\n```\nAvailable Grafana folders:\n\n  ID: 1      Title: General\n  ID: 42     Title: PR Dashboards\n  ID: 103    Title: Production\n\nSet GRAFANA_FOLDER_ID in your .env file to use a specific folder\n```\n\n### 3. Generate Dashboard\n\n```bash\n# From your repo with changed files\ndiffdash\n\n# Or dry-run to see JSON without uploading\ndiffdash --dry-run\n```\n\n## CLI Usage\n\n```bash\ndiffdash [command] [options]\n```\n\n**Commands:**\n- `folders` - List available Grafana folders\n- *(none)* - Run analysis and generate/upload dashboard\n\n**Options:**\n- `--dry-run` - Generate JSON only, don't upload to Grafana\n- `--verbose` - Show detailed progress and dynamic metric warnings\n- `--help` - Show help\n\n## Environment Variables\n\nSet these in a `.env` file in your project root:\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `GRAFANA_URL` | Yes | Grafana instance URL (e.g., `https://myorg.grafana.net`) |\n| `GRAFANA_TOKEN` | Yes | Grafana API token (Service Account token with Editor role) |\n| `GRAFANA_FOLDER_ID` | No | Target folder ID for dashboards |\n| `DIFFDASH_DRY_RUN` | No | Set to `true` to force dry-run mode |\n\n## Output\n\nWhen signals are found, JSON is output first, then a summary:\n\n```\n[diffdash] v0.4.0\n{ ... dashboard JSON ... }\n\n[diffdash] Dashboard created with 4 panels: 2 logs, 3 counters, 1 gauge, 1 histogram\n[diffdash] Uploaded to: https://myorg.grafana.net/d/abc123/feature-branch\n[diffdash] Note: 1 dynamic metric could not be added\n```\n\nIn dry-run mode:\n\n```\n[diffdash] v0.4.0\n{ ... dashboard JSON ... }\n\n[diffdash] Dashboard created with 4 panels: 2 logs, 3 counters, 1 gauge, 1 histogram\n[diffdash] Mode: dry-run (not uploaded)\n```\n\n**If no signals are found, no dashboard is created:**\n\n```\n[diffdash] v0.4.0\n[diffdash] No observability signals found in changed files\n[diffdash] Dashboard not created\n```\n\n## Observability Signals\n\n### Logs\n\n- `logger.info`, `logger.debug`, `logger.warn`, `logger.error`, `logger.fatal`\n- `Rails.logger.*`\n- `@logger.*`\n\n### Metrics\n\n| Client | Methods | Metric Type |\n|--------|---------|-------------|\n| Prometheus | `counter().increment` | counter |\n| Prometheus | `gauge().set` | gauge |\n| Prometheus | `histogram().observe` | histogram |\n| Prometheus | `summary()` | summary |\n| StatsD | `increment`, `incr` | counter |\n| StatsD | `gauge`, `set` | gauge |\n| StatsD | `timing`, `time` | histogram |\n| Statsd | (same as StatsD) | |\n| Hesiod | `emit` | counter |\n\n### Dynamic Metrics Warning\n\nMetrics with runtime-determined names cannot be added to dashboards:\n\n```ruby\n# ❌ Dynamic - cannot be analyzed statically\nPrometheus.counter(entity.id).increment\n\n# ✅ Static - will be detected and added to dashboard\nPrometheus.counter(:records_processed).increment(labels: { entity_id: id })\n```\n\nUse `--verbose` to see details about dynamic metrics that were detected but couldn't be added.\n\n## Guard Rails\n\nHard limits prevent noisy dashboards:\n\n| Signal Type | Max Count |\n|-------------|-----------|\n| Logs | 10 |\n| Metrics | 10 |\n| Events | 5 |\n| Total Panels | 12 |\n\nIf any limit is exceeded, the gem aborts with a clear error message and exits with code 1.\n\n## File Filtering\n\n**Included:**\n- Files ending with `.rb`\n- Ruby application code\n\n**Excluded:**\n- `*_spec.rb`, `*_test.rb`\n- Files in `/spec/`, `/test/`, `/config/`\n- Non-Ruby files\n\n## Inheritance \u0026 Module Support\n\nSignals are extracted from:\n- The touched class/module (depth = 0)\n- Parent classes (multi-level inheritance up to 5 levels deep)\n- Included modules (`include`)\n- Prepended modules (`prepend`)\n\n### Example\n\n```ruby\nmodule Loggable\n  def log_action\n    logger.info \"action_performed\"  # ✅ Detected\n  end\nend\n\nclass BaseProcessor\n  def process\n    StatsD.increment(\"base.processed\")  # ✅ Detected\n  end\nend\n\nclass PaymentProcessor \u003c BaseProcessor\n  include Loggable\n  \n  def charge\n    StatsD.increment(\"payment.charged\")  # ✅ Detected\n  end\nend\n```\n\nWhen `PaymentProcessor` is changed, signals from `BaseProcessor` and `Loggable` are also extracted.\n\n## Dashboard Behavior\n\n- **Deterministic UID:** Dashboard UID is derived from the branch name, ensuring the same PR always updates the same dashboard\n- **Overwrite:** Re-running the gem updates the existing dashboard rather than creating duplicates\n- **Template Variables:** Dashboards include `$service`, `$env`, and `$datasource` variables\n\n## GitHub Actions Integration\n\n### Setup\n\n1. **Add secrets to your repository:**\n   - `GRAFANA_URL` - Your Grafana instance URL\n   - `GRAFANA_TOKEN` - Service Account token with Editor role\n   - `GRAFANA_FOLDER_ID` (optional) - Folder ID for dashboards\n\n2. **Create workflow file** `.github/workflows/pr-dashboard.yml`:\n\n```yaml\nname: PR Observability Dashboard\n\non:\n  pull_request:\n    types: [opened, reopened, synchronize]\n\njobs:\n  dashboard:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: '3.x'\n\n      - name: Install diffdash\n        run: gem install diffdash\n\n      - name: Generate dashboard\n        env:\n          GRAFANA_URL: ${{ secrets.GRAFANA_URL }}\n          GRAFANA_TOKEN: ${{ secrets.GRAFANA_TOKEN }}\n          GRAFANA_FOLDER_ID: ${{ secrets.GRAFANA_FOLDER_ID }}\n        run: diffdash --verbose\n```\n\n## Development\n\n```bash\n# Install dependencies\nbundle install\n\n# Run tests\nbundle exec rspec\n\n# Run linter\nbundle exec rubocop\n\n# Build gem\ngem build diffdash.gemspec\n\n# Publish to GitHub Packages\n# Replace \u003cgithub_user_name\u003e with your GitHub username (e.g., rossme)\ngem push --key github \\\n  --host https://rubygems.pkg.github.com/\u003cgithub_user_name\u003e \\\n  diffdash-0.1.5.gem\n```\n\n## Testing Locally with Remote Grafana\n\nTo stream local logs/metrics to a remote Grafana instance, run Promtail and Prometheus\nalongside your app and then run Diffdash locally.\n\n**Requirements:**\n- Promtail (for logs) and Prometheus (for metrics)\n- `bundle exec diffdash` to generate/upload dashboards\n\n**Promtail (Docker)**\n\n```bash\ndocker run -d \\\n  --name promtail \\\n  -v $(pwd)/log:/host/log \\\n  -v $(pwd)/promtail.yml:/etc/promtail/config.yml \\\n  grafana/promtail:2.9.0 \\\n  -config.file=/etc/promtail/config.yml\n```\n\n**Configuration files**\n\nIn the app where Diffdash is installed, keep:\n- `promtail.yml` (Promtail config)\n- `prometheus.yml` (Prometheus config)\n\nThese live in the app root and are referenced by the commands above.\n\n**Run Diffdash locally**\n\n```bash\nbundle exec diffdash\n```\n\n## Log Matching Notes\n\nDiffdash builds Loki queries from log messages. For **plain string or symbol**\nmessages, it uses the exact literal in the query:\n\n```text\n{env=~\"$env\", app=~\"$app\"} |= \"Hello from Grape API!\"\n```\n\nFor **interpolated or dynamic strings**, Diffdash falls back to a sanitized\nidentifier to keep queries stable.\n\n## Grafana Schema Validation\n\nGrafana’s Schema v2 is still experimental, so Diffdash currently validates\nagainst the **v1 dashboard JSON model** (the format used by the Grafana API).\nWe enforce this via a golden‑file contract test to keep output stable.\n\nReference:\n- Grafana v1 dashboard JSON model: https://grafana.com/docs/grafana/latest/visualizations/dashboards/build-dashboards/view-dashboard-json-model/#dashboard-json\n- Grafana Schema v2 (experimental): https://grafana.com/docs/grafana/latest/as-code/observability-as-code/schema-v2/\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frossme%2Fdiffdash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frossme%2Fdiffdash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frossme%2Fdiffdash/lists"}