Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.3.4
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Set these in a `.env` file in your project root:
| `DIFFDASH_GRAFANA_FOLDER_ID` | No | Target folder ID for dashboards |
| `DIFFDASH_OUTPUTS` | No | Comma-separated outputs (default: `grafana`) |
| `DIFFDASH_DRY_RUN` | No | Set to `true` to force dry-run mode |
| `DIFFDASH_PR_DEPLOY_ANNOTATION_EXPR` | No | PromQL expr for PR deployment annotation |

Legacy `GRAFANA_*` env vars are still supported as fallbacks for now.

Expand Down
11 changes: 6 additions & 5 deletions lib/diffdash/cli/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,12 @@ def print_help
--help Show this help message

Environment Variables (set in .env file):
DIFFDASH_GRAFANA_URL Grafana instance URL (required)
DIFFDASH_GRAFANA_TOKEN Grafana API token (required)
DIFFDASH_GRAFANA_FOLDER_ID Target folder ID (optional)
DIFFDASH_OUTPUTS Comma-separated outputs (default: grafana)
DIFFDASH_DRY_RUN Set to 'true' to force dry-run mode
DIFFDASH_GRAFANA_URL Grafana instance URL (required)
DIFFDASH_GRAFANA_TOKEN Grafana API token (required)
DIFFDASH_GRAFANA_FOLDER_ID Target folder ID (optional)
DIFFDASH_OUTPUTS Comma-separated outputs (default: grafana)
DIFFDASH_DRY_RUN Set to 'true' to force dry-run mode
DIFFDASH_PR_DEPLOY_ANNOTATION_EXPR PromQL for PR deployment annotation

Output:
Prints output JSON to STDOUT (Grafana first if configured).
Expand Down
60 changes: 46 additions & 14 deletions lib/diffdash/outputs/grafana.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def build_dashboard(signal_bundle)
},
templating: build_templating,
panels: build_panels(signal_bundle),
annotations: build_annotations
annotations: build_annotations(signal_bundle)
}
end

Expand Down Expand Up @@ -107,19 +107,12 @@ def app_variable
}
end

def build_annotations
{
list: [
{
name: "Deployments",
datasource: { type: "prometheus", uid: "${datasource}" },
enable: true,
expr: "changes(deploy_timestamp[5m]) > 0",
tagKeys: "app,env",
titleFormat: "Deploy"
}
]
}
def build_annotations(signal_bundle)
annotations = [deployment_annotation]
pr_annotation = pr_deployment_annotation(signal_bundle)
annotations << pr_annotation if pr_annotation

{ list: annotations }
end

def build_panels(signal_bundle)
Expand Down Expand Up @@ -323,6 +316,10 @@ def escape_log_value(value)
value.to_s.gsub("\\", "\\\\").gsub("\"", "\\\"")
end

def escape_promql_label(value)
value.to_s.gsub("\\", "\\\\").gsub("\"", "\\\"")
end

def sanitize_metric_name(name)
name.to_s.gsub(/[^a-zA-Z0-9_:]/, "_")
end
Expand All @@ -338,6 +335,41 @@ def relative_path(path)
def log_verbose(message)
warn "[diffdash] #{message}" if @verbose
end

def deployment_annotation
{
name: "Deployments",
datasource: { type: "prometheus", uid: "${datasource}" },
enable: true,
expr: "changes(deploy_timestamp[5m]) > 0",
tagKeys: "app,env",
titleFormat: "Deploy"
}
end

def pr_deployment_annotation(signal_bundle)
expr = pr_deployment_expr(signal_bundle)
return nil if expr.nil? || expr.empty?

{
name: "PR Deployments",
datasource: { type: "prometheus", uid: "${datasource}" },
enable: true,
expr: expr,
tagKeys: "app,env,branch",
titleFormat: "PR Deploy"
}
end

def pr_deployment_expr(signal_bundle)
override = ENV["DIFFDASH_PR_DEPLOY_ANNOTATION_EXPR"].to_s.strip
return override unless override.empty?

branch = signal_bundle.metadata.dig(:change_set, :branch_name).to_s.strip
return nil if branch.empty?

"changes(deploy_timestamp{branch=\"#{escape_promql_label(branch)}\"}[5m]) > 0"
end
end
end
end
35 changes: 32 additions & 3 deletions spec/diffdash/config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,55 @@
end

describe "#grafana_url" do
it "reads from GRAFANA_URL environment variable" do
it "prefers DIFFDASH_GRAFANA_URL when set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_URL").and_return("https://diffdash.example.com")
allow(ENV).to receive(:[]).with("GRAFANA_URL").and_return("https://grafana.example.com")
expect(config.grafana_url).to eq("https://diffdash.example.com")
end

it "falls back to GRAFANA_URL when DIFFDASH_GRAFANA_URL is not set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_URL").and_return(nil)
allow(ENV).to receive(:[]).with("GRAFANA_URL").and_return("https://grafana.example.com")
expect(config.grafana_url).to eq("https://grafana.example.com")
end

it "returns nil when not set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_URL").and_return(nil)
allow(ENV).to receive(:[]).with("GRAFANA_URL").and_return(nil)
expect(config.grafana_url).to be_nil
end
end

describe "#grafana_token" do
it "reads from GRAFANA_TOKEN environment variable" do
it "prefers DIFFDASH_GRAFANA_TOKEN when set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_TOKEN").and_return("diffdash-token")
allow(ENV).to receive(:[]).with("GRAFANA_TOKEN").and_return("secret-token")
expect(config.grafana_token).to eq("diffdash-token")
end

it "falls back to GRAFANA_TOKEN when DIFFDASH_GRAFANA_TOKEN is not set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_TOKEN").and_return(nil)
allow(ENV).to receive(:[]).with("GRAFANA_TOKEN").and_return("secret-token")
expect(config.grafana_token).to eq("secret-token")
end
end

describe "#grafana_folder_id" do
it "reads from GRAFANA_FOLDER_ID environment variable" do
it "prefers DIFFDASH_GRAFANA_FOLDER_ID when set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_FOLDER_ID").and_return("456")
allow(ENV).to receive(:[]).with("GRAFANA_FOLDER_ID").and_return("123")
expect(config.grafana_folder_id).to eq("456")
end

it "falls back to GRAFANA_FOLDER_ID when DIFFDASH_GRAFANA_FOLDER_ID is not set" do
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:[]).with("DIFFDASH_GRAFANA_FOLDER_ID").and_return(nil)
allow(ENV).to receive(:[]).with("GRAFANA_FOLDER_ID").and_return("123")
expect(config.grafana_folder_id).to eq("123")
end
Expand Down
5 changes: 4 additions & 1 deletion spec/diffdash/renderers/grafana_contract_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def stringify_keys(value)
logs: [signal],
metrics: [],
traces: [],
metadata: { time_range: { from: "now-1h", to: "now" } }
metadata: {
time_range: { from: "now-1h", to: "now" },
change_set: { branch_name: "contract-dashboard" }
}
)
renderer = Diffdash::Outputs::Grafana.new(
title: "contract-dashboard",
Expand Down
37 changes: 33 additions & 4 deletions spec/diffdash/renderers/grafana_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
describe "#render" do
context "with empty signals" do
subject(:renderer) { described_class.new(title: "Empty Dashboard", folder_id: nil) }
let(:bundle) { Diffdash::Engine::SignalBundle.new(metadata: { time_range: { from: "now-1h", to: "now" } }) }
let(:bundle) do
Diffdash::Engine::SignalBundle.new(
metadata: {
time_range: { from: "now-1h", to: "now" },
change_set: { branch_name: "feature/pr-123" }
}
)
end

it "returns valid Grafana JSON structure" do
result = renderer.render(bundle)
Expand Down Expand Up @@ -296,7 +303,14 @@

context "dashboard metadata" do
subject(:renderer) { described_class.new(title: "Test Dashboard", folder_id: nil) }
let(:bundle) { Diffdash::Engine::SignalBundle.new(metadata: { time_range: { from: "now-1h", to: "now" } }) }
let(:bundle) do
Diffdash::Engine::SignalBundle.new(
metadata: {
time_range: { from: "now-1h", to: "now" },
change_set: { branch_name: "feature/pr-123" }
}
)
end

it "includes Grafana tags" do
result = renderer.render(bundle)
Expand All @@ -321,8 +335,23 @@
result = renderer.render(bundle)
annotations = result[:dashboard][:annotations][:list]

expect(annotations.size).to eq(1)
expect(annotations.first[:name]).to eq("Deployments")
expect(annotations.size).to eq(2)
base = annotations.first
pr = annotations.last

expect(base[:name]).to eq("Deployments")
expect(base[:datasource]).to eq(type: "prometheus", uid: "${datasource}")
expect(base[:enable]).to be true
expect(base[:expr]).to eq("changes(deploy_timestamp[5m]) > 0")
expect(base[:tagKeys]).to eq("app,env")
expect(base[:titleFormat]).to eq("Deploy")

expect(pr[:name]).to eq("PR Deployments")
expect(pr[:datasource]).to eq(type: "prometheus", uid: "${datasource}")
expect(pr[:enable]).to be true
expect(pr[:expr]).to eq("changes(deploy_timestamp{branch=\"feature/pr-123\"}[5m]) > 0")
expect(pr[:tagKeys]).to eq("app,env,branch")
expect(pr[:titleFormat]).to eq("PR Deploy")
end

it "sets schema version" do
Expand Down
11 changes: 11 additions & 0 deletions spec/fixtures/grafana/dashboard_v1_fixture.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@
"expr": "changes(deploy_timestamp[5m]) > 0",
"tagKeys": "app,env",
"titleFormat": "Deploy"
},
{
"name": "PR Deployments",
"datasource": {
"type": "prometheus",
"uid": "${datasource}"
},
"enable": true,
"expr": "changes(deploy_timestamp{branch=\"contract-dashboard\"}[5m]) > 0",
"tagKeys": "app,env,branch",
"titleFormat": "PR Deploy"
}
]
}
Expand Down
Loading