From e76a488376983999b9ee6821f7963b67b85b52ce Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 13 Mar 2026 14:00:00 +0100 Subject: [PATCH 01/28] Initial commit based on https://github.com/grafana/terraform-provider-grafana/pull/2191 --- docs/resources/integration.md | 123 ++++++ .../resources/grafana_integration/import.sh | 1 + .../resources/grafana_integration/resource.tf | 42 ++ internal/common/resource.go | 1 + .../resources/integrations/api-quick-doc.md | 96 +++++ internal/resources/integrations/client.go | 371 ++++++++++++++++++ internal/resources/integrations/models.go | 102 +++++ .../integrations/resource_integration.go | 314 +++++++++++++++ internal/resources/integrations/resources.go | 9 + pkg/provider/resources.go | 2 + 10 files changed, 1061 insertions(+) create mode 100644 docs/resources/integration.md create mode 100644 examples/resources/grafana_integration/import.sh create mode 100644 examples/resources/grafana_integration/resource.tf create mode 100644 internal/resources/integrations/api-quick-doc.md create mode 100644 internal/resources/integrations/client.go create mode 100644 internal/resources/integrations/models.go create mode 100644 internal/resources/integrations/resource_integration.go create mode 100644 internal/resources/integrations/resources.go diff --git a/docs/resources/integration.md b/docs/resources/integration.md new file mode 100644 index 000000000..c34db344a --- /dev/null +++ b/docs/resources/integration.md @@ -0,0 +1,123 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "grafana_integration Resource - terraform-provider-grafana" +subcategory: "Cloud" +description: |- + Manages Grafana Cloud integrations. + Official documentation https://grafana.com/docs/grafana-cloud/data-configuration/integrations/ + Required access policy scopes: + folders:readfolders:writedashboards:readdashboards:write + Note: This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. +--- + +# grafana_integration (Resource) + +Manages Grafana Cloud integrations. + +* [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) + +Required access policy scopes: + +* folders:read +* folders:write +* dashboards:read +* dashboards:write + +**Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. + +## Example Usage + +```terraform +# Install Docker integration with logs and alerts enabled +resource "grafana_integration" "docker" { + slug = "docker" + + configuration { + configurable_logs { + logs_disabled = false + } + configurable_alerts { + alerts_disabled = false + } + } +} + +# Install Linux Node integration with logs enabled but alerts disabled +resource "grafana_integration" "linux_node" { + slug = "linux-node" + + configuration { + configurable_logs { + logs_disabled = false + } + configurable_alerts { + alerts_disabled = true + } + } +} + +# Install Windows integration with minimal configuration +resource "grafana_integration" "windows" { + slug = "windows-exporter" +} + +# Output integration information +output "docker_integration" { + value = { + name = grafana_integration.docker.name + version = grafana_integration.docker.version + installed = grafana_integration.docker.installed + dashboard_folder = grafana_integration.docker.dashboard_folder + } +} +``` + + +## Schema + +### Required + +- `slug` (String) The slug of the integration to install (e.g., 'docker', 'linux-node'). + +### Optional + +- `configuration` (Block List, Max: 1) Configuration options for the integration. (see [below for nested schema](#nestedblock--configuration)) + +### Read-Only + +- `dashboard_folder` (String) The dashboard folder associated with this integration. +- `id` (String) The ID of this resource. +- `installed` (Boolean) Whether the integration is currently installed. +- `name` (String) The display name of the integration. +- `version` (String) The version of the installed integration. + + +### Nested Schema for `configuration` + +Optional: + +- `configurable_alerts` (Block List, Max: 1) Alerts configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_alerts)) +- `configurable_logs` (Block List, Max: 1) Logs configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_logs)) + + +### Nested Schema for `configuration.configurable_alerts` + +Optional: + +- `alerts_disabled` (Boolean) Whether to disable alerts for this integration. Defaults to `false`. + + + +### Nested Schema for `configuration.configurable_logs` + +Optional: + +- `logs_disabled` (Boolean) Whether to disable logs collection for this integration. Defaults to `false`. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import grafana_integration.name "{{ slug }}" +``` diff --git a/examples/resources/grafana_integration/import.sh b/examples/resources/grafana_integration/import.sh new file mode 100644 index 000000000..07389126f --- /dev/null +++ b/examples/resources/grafana_integration/import.sh @@ -0,0 +1 @@ +terraform import grafana_integration.name "{{ slug }}" diff --git a/examples/resources/grafana_integration/resource.tf b/examples/resources/grafana_integration/resource.tf new file mode 100644 index 000000000..4db78c6bf --- /dev/null +++ b/examples/resources/grafana_integration/resource.tf @@ -0,0 +1,42 @@ +# Install Docker integration with logs and alerts enabled +resource "grafana_integration" "docker" { + slug = "docker" + + configuration { + configurable_logs { + logs_disabled = false + } + configurable_alerts { + alerts_disabled = false + } + } +} + +# Install Linux Node integration with logs enabled but alerts disabled +resource "grafana_integration" "linux_node" { + slug = "linux-node" + + configuration { + configurable_logs { + logs_disabled = false + } + configurable_alerts { + alerts_disabled = true + } + } +} + +# Install Windows integration with minimal configuration +resource "grafana_integration" "windows" { + slug = "windows-exporter" +} + +# Output integration information +output "docker_integration" { + value = { + name = grafana_integration.docker.name + version = grafana_integration.docker.version + installed = grafana_integration.docker.installed + dashboard_folder = grafana_integration.docker.dashboard_folder + } +} diff --git a/internal/common/resource.go b/internal/common/resource.go index 88fde8de1..125f93e37 100644 --- a/internal/common/resource.go +++ b/internal/common/resource.go @@ -28,6 +28,7 @@ var ( CategoryFrontendO11y ResourceCategory = "Frontend Observability" CategoryAsserts ResourceCategory = "Knowledge Graph" CategoryK6 ResourceCategory = "k6" + CategoryIntegration ResourceCategory = "Integration" ) type ResourceCommon struct { diff --git a/internal/resources/integrations/api-quick-doc.md b/internal/resources/integrations/api-quick-doc.md new file mode 100644 index 000000000..1c92101e2 --- /dev/null +++ b/internal/resources/integrations/api-quick-doc.md @@ -0,0 +1,96 @@ +base path: `` + + +# List integrations +- method GET +- path /grafana-easystart-app/integrations-api-editor/integrations +- parameters: + - `installed=true` for only the installed one +- expected: + - code 200 + - example body: + ``` + {"data":{"docker":{"name":"Docker","slug":"docker","version":"1.3.4","overview":"Docker is a popular open-source platform that enables developers to create, deploy, and run applications in a virtualized environment called a container. It allows developers to package an application with all its dependencies, libraries, and other components required to run it, into a single container image. The Docker integration collects metrics and logs from a Docker instance and provides useful pre-built dashboards to monitor them. \n\n**Links** \n[Docker mixin](https://github.com/grafana/jsonnet-libs/tree/master/docker-mixin) \n[Docker docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-docker/) \n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png"},"type":"agent","installation":{"version":"1.3.4","installed_on":"2025-03-28T18:26:54Z"},"search_keywords":["container","deployment","docker"],"categories":["Servers and VMs"],"dashboard_folder":"Integration - Docker","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":"integrations-docker"},"linux-node":{"name":"Linux Server","slug":"linux-node","version":"1.6.1","overview":"Linux is a family of open-source Unix-like operating systems based on the Linux kernel. Linux is the leading operating system on servers, and is one of the most prominent examples of free and open-source software collaboration.\n\nLinux Server integration for Grafana Cloud enables you to collect metrics related to the operating system running on a node, including aspects like CPU usage, load average, memory usage, and disk and networking I/O using node_exporter integration. It also allows you to use Grafana Alloy to scrape logs.\n\n**Links** \n[Linux Server mixin](https://github.com/grafana/node_exporter/tree/master/docs/node-mixin/lib/linux) \n[Linux Server docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-linux-node/) \n[Learning journey: Monitor a Linux server in Grafana Cloud](https://grafana.com/docs/learning-journeys/linux-server-integration/)\n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/linux.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/linux.png"},"type":"agent","installation":{"version":"1.6.1","installed_on":"2025-04-27T17:06:08Z"},"search_keywords":["operating system","server","linux","node","linux-node"],"categories":["Servers and VMs","Operating System"],"dashboard_folder":"Integration - Linux Node","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":"integrations-linux-node"},"traefik":{"name":"Traefik","slug":"traefik","version":"0.0.6","overview":"Traefik is a dynamic load balancer designed for ease of configuration, especially in dynamic environments. It supports automatic discovery of services, metrics, tracing, and has Let's Encrypt support out of the box. Traefik provides a “ready to go” system for serving production traffic with these additions.\n\n**Links** \n[Traefik mixin](https://github.com/grafana/jsonnet-libs/tree/master/traefik-mixin) \n[Traefik docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-traefik/)\n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/traefik.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/traefik.png"},"type":"agent","installation":{"version":"0.0.6","installed_on":"2023-11-04T15:32:35Z"},"search_keywords":["load","balancer","networking","traefik"],"categories":["Networking"],"dashboard_folder":"Integration - Traefik","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":""},"windows-exporter":{"name":"Windows","slug":"windows-exporter","version":"1.1.5","overview":"Monitor Windows instances using Grafana Alloy. The integration comes with pre installed dashboards, which give an overview of your Windows' fleet at once, a single host overview, as well as additional dashboards that provide more metrics for further system performance analysis.\nThe integration also provides dashboard showing Windows event logs.\n\n**Links** \n[Windows mixin](https://github.com/grafana/jsonnet-libs/tree/master/windows-mixin) \n[Windows docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-windows-exporter/) \n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/windows.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/windows.png"},"type":"agent","installation":{"version":"1.1.5","installed_on":"2025-05-15T09:38:02Z"},"search_keywords":["operating system","windows","microsoft","windows-exporter"],"categories":["Operating System","Servers and VMs"],"dashboard_folder":"Integration - Windows Exporter","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":"integrations-windows-exporter"}}} + ``` + +# Get one integration state +- method GET +- path /grafana-easystart-app/integrations-api-editor/integrations/{slug} +- parameters: + - `{slug}` the integration +- expected: + - code 200 + - example body: + ``` + {"data":{"name":"Docker","slug":"docker","version":"1.3.4","overview":"Docker is a popular open-source platform that enables developers to create, deploy, and run applications in a virtualized environment called a container. It allows developers to package an application with all its dependencies, libraries, and other components required to run it, into a single container image. The Docker integration collects metrics and logs from a Docker instance and provides useful pre-built dashboards to monitor them. \n\n**Links** \n[Docker mixin](https://github.com/grafana/jsonnet-libs/tree/master/docker-mixin) \n[Docker docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-docker/) \n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png"},"type":"agent","installation":{"version":"1.3.4","installed_on":"2025-03-28T18:26:54Z","configuration":{"configurable_logs":{"logs_disabled":false},"configurable_alerts":{"alerts_disabled":false}}},"search_keywords":null,"categories":["Servers and VMs"],"dashboard_folder":"Integration - Docker","has_update":false,"metrics_check_query":"vector(1) and on() ((count(up{job=\"integrations/docker\"} == 1) \u003e 0) and (absent(absent(machine_scrape_error{job=\"integrations/docker\"})))) or vector(0) and on() ((count(up{job=\"integrations/docker\"}) \u003e 0) and (absent(machine_scrape_error{job=\"integrations/docker\"})))","logs_check_query":"sum(count_over_time({job=\"integrations/docker\"}[5m]))","rule_namespace":"","agent_configuration":{"supported_platforms":["linux"]},"metrics":{"status":"available","available_metrics":[{"name":"container_cpu_usage_seconds_total","type":"counter","description":"Cumulative cpu time consumed in seconds."},{"name":"container_fs_reads_total","type":"counter","description":"Cumulative count of reads completed"},{"name":"container_fs_usage_bytes","type":"gauge","description":"Number of bytes that are consumed by the container on this filesystem."},{"name":"container_fs_writes_total","type":"counter","description":"Cumulative count of writes completed"},{"name":"container_last_seen","type":"gauge","description":"Last time a container was seen by the exporter"},{"name":"container_memory_usage_bytes","type":"gauge","description":"Current memory usage in bytes, including all memory regardless of when it was accessed"},{"name":"container_network_receive_bytes_total","type":"counter","description":"Cumulative count of bytes received"},{"name":"container_network_receive_errors_total","type":"counter","description":"Cumulative count of errors encountered while receiving"},{"name":"container_network_receive_packets_dropped_total","type":"counter","description":"Cumulative count of packets dropped while receiving"},{"name":"container_network_transmit_bytes_total","type":"counter","description":"Cumulative count of bytes transmitted"},{"name":"container_network_transmit_errors_total","type":"counter","description":"Cumulative count of errors encountered while transmitting"},{"name":"container_network_transmit_packets_dropped_total","type":"counter","description":"Cumulative count of packets dropped while transmitting"},{"name":"container_spec_memory_reservation_limit_bytes","type":"gauge","description":"Memory reservation limit for the container."},{"name":"machine_memory_bytes","type":"gauge","description":"Amount of memory installed on the machine."},{"name":"machine_scrape_error","type":"gauge","description":"1 if there was an error while getting machine metrics, 0 otherwise."},{"name":"up"}]},"rules":{"status":"available","available_rules":[{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:usage"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:gauge"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:total"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:total"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:gauge"}]},"alerts":{"status":"not_supported"},"logs":{"status":"available"},"traces":{"status":"not_supported"},"asserts":{"status":"available"},"dashboards":{"status":"available","screenshots":[{"url":"https://storage.googleapis.com/grafanalabs-integration-assets/docker/screenshots/docker-1.2.png","sort_order":1,"description":"Overview \u0026 Compute"},{"url":"https://storage.googleapis.com/grafanalabs-integration-assets/docker/screenshots/docker-logs-1.2.png","sort_order":2,"description":"Logs"}],"folder_name":"Integration - Docker"},"configuration_defaults":{"enable_logs_toggle":true,"enable_alerts_toggle":false,"configurable_logs_defaults":{"logs_disabled":false},"configurable_alerts_defaults":{"alerts_disabled":false}}}} + ``` + +# Install one integration +## Get the dashboards +- method POST +- path /api/plugin-proxy/grafana-easystart-app/integrations-api-admin/integrations/{slug}/dashboards +- parameters: + - `{slug}` the integration +- body example: + ``` + {"configuration":{"configurable_logs":{"logs_disabled":false}}} + ``` +- expected: + - code 200 + - example body + ``` + {"data":[{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows if metrics are being received for the selected time range.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"mappings":[{"options":{"match":"null","result":{"color":"light-red","index":0,"text":"No metrics received - Check configuration"}},"type":"special"},{"options":{"from":0,"result":{"color":"light-red","index":1,"text":"Failed to collect metrics"},"to":0},"type":"range"},{"options":{"from":1,"result":{"color":"light-green","index":2,"text":"Receiving metrics"},"to":1000000},"type":"range"}],"noValue":"No data","unit":"string"}},"gridPos":{"h":2,"w":8,"x":0,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"]}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"title":"Metrics","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the timestamp of the latest metrics received for this integration in the last 24 hours.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"No data","unit":"dateTimeFromNow"}},"gridPos":{"h":2,"w":8,"x":8,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"],"fields":"Time"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"timeFrom":"now-24h","title":"Latest metrics received","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the installed version of this integration.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"1.0.1","unit":"string"}},"gridPos":{"h":2,"w":8,"x":16,"y":0},"pluginVersion":"v10.0.0","title":"Integration version","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of clusters that are reporting metrics from ActiveMQ.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":3},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Clusters","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of broker instances across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":3},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Brokers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of message producers active on destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":3},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":3},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been sent to destinations in a cluster","fieldConfig":{"defaults":{"color":{"fixedColor":"#C8F2C2","mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":9},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Enqueue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been acknowledged (and removed) from destinations in a cluster.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":9},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Dequeue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of temporary memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":0,"y":17},"id":8,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"text":{},"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_temp_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average temporary memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of store memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":8,"y":17},"id":9,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_store_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average store memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of broker memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":16,"y":17},"id":10,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average broker memory usage","type":"bargauge"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ cluster overview","uid":"apache-activemq-cluster-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of memory used by both topics and queues across brokers.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":0},"id":2,"options":{"minVizHeight":75,"minVizWidth":75,"orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Average broker memory usage","type":"gauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of store memory used by both topics and queues across brokers.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":0},"id":3,"options":{"minVizHeight":75,"minVizWidth":75,"orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_store_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Average store memory usage","type":"gauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of temporary memory used by both topics and queues across brokers.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":0},"id":4,"options":{"minVizHeight":75,"minVizWidth":75,"orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_temp_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Average temporary memory usage","type":"gauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Recent number of unacknowledged messages on the broker.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":0},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (increase(activemq_message_total{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"__auto"}],"title":"Unacknowledged messages / $__interval","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Firing alerts for Apache ActiveMQ environment.","gridPos":{"h":6,"w":12,"x":0,"y":4},"id":6,"options":{"alertInstanceLabelFilter":"{job=~\"${job:regex}\", cluster=~\"${cluster:regex}\", activemq_cluster=~\"${activemq_cluster:regex}\", instance=~\"${instance:regex}\"}","alertName":"","dashboardAlerts":false,"datasource":{"uid":"${prometheus_datasource}"},"groupBy":[],"groupMode":"default","maxItems":5,"sortOrder":1,"stateFilter":{"error":true,"firing":true,"noData":true,"normal":true,"pending":true},"viewMode":"list"},"targets":[],"title":"ActiveMQ alerts","type":"alertlist"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of producers attached to destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":6,"w":6,"x":12,"y":4},"id":7,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}) + sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to destinations on the broker.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":6,"w":6,"x":18,"y":4},"id":8,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}) + sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages on queue destinations, including any that have been dispatched but not acknowledged.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":10},"id":9,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (activemq_queue_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Queue size","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of memory being used by topic and queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":10},"id":10,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (activemq_queue_memory_percent_usage{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (activemq_topic_memory_percent_usage{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Destination memory usage","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been sent to the destination.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":18},"id":11,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Enqueue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been acknowledged (and removed) from the destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":18},"id":12,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster) (increase(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster) (increase(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Dequeue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average time a message was held across all destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"ms"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":26},"id":13,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, instance, job) (activemq_queue_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, instance, job) (activemq_topic_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Average enqueue time","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages across destinations that are expired.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":26},"id":14,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_queue_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_topic_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Expired messages / $__interval","type":"timeseries"},{"collapsed":false,"datasource":{"uid":"${prometheus_datasource}"},"gridPos":{"h":1,"w":24,"x":0,"y":34},"id":15,"targets":[],"title":"JVM resources","type":"row"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The time spent performing recent garbage collections","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]},"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":35},"id":16,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"jvm_gc_duration_seconds{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Garbage collection duration","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The recent increase in the number of garbage collection events for the JVM.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":35},"id":17,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"increase(jvm_gc_collection_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", name=\"G1 Young Generation\"}[$__interval:])","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Garbage collection count / $__interval","type":"timeseries"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Instance","multi":true,"name":"instance","options":[],"query":"label_values(activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"},instance)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ instance overview","uid":"apache-activemq-instance-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"editable":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows if logs are being received for the selected time range.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"mappings":[{"options":{"match":"null","result":{"color":"light-yellow","index":0,"text":"Failed to collect logs or no logs available"}},"type":"special"},{"options":{"from":0,"result":{"color":"light-yellow","index":1,"text":"Failed to collect logs or no logs available"},"to":0},"type":"range"},{"options":{"from":1,"result":{"color":"light-green","index":2,"text":"Receiving logs"},"to":1000000},"type":"range"}],"noValue":"No data","unit":"string"}},"gridPos":{"h":2,"w":8,"x":0,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"]}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"$loki_datasource"},"expr":"sum(count_over_time({job=~\"$job\"}[5m]))\n"}],"title":"Logs","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the timestamp of the latest logs received for this integration in the last 24 hours.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"No data","unit":"dateTimeFromNow"}},"gridPos":{"h":2,"w":8,"x":8,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"],"fields":"Time"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"$loki_datasource"},"expr":"sum(count_over_time({job=~\"$job\"}[5m]))\n"}],"timeFrom":"now-24h","title":"Latest logs received","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the installed version of this integration.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"1.0.1","unit":"string"}},"gridPos":{"h":2,"w":8,"x":16,"y":0},"pluginVersion":"v10.0.0","title":"Integration version","type":"stat"},{"datasource":{"type":"loki","uid":"${loki_datasource}"},"description":"Logs volume grouped by \"level\" label.","fieldConfig":{"defaults":{"custom":{"drawStyle":"bars","fillOpacity":50,"stacking":{"mode":"normal"}},"unit":"none"},"overrides":[{"matcher":{"id":"byRegexp","options":"(E|e)merg|(F|f)atal|(A|a)lert|(C|c)rit.*"},"properties":[{"id":"color","value":{"fixedColor":"purple","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(E|e)(rr.*|RR.*)"},"properties":[{"id":"color","value":{"fixedColor":"red","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(W|w)(arn.*|ARN.*|rn|RN)"},"properties":[{"id":"color","value":{"fixedColor":"orange","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(N|n)(otice|ote)|(I|i)(nf.*|NF.*)"},"properties":[{"id":"color","value":{"fixedColor":"green","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"dbg.*|DBG.*|(D|d)(EBUG|ebug)"},"properties":[{"id":"color","value":{"fixedColor":"blue","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(T|t)(race|RACE)"},"properties":[{"id":"color","value":{"fixedColor":"light-blue","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"logs"},"properties":[{"id":"color","value":{"fixedColor":"text","mode":"fixed"}}]}]},"gridPos":{"h":6,"w":24,"y":3},"id":1,"interval":"30s","options":{"tooltip":{"mode":"multi","sort":"desc"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"${loki_datasource}"},"expr":"sum by (level) (count_over_time({job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\",level=~\"$level\"}\n|~ \"$regex_search\"\n\n[$__interval]))\n","legendFormat":"{{ level }}"}],"title":"Logs volume","transformations":[{"id":"renameByRegex","options":{"regex":"Value","renamePattern":"logs"}}],"type":"timeseries"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"gridPos":{"h":18,"w":24,"y":3},"id":2,"options":{"dedupStrategy":"exact","enableLogDetails":true,"prettifyLogMessage":true,"showTime":false,"wrapLogMessage":true},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"${loki_datasource}"},"expr":"{job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\",level=~\"$level\"} \n|~ \"$regex_search\"\n\n\n"}],"title":"Logs","type":"logs"}],"refresh":"30s","schemaVersion":36,"tags":["apache-activemq-integration"],"templating":{"list":[{"label":"Loki data source","name":"loki_datasource","query":"loki","regex":"(?!grafanacloud.+usage-insights|grafanacloud.+alert-state-history).+","type":"datasource"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Job","multi":true,"name":"job","query":"label_values({job=~\"integrations/apache-activemq\"}, job)","refresh":2,"sort":1,"type":"query"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Activemq_cluster","multi":true,"name":"activemq_cluster","query":"label_values({job=~\"integrations/apache-activemq\",job=~\"$job\"}, activemq_cluster)","refresh":2,"sort":1,"type":"query"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Instance","multi":true,"name":"instance","query":"label_values({job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\"}, instance)","refresh":2,"sort":1,"type":"query"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Level","multi":true,"name":"level","query":"label_values({job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\"}, level)","refresh":2,"sort":1,"type":"query"},{"current":{"selected":false,"text":"","value":""},"label":"Regex search","name":"regex_search","options":[{"selected":true,"text":"","value":""}],"query":"","type":"textbox"}]},"time":{"from":"now-30m","to":"now"},"timezone":"utc","title":"Apache ActiveMQ logs","uid":"apache-activemq-logs"},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of queues connected with the broker instance.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":0},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_queue_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Queues","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages in queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]}},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":0},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Queue size","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of producers attached to queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":0},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":0},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages sent to queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":4},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by enqueue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have been acknowledged (and removed) from queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":4},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster) ($k_selector, rate(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by dequeue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average time a message was held on queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"ms"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":12},"id":8,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster) ($k_selector, activemq_queue_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by average enqueue time","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have expired on queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":12},"id":9,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_queue_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by expired message rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average message size on queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"axisSoftMin":0,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":7,"w":24,"x":0,"y":20},"id":10,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_queue_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by average message size","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Summary of queues showing queue name, enqueue and dequeue rate, average enqueue time, and average message size.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"custom":{"align":"center","cellOptions":{"type":"auto"},"inspect":false},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]}},"overrides":[{"matcher":{"id":"byName","options":"Average message size"},"properties":[{"id":"unit","value":"decbytes"}]},{"matcher":{"id":"byName","options":"Enqueue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Dequeue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Average enqueue time"},"properties":[{"id":"unit","value":"ms"}]},{"matcher":{"id":"byName","options":"ActiveMQ cluster"},"properties":[{"id":"links","value":[{"title":"Cluster link","url":"d/apache-activemq-cluster-overview?var-activemq_cluster=${__data.fields.activemq_cluster}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]},{"matcher":{"id":"byName","options":"Instance"},"properties":[{"id":"links","value":[{"title":"Instance link","url":"d/apache-activemq-instance-overview?var-instance=${__data.fields.instance}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]}]},"gridPos":{"h":7,"w":24,"x":0,"y":27},"id":11,"options":{"cellHeight":"sm","footer":{"countRows":false,"fields":"","reducer":["sum"],"show":false},"showHeader":true,"sortBy":[]},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval:])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval:])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_queue_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_queue_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"}],"title":"Queue summary","transformations":[{"id":"joinByField","options":{"byField":"destination","mode":"outer"}},{"id":"organize","options":{"indexByName":{},"renameByName":{"Time 1":"","Value #A":"Enqueue rate","Value #B":"Dequeue rate","Value #C":"Average enqueue time","Value #D":"Average message size","activemq_cluster 1":"ActiveMQ cluster","destination":"Destination","instance 1":"Instance"}}},{"id":"filterFieldsByName","options":{"include":{"names":["ActiveMQ cluster","Instance","Enqueue rate","Dequeue rate","Average enqueue time","Average message size","Destination"]}}}],"type":"table"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Instance","multi":true,"name":"instance","options":[],"query":"label_values(activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"},instance)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":"","current":{"text":"4","value":"4"},"hide":0,"includeAll":false,"label":"Top queue count","multi":false,"name":"k_selector","options":[{"text":"2","value":"2"},{"text":"4","value":"4"},{"text":"6","value":"6"},{"text":"8","value":"8"},{"text":"10","value":"10"}],"query":"2,4,6,8,10","refresh":0,"type":"custom"},{"current":{"selected":false,"text":"","value":""},"label":"Queue by name","name":"name","query":"","type":"textbox"}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ queue overview","uid":"apache-activemq-queue-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of topics connected with the broker instance.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":0},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_topic_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Topics","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of producers attached to topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":0},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":0},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average number of consumers per topic.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":0},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Average consumers per topic","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages are sent to topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":4},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by enqueue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have been acknowledged (and removed) from topic destinations","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":4},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by dequeue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average time a message was held across all topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"ms"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":12},"id":8,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_topic_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by average enqueue time","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have expired on topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":12},"id":9,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_topic_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by expired message rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to the most active/used topics.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":20},"id":10,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":true,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by consumers","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average message size on topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"axisSoftMin":0,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":20},"id":11,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_topic_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by average message size","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Summary of topics showing topic name, enqueue and dequeue rate, average enqueue time, and average message size.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"custom":{"align":"left","cellOptions":{"type":"auto"},"inspect":false},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]}},"overrides":[{"matcher":{"id":"byName","options":"Enqueue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Dequeue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Average enqueue time"},"properties":[{"id":"unit","value":"ms"}]},{"matcher":{"id":"byName","options":"Average message size"},"properties":[{"id":"unit","value":"decbytes"}]},{"matcher":{"id":"byName","options":"ActiveMQ cluster"},"properties":[{"id":"links","value":[{"title":"Cluster link","url":"d/apache-activemq-cluster-overview?var-activemq_cluster=${__data.fields.activemq_cluster}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]},{"matcher":{"id":"byName","options":"Instance"},"properties":[{"id":"links","value":[{"title":"Instance link","url":"d/apache-activemq-instance-overview?var-instance=${__data.fields.instance}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]}]},"gridPos":{"h":8,"w":24,"x":0,"y":28},"id":12,"options":{"cellHeight":"sm","footer":{"countRows":false,"fields":"","reducer":["sum"],"show":false},"showHeader":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_topic_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_topic_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"}],"title":"Topic summary","transformations":[{"id":"joinByField","options":{"byField":"destination","mode":"outer"}},{"id":"organize","options":{"indexByName":{},"renameByName":{"Time 3":"","Value #A":"Enqueue rate","Value #B":"Dequeue rate","Value #C":"Average enqueue time","Value #D":"Average message size","activemq_cluster 1":"ActiveMQ cluster","destination":"Destination","instance 1":"Instance"}}},{"id":"filterFieldsByName","options":{"include":{"names":["ActiveMQ cluster","Instance","Enqueue rate","Dequeue rate","Average enqueue time","Average message size","Destination"]}}}],"type":"table"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Instance","multi":true,"name":"instance","options":[],"query":"label_values(activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\",activemq_cluster=~\"$activemq_cluster\"},instance)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":"","current":{"text":"4","value":"4"},"hide":0,"includeAll":false,"label":"Top topic count","multi":false,"name":"k_selector","options":[{"text":"2","value":"2"},{"text":"4","value":"4"},{"text":"6","value":"6"},{"text":"8","value":"8"},{"text":"10","value":"10"}],"query":"2,4,6,8,10","refresh":0,"type":"custom"},{"current":{"selected":false,"text":"","value":""},"label":"Topic by name","name":"name","query":"","type":"textbox"}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ topic overview","uid":"apache-activemq-topic-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true}]} + ``` + +## Create the folders +- method POST +- path /api/folders +- body example: + ``` + {"title":"Integration - Apache ActiveMQ","uid":"integration---apache-activemq"} + ``` +- expected: + - code 200 + - body example + ``` + {"id":224,"uid":"integration---apache-activemq","orgId":1,"title":"Integration - Apache ActiveMQ","url":"/dashboards/f/integration---apache-activemq/integration-apache-activemq","hasAcl":false,"canSave":true,"canEdit":true,"canAdmin":true,"canDelete":true,"createdBy":"clement2b5f","created":"2025-05-23T15:17:59Z","updatedBy":"clement2b5f","updated":"2025-05-23T15:17:59Z","version":1} + ``` + +## Add the dashboard (Iterate for each dashboard of the integration) +- method POST +- path /api/dashboards/db +- body example: + ``` + {"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows if metrics are being received for the selected time range.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"mappings":[{"options":{"match":"null","result":{"color":"light-red","index":0,"text":"No metrics received - Check configuration"}},"type":"special"},{"options":{"from":0,"result":{"color":"light-red","index":1,"text":"Failed to collect metrics"},"to":0},"type":"range"},{"options":{"from":1,"result":{"color":"light-green","index":2,"text":"Receiving metrics"},"to":1000000},"type":"range"}],"noValue":"No data","unit":"string"}},"gridPos":{"h":2,"w":8,"x":0,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"]}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"title":"Metrics","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the timestamp of the latest metrics received for this integration in the last 24 hours.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"No data","unit":"dateTimeFromNow"}},"gridPos":{"h":2,"w":8,"x":8,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"],"fields":"Time"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"timeFrom":"now-24h","title":"Latest metrics received","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the installed version of this integration.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"1.0.1","unit":"string"}},"gridPos":{"h":2,"w":8,"x":16,"y":0},"pluginVersion":"v10.0.0","title":"Integration version","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of clusters that are reporting metrics from ActiveMQ.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":3},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Clusters","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of broker instances across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":3},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Brokers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of message producers active on destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":3},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":3},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been sent to destinations in a cluster","fieldConfig":{"defaults":{"color":{"fixedColor":"#C8F2C2","mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":9},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Enqueue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been acknowledged (and removed) from destinations in a cluster.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":9},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Dequeue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of temporary memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":0,"y":17},"id":8,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"text":{},"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_temp_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average temporary memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of store memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":8,"y":17},"id":9,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_store_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average store memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of broker memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":16,"y":17},"id":10,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average broker memory usage","type":"bargauge"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ cluster overview","uid":"apache-activemq-cluster-overview","version":0},"folderUid":"integration---apache-activemq","overwrite":true,"message":"creating dashboard from the Cloud Connections plugin"} + ``` +- expected: + - code 200 + - example body + ``` + {"folderUid":"integration---apache-activemq","id":235,"slug":"apache-activemq-cluster-overview","status":"success","uid":"apache-activemq-cluster-overview","url":"/d/apache-activemq-cluster-overview/apache-activemq-cluster-overview","version":1} + ``` + +## Install the integration +- method POST +- path /api/plugin-proxy/grafana-easystart-app/integrations-api-admin/integrations/{slug}/install +- parameters: + - `{slug}` the integration +- body example: + ``` + {"configuration":{"configurable_logs":{"logs_disabled":false},"configurable_alerts":{"alerts_disabled":false}}} + ``` +- expected: + - code 204 + - no body + +# Uninstall one integration +- method POST +- path /api/plugin-proxy/grafana-easystart-app/integrations-api-admin/integrations/{slug}/uninstall +- parameters: + - `{slug}` the integration +- expected: + - code 204 + - no body + + +> WARNING. I have a doubt that the webclient is actually the one deleting the folders \ No newline at end of file diff --git a/internal/resources/integrations/client.go b/internal/resources/integrations/client.go new file mode 100644 index 000000000..a7b805d59 --- /dev/null +++ b/internal/resources/integrations/client.go @@ -0,0 +1,371 @@ +package integrations + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/grafana/grafana-openapi-client-go/client/dashboards" + "github.com/grafana/grafana-openapi-client-go/client/folders" + "github.com/grafana/grafana-openapi-client-go/models" + "github.com/hashicorp/go-retryablehttp" +) + +const ( + // Base paths for different API operations + editorBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-editor" + adminBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-admin" + + defaultRetries = 3 + defaultTimeout = 90 * time.Second +) + +// Client wraps the HTTP client for integrations API calls +type Client struct { + authToken string + client *http.Client + grafanaAPIHost string + userAgent string + defaultHeaders map[string]string + foldersClient folders.ClientService // Grafana OpenAPI client for folder operations + dashboardsClient dashboards.ClientService // Grafana OpenAPI client for dashboard operations +} + +// NewClient creates a new integrations client +func NewClient(grafanaAPIHost string, authToken string, client *http.Client, userAgent string, defaultHeaders map[string]string) (*Client, error) { + if client == nil { + retryClient := retryablehttp.NewClient() + retryClient.RetryMax = defaultRetries + client = retryClient.StandardClient() + client.Timeout = defaultTimeout + } + + return &Client{ + authToken: authToken, + client: client, + grafanaAPIHost: grafanaAPIHost, + userAgent: userAgent, + defaultHeaders: defaultHeaders, + foldersClient: nil, // Will be set by the resource when available + dashboardsClient: nil, // Will be set by the resource when available + }, nil +} + +// SetFoldersClient sets the Grafana OpenAPI folders client +func (c *Client) SetFoldersClient(foldersClient folders.ClientService) { + c.foldersClient = foldersClient +} + +// SetDashboardsClient sets the Grafana OpenAPI dashboards client +func (c *Client) SetDashboardsClient(dashboardsClient dashboards.ClientService) { + c.dashboardsClient = dashboardsClient +} + +// ListIntegrations retrieves all integrations, optionally filtering by installed status +func (c *Client) ListIntegrations(ctx context.Context, installed bool) (*ListIntegrationsResponse, error) { + path := fmt.Sprintf("%s/integrations", editorBasePath) + + // Add query parameter if filtering by installed + if installed { + path += "?installed=true" + } + + var response ListIntegrationsResponse + err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) + if err != nil { + return nil, fmt.Errorf("failed to list integrations: %w", err) + } + + return &response, nil +} + +// GetIntegration retrieves a specific integration by slug +func (c *Client) GetIntegration(ctx context.Context, slug string) (*GetIntegrationResponse, error) { + path := fmt.Sprintf("%s/integrations/%s", editorBasePath, url.PathEscape(slug)) + + var response GetIntegrationResponse + err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) + if err != nil { + return nil, fmt.Errorf("failed to get integration %s: %w", slug, err) + } + + return &response, nil +} + +// PostDashboards posts dashboards for an integration with the given configuration +func (c *Client) PostDashboards(ctx context.Context, slug string, config *InstallationConfig) (*GetDashboardsResponse, error) { + path := fmt.Sprintf("%s/integrations/%s/dashboards", adminBasePath, url.PathEscape(slug)) + + requestBody := InstallIntegrationRequest{ + Configuration: config, + } + + var response GetDashboardsResponse + err := c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, &response) + if err != nil { + return nil, fmt.Errorf("failed to post dashboards for integration %s: %w", slug, err) + } + + return &response, nil +} + +// generateFolderUID generates a folder UID from an integration slug +func (c *Client) generateFolderUID(slug string) string { + // Replace any special characters with dashes and add the integration prefix + uid := strings.ReplaceAll(slug, "_", "-") + uid = strings.ReplaceAll(uid, " ", "-") + return fmt.Sprintf("integration---%s", uid) +} + +// CreateFolder creates a folder using the Grafana OpenAPI client +func (c *Client) CreateFolder(ctx context.Context, title, uid string) error { + if c.foldersClient == nil { + return fmt.Errorf("folders client not available") + } + + body := models.CreateFolderCommand{ + Title: title, + UID: uid, + } + + _, err := c.foldersClient.CreateFolder(&body) + if err != nil { + return fmt.Errorf("failed to create folder %s: %w", title, err) + } + + return nil +} + +// DeleteFolder deletes a folder using the Grafana OpenAPI client +func (c *Client) DeleteFolder(ctx context.Context, uid string) error { + if c.foldersClient == nil { + return fmt.Errorf("folders client not available") + } + + _, err := c.foldersClient.DeleteFolder(folders.NewDeleteFolderParams().WithFolderUID(uid)) + if err != nil { + return fmt.Errorf("failed to delete folder %s: %w", uid, err) + } + + return nil +} + +// CreateDashboard creates a dashboard in the specified folder using the Grafana OpenAPI client +func (c *Client) CreateDashboard(ctx context.Context, dashboard Dashboard, folderUID string) error { + if c.dashboardsClient == nil { + // Fallback to raw HTTP request if OpenAPI client is not available + return c.createDashboardHTTP(ctx, dashboard, folderUID) + } + + // Make a copy of the dashboard data to avoid modifying the original + dashboardData := make(map[string]interface{}) + for k, v := range dashboard.Dashboard { + dashboardData[k] = v + } + + // Remove id from dashboard if present (similar to resource_dashboard.go) + delete(dashboardData, "id") + + // Convert the dashboard data to the proper format + dashboardCommand := models.SaveDashboardCommand{ + Dashboard: dashboardData, + FolderUID: folderUID, + Overwrite: dashboard.Overwrite, + Message: "creating dashboard from the Cloud Connections plugin", + } + + // Use the OpenAPI client + _, err := c.dashboardsClient.PostDashboard(&dashboardCommand) + if err != nil { + return fmt.Errorf("failed to create dashboard using OpenAPI client: %w", err) + } + + return nil +} + +// createDashboardHTTP creates a dashboard using raw HTTP requests as fallback +func (c *Client) createDashboardHTTP(ctx context.Context, dashboard Dashboard, folderUID string) error { + path := "/api/dashboards/db" + + // Make a copy of the dashboard data to avoid modifying the original + dashboardData := make(map[string]interface{}) + for k, v := range dashboard.Dashboard { + dashboardData[k] = v + } + + // Remove id from dashboard if present + delete(dashboardData, "id") + + requestBody := map[string]interface{}{ + "dashboard": dashboardData, + "folderUid": folderUID, + "overwrite": dashboard.Overwrite, + "message": "creating dashboard from the Cloud Connections plugin", + } + + err := c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, nil) + if err != nil { + return fmt.Errorf("failed to create dashboard using HTTP client: %w", err) + } + + return nil +} + +// InstallIntegration installs an integration with the given configuration using the new multi-step workflow +func (c *Client) InstallIntegration(ctx context.Context, slug string, config *InstallationConfig) error { + // Step 1: Get the integration details to get the folder name + integration, err := c.GetIntegration(ctx, slug) + if err != nil { + return fmt.Errorf("failed to get integration details: %w", err) + } + + // Step 2: Post dashboards (this prepares the dashboards) + dashboardsResponse, err := c.PostDashboards(ctx, slug, config) + if err != nil { + return fmt.Errorf("failed to post dashboards: %w", err) + } + + // Step 3: Create the folder + folderUID := c.generateFolderUID(slug) + folderTitle := integration.Data.DashboardFolder + err = c.CreateFolder(ctx, folderTitle, folderUID) + if err != nil { + // Check if it's a 412 error (folder already exists) + if strings.Contains(err.Error(), "412") || strings.Contains(err.Error(), "already exists") { + // Folder already exists, continue with dashboard creation + } else { + return fmt.Errorf("failed to create folder: %w", err) + } + } + + // Step 4: Add each dashboard to the folder + for i, dashboard := range dashboardsResponse.Data { + err = c.CreateDashboard(ctx, dashboard, folderUID) + if err != nil { + // If dashboard creation fails, try to clean up the folder + _ = c.DeleteFolder(ctx, folderUID) + return fmt.Errorf("failed to create dashboard %d: %w", i+1, err) + } + } + + // Step 5: Install the integration + path := fmt.Sprintf("%s/integrations/%s/install", adminBasePath, url.PathEscape(slug)) + + requestBody := InstallIntegrationRequest{ + Configuration: config, + } + + err = c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, nil) + if err != nil { + // If installation fails, try to clean up the folder + _ = c.DeleteFolder(ctx, folderUID) + return fmt.Errorf("failed to install integration %s: %w", slug, err) + } + + return nil +} + +// UninstallIntegration uninstalls an integration and deletes its folder +func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { + // Step 1: Uninstall the integration + path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) + + err := c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) + if err != nil { + return fmt.Errorf("failed to uninstall integration %s: %w", slug, err) + } + + // Step 2: Delete the folder + folderUID := c.generateFolderUID(slug) + err = c.DeleteFolder(ctx, folderUID) + if err != nil { + // Log the error but don't fail the uninstall if folder deletion fails + return fmt.Errorf("integration uninstalled but failed to delete folder %s: %w", folderUID, err) + } + + return nil +} + +// IsIntegrationInstalled checks if an integration is currently installed +func (c *Client) IsIntegrationInstalled(ctx context.Context, slug string) (bool, error) { + integration, err := c.GetIntegration(ctx, slug) + if err != nil { + return false, err + } + + return integration.Data.Installation != nil, nil +} + +var ( + ErrNotFound = fmt.Errorf("not found") + ErrUnauthorized = fmt.Errorf("request not authorized") +) + +func (c *Client) doAPIRequest(ctx context.Context, method string, path string, body any, responseData any) error { + parsedURL, err := url.Parse(c.grafanaAPIHost) + if err != nil { + return fmt.Errorf("failed to parse grafana API url: %w", err) + } + + var reqBodyBytes io.Reader + if body != nil { + bs, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("failed to marshal request body: %w", err) + } + reqBodyBytes = bytes.NewReader(bs) + } + + // Ensure no double slashes in URL construction + baseURL := strings.TrimSuffix(parsedURL.String(), "/") + fullURL := baseURL + path + req, err := http.NewRequestWithContext(ctx, method, fullURL, reqBodyBytes) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Add default headers + for k, v := range c.defaultHeaders { + req.Header.Add(k, v) + } + + // Add authentication + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.authToken)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add("User-Agent", c.userAgent) + + // Debug logging - add the full URL to error messages + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("failed to do request to %s: %w", fullURL, err) + } + + bodyContents, err := io.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + + switch { + case resp.StatusCode == http.StatusNotFound: + return ErrNotFound + case resp.StatusCode == http.StatusUnauthorized: + return ErrUnauthorized + case resp.StatusCode >= 400: + return fmt.Errorf("status: %d for URL: %s, body: %s", resp.StatusCode, fullURL, string(bodyContents)) + case responseData == nil || resp.StatusCode == http.StatusNoContent: + return nil + } + + err = json.Unmarshal(bodyContents, &responseData) + if err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + return nil +} diff --git a/internal/resources/integrations/models.go b/internal/resources/integrations/models.go new file mode 100644 index 000000000..c9d2acb5a --- /dev/null +++ b/internal/resources/integrations/models.go @@ -0,0 +1,102 @@ +package integrations + +import "time" + +// Integration represents an integration from the API +type Integration struct { + Name string `json:"name"` + Slug string `json:"slug"` + Version string `json:"version"` + Overview string `json:"overview"` + Logo Logo `json:"logo"` + Type string `json:"type"` + Installation *Installation `json:"installation,omitempty"` + SearchKeywords []string `json:"search_keywords"` + Categories []string `json:"categories"` + DashboardFolder string `json:"dashboard_folder"` + HasUpdate bool `json:"has_update"` + MetricsCheckQuery string `json:"metrics_check_query"` + LogsCheckQuery string `json:"logs_check_query"` + RuleNamespace string `json:"rule_namespace"` +} + +// Logo represents the logo URLs for an integration +type Logo struct { + DarkThemeURL string `json:"dark_theme_url"` + LightThemeURL string `json:"light_theme_url"` +} + +// Installation represents the installation details of an integration +type Installation struct { + Version string `json:"version"` + InstalledOn time.Time `json:"installed_on"` + Configuration *InstallationConfig `json:"configuration,omitempty"` +} + +// InstallationConfig represents the configuration for installing an integration +type InstallationConfig struct { + ConfigurableLogs *ConfigurableLogs `json:"configurable_logs,omitempty"` + ConfigurableAlerts *ConfigurableAlerts `json:"configurable_alerts,omitempty"` +} + +// ConfigurableLogs represents the logs configuration +type ConfigurableLogs struct { + LogsDisabled bool `json:"logs_disabled"` +} + +// ConfigurableAlerts represents the alerts configuration +type ConfigurableAlerts struct { + AlertsDisabled bool `json:"alerts_disabled"` +} + +// ListIntegrationsResponse represents the response from the list integrations API +type ListIntegrationsResponse struct { + Data map[string]Integration `json:"data"` +} + +// GetIntegrationResponse represents the response from the get integration API +type GetIntegrationResponse struct { + Data Integration `json:"data"` +} + +// InstallIntegrationRequest represents the request body for installing an integration +type InstallIntegrationRequest struct { + Configuration *InstallationConfig `json:"configuration,omitempty"` +} + +// Dashboard represents a dashboard from the get dashboards API +type Dashboard struct { + Dashboard map[string]interface{} `json:"dashboard"` + FolderName string `json:"folder_name"` + Overwrite bool `json:"overwrite"` +} + +// GetDashboardsResponse represents the response from the get dashboards API +type GetDashboardsResponse struct { + Data []Dashboard `json:"data"` +} + +// CreateFolderRequest represents the request body for creating a folder +type CreateFolderRequest struct { + Title string `json:"title"` + UID string `json:"uid"` +} + +// CreateFolderResponse represents the response from creating a folder +type CreateFolderResponse struct { + ID int `json:"id"` + UID string `json:"uid"` + OrgID int `json:"orgId"` + Title string `json:"title"` + URL string `json:"url"` + HasACL bool `json:"hasAcl"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanDelete bool `json:"canDelete"` + CreatedBy string `json:"createdBy"` + Created time.Time `json:"created"` + UpdatedBy string `json:"updatedBy"` + Updated time.Time `json:"updated"` + Version int `json:"version"` +} diff --git a/internal/resources/integrations/resource_integration.go b/internal/resources/integrations/resource_integration.go new file mode 100644 index 000000000..9289b14b3 --- /dev/null +++ b/internal/resources/integrations/resource_integration.go @@ -0,0 +1,314 @@ +package integrations + +import ( + "context" + "fmt" + + "github.com/grafana/terraform-provider-grafana/v4/internal/common" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceIntegration() *common.Resource { + schema := &schema.Resource{ + Description: ` +Manages Grafana Cloud integrations. + +* [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) + +Required access policy scopes: + +* folders:read +* folders:write +* dashboards:read +* dashboards:write + +**Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. +`, + + CreateContext: createIntegration, + ReadContext: readIntegration, + UpdateContext: updateIntegration, + DeleteContext: deleteIntegration, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "slug": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The slug of the integration to install (e.g., 'docker', 'linux-node').", + }, + "installed": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the integration is currently installed.", + }, + "version": { + Type: schema.TypeString, + Computed: true, + Description: "The version of the installed integration.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The display name of the integration.", + }, + "dashboard_folder": { + Type: schema.TypeString, + Computed: true, + Description: "The dashboard folder associated with this integration.", + }, + "configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Configuration options for the integration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "configurable_logs": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Logs configuration for the integration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "logs_disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to disable logs collection for this integration.", + }, + }, + }, + }, + "configurable_alerts": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Alerts configuration for the integration.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "alerts_disabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to disable alerts for this integration.", + }, + }, + }, + }, + }, + }, + }, + }, + } + + return common.NewLegacySDKResource( + common.CategoryCloud, + "grafana_integration", + common.NewResourceID(common.StringIDField("slug")), + schema, + ) +} + +func createIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getIntegrationsClient(meta) + if err != nil { + return diag.FromErr(err) + } + + slug := d.Get("slug").(string) + + // Check if integration already exists and is installed + installed, err := client.IsIntegrationInstalled(ctx, slug) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to check integration status: %w", err)) + } + + if installed { + // Integration is already installed, just set the ID and read the state + d.SetId(slug) + return readIntegration(ctx, d, meta) + } + + // Parse configuration + config := parseInstallationConfig(d) + + // Install the integration + err = client.InstallIntegration(ctx, slug, config) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to install integration: %w", err)) + } + + d.SetId(slug) + return readIntegration(ctx, d, meta) +} + +func readIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getIntegrationsClient(meta) + if err != nil { + return diag.FromErr(err) + } + + slug := d.Id() + + integration, err := client.GetIntegration(ctx, slug) + if err != nil { + if err == ErrNotFound { + return common.WarnMissing("integration", d) + } + return diag.FromErr(fmt.Errorf("failed to get integration: %w", err)) + } + + // Set computed attributes + d.Set("slug", integration.Data.Slug) + d.Set("name", integration.Data.Name) + d.Set("version", integration.Data.Version) + d.Set("dashboard_folder", integration.Data.DashboardFolder) + d.Set("installed", integration.Data.Installation != nil) + + // Set configuration if available + if integration.Data.Installation != nil && integration.Data.Installation.Configuration != nil { + config := flattenInstallationConfig(integration.Data.Installation.Configuration) + d.Set("configuration", config) + } + + return nil +} + +func updateIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getIntegrationsClient(meta) + if err != nil { + return diag.FromErr(err) + } + + slug := d.Id() + + // For now, we handle updates by uninstalling and reinstalling + // This is because the API doesn't seem to have an update endpoint + if d.HasChange("configuration") { + // Uninstall first + err = client.UninstallIntegration(ctx, slug) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to uninstall integration for update: %w", err)) + } + + // Parse new configuration + config := parseInstallationConfig(d) + + // Reinstall with new configuration + err = client.InstallIntegration(ctx, slug, config) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to reinstall integration with new configuration: %w", err)) + } + } + + return readIntegration(ctx, d, meta) +} + +func deleteIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client, err := getIntegrationsClient(meta) + if err != nil { + return diag.FromErr(err) + } + + slug := d.Id() + + err = client.UninstallIntegration(ctx, slug) + if err != nil { + if err == ErrNotFound { + // Integration is already uninstalled + return nil + } + return diag.FromErr(fmt.Errorf("failed to uninstall integration: %w", err)) + } + + return nil +} + +func getIntegrationsClient(meta interface{}) (*Client, error) { + client := meta.(*common.Client) + + // Get the auth token from the Grafana API config + authToken := "" + if client.GrafanaAPIConfig != nil { + authToken = client.GrafanaAPIConfig.APIKey + } + + // Create integrations client using the same pattern as frontendo11y + integrationsClient, err := NewClient( + client.GrafanaAPIURL, + authToken, + nil, // Use default HTTP client + "terraform-provider-grafana", + nil, // No default headers for now + ) + if err != nil { + return nil, fmt.Errorf("failed to create integrations client: %w", err) + } + + // Set the Grafana OpenAPI clients for folder and dashboard operations + if client.GrafanaAPI != nil { + integrationsClient.SetFoldersClient(client.GrafanaAPI.Folders) + integrationsClient.SetDashboardsClient(client.GrafanaAPI.Dashboards) + } + + return integrationsClient, nil +} + +func parseInstallationConfig(d *schema.ResourceData) *InstallationConfig { + configList := d.Get("configuration").([]interface{}) + if len(configList) == 0 { + return nil + } + + configMap := configList[0].(map[string]interface{}) + config := &InstallationConfig{} + + // Parse configurable_logs + if logsConfigList, ok := configMap["configurable_logs"].([]interface{}); ok && len(logsConfigList) > 0 { + logsConfigMap := logsConfigList[0].(map[string]interface{}) + config.ConfigurableLogs = &ConfigurableLogs{ + LogsDisabled: logsConfigMap["logs_disabled"].(bool), + } + } + + // Parse configurable_alerts + if alertsConfigList, ok := configMap["configurable_alerts"].([]interface{}); ok && len(alertsConfigList) > 0 { + alertsConfigMap := alertsConfigList[0].(map[string]interface{}) + config.ConfigurableAlerts = &ConfigurableAlerts{ + AlertsDisabled: alertsConfigMap["alerts_disabled"].(bool), + } + } + + return config +} + +func flattenInstallationConfig(config *InstallationConfig) []interface{} { + if config == nil { + return nil + } + + result := make(map[string]interface{}) + + if config.ConfigurableLogs != nil { + result["configurable_logs"] = []interface{}{ + map[string]interface{}{ + "logs_disabled": config.ConfigurableLogs.LogsDisabled, + }, + } + } + + if config.ConfigurableAlerts != nil { + result["configurable_alerts"] = []interface{}{ + map[string]interface{}{ + "alerts_disabled": config.ConfigurableAlerts.AlertsDisabled, + }, + } + } + + return []interface{}{result} +} diff --git a/internal/resources/integrations/resources.go b/internal/resources/integrations/resources.go new file mode 100644 index 000000000..3efd013d6 --- /dev/null +++ b/internal/resources/integrations/resources.go @@ -0,0 +1,9 @@ +package integrations + +import ( + "github.com/grafana/terraform-provider-grafana/v4/internal/common" +) + +var Resources = []*common.Resource{ + resourceIntegration(), +} diff --git a/pkg/provider/resources.go b/pkg/provider/resources.go index 1c3da02d4..63e8f922f 100644 --- a/pkg/provider/resources.go +++ b/pkg/provider/resources.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/terraform-provider-grafana/v4/internal/resources/fleetmanagement" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/frontendo11y" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/grafana" + "github.com/grafana/terraform-provider-grafana/v4/internal/resources/integrations" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/k6" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/machinelearning" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/oncall" @@ -67,6 +68,7 @@ func Resources() []*common.Resource { var resources []*common.Resource resources = append(resources, cloud.Resources...) resources = append(resources, grafana.Resources...) + resources = append(resources, integrations.Resources...) resources = append(resources, oncall.Resources...) resources = append(resources, machinelearning.Resources...) resources = append(resources, slo.Resources...) From e69265621b9112a4c13071fdbb0bb7458160ac99 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 20 Mar 2026 16:21:51 +0100 Subject: [PATCH 02/28] Add rule models --- internal/resources/integrations/models.go | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/internal/resources/integrations/models.go b/internal/resources/integrations/models.go index c9d2acb5a..fdddbe2c4 100644 --- a/internal/resources/integrations/models.go +++ b/internal/resources/integrations/models.go @@ -18,6 +18,8 @@ type Integration struct { MetricsCheckQuery string `json:"metrics_check_query"` LogsCheckQuery string `json:"logs_check_query"` RuleNamespace string `json:"rule_namespace"` + + GrafanaManagedAlertsRolloutLevel *int `json:"grafana_managed_alerts_rollout_level,omitempty"` } // Logo represents the logo URLs for an integration @@ -76,6 +78,42 @@ type GetDashboardsResponse struct { Data []Dashboard `json:"data"` } +// RuleGroup represents a Prometheus rule group (recording or alerting) +type RuleGroup struct { + Name string `json:"name"` + Rules []Rule `json:"rules"` +} + +// Rule represents a single Prometheus rule (recording or alerting) +type Rule struct { + Record string `json:"record,omitempty"` + Alert string `json:"alert,omitempty"` + Expr string `json:"expr"` + For string `json:"for,omitempty"` + KeepFiringFor string `json:"keep_firing_for,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +// IntegrationRulesResponse is the response from GET /integrations/{id}/rules +type IntegrationRulesResponse struct { + Data IntegrationRulesData `json:"data"` +} + +// IntegrationRulesData contains the rule groups for an integration +type IntegrationRulesData struct { + Namespace string `json:"namespace"` + RecordingRules []RuleGroup `json:"recording_rules,omitempty"` + AlertingRules []RuleGroup `json:"alerting_rules,omitempty"` +} + +// ImportRulesResponse is the response from POST /api/convert/prometheus/config/v1/rules +type ImportRulesResponse struct { + Message string `json:"message"` + Created int `json:"created"` + Updated int `json:"updated"` +} + // CreateFolderRequest represents the request body for creating a folder type CreateFolderRequest struct { Title string `json:"title"` From 4c48862cbaa889e8c44b5b3e808d238d27b843f7 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Tue, 24 Mar 2026 10:34:36 +0100 Subject: [PATCH 03/28] Add basic rules install & clean up code slightly --- internal/resources/integrations/client.go | 282 +++++++++++++----- .../integrations/resource_integration.go | 58 +++- 2 files changed, 259 insertions(+), 81 deletions(-) diff --git a/internal/resources/integrations/client.go b/internal/resources/integrations/client.go index a7b805d59..0648452ac 100644 --- a/internal/resources/integrations/client.go +++ b/internal/resources/integrations/client.go @@ -18,12 +18,18 @@ import ( ) const ( - // Base paths for different API operations editorBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-editor" adminBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-admin" + grafanaCloudPromUID = "grafanacloud-prom" + rulesConvertAPIPath = "/api/convert/prometheus/config/v1/rules" + defaultRetries = 3 defaultTimeout = 90 * time.Second + + RolloutLevelMimir = 0 + RolloutLevelInstallOnly = 1 + RolloutLevelGrafana = 2 ) // Client wraps the HTTP client for integrations API calls @@ -123,7 +129,7 @@ func (c *Client) generateFolderUID(slug string) string { return fmt.Sprintf("integration---%s", uid) } -// CreateFolder creates a folder using the Grafana OpenAPI client +// CreateFolder creates a folder func (c *Client) CreateFolder(ctx context.Context, title, uid string) error { if c.foldersClient == nil { return fmt.Errorf("folders client not available") @@ -142,13 +148,16 @@ func (c *Client) CreateFolder(ctx context.Context, title, uid string) error { return nil } -// DeleteFolder deletes a folder using the Grafana OpenAPI client +// DeleteFolder deletes a folder func (c *Client) DeleteFolder(ctx context.Context, uid string) error { if c.foldersClient == nil { return fmt.Errorf("folders client not available") } - _, err := c.foldersClient.DeleteFolder(folders.NewDeleteFolderParams().WithFolderUID(uid)) + force := true + params := folders.NewDeleteFolderParams().WithFolderUID(uid) + params.WithForceDeleteRules(&force) + _, err := c.foldersClient.DeleteFolder(params) if err != nil { return fmt.Errorf("failed to delete folder %s: %w", uid, err) } @@ -156,12 +165,8 @@ func (c *Client) DeleteFolder(ctx context.Context, uid string) error { return nil } -// CreateDashboard creates a dashboard in the specified folder using the Grafana OpenAPI client +// CreateDashboard creates a dashboard in the specified folder func (c *Client) CreateDashboard(ctx context.Context, dashboard Dashboard, folderUID string) error { - if c.dashboardsClient == nil { - // Fallback to raw HTTP request if OpenAPI client is not available - return c.createDashboardHTTP(ctx, dashboard, folderUID) - } // Make a copy of the dashboard data to avoid modifying the original dashboardData := make(map[string]interface{}) @@ -183,78 +188,70 @@ func (c *Client) CreateDashboard(ctx context.Context, dashboard Dashboard, folde // Use the OpenAPI client _, err := c.dashboardsClient.PostDashboard(&dashboardCommand) if err != nil { - return fmt.Errorf("failed to create dashboard using OpenAPI client: %w", err) + return fmt.Errorf("failed to create dashboard: %w", err) } return nil } -// createDashboardHTTP creates a dashboard using raw HTTP requests as fallback -func (c *Client) createDashboardHTTP(ctx context.Context, dashboard Dashboard, folderUID string) error { - path := "/api/dashboards/db" - - // Make a copy of the dashboard data to avoid modifying the original - dashboardData := make(map[string]interface{}) - for k, v := range dashboard.Dashboard { - dashboardData[k] = v - } - - // Remove id from dashboard if present - delete(dashboardData, "id") - - requestBody := map[string]interface{}{ - "dashboard": dashboardData, - "folderUid": folderUID, - "overwrite": dashboard.Overwrite, - "message": "creating dashboard from the Cloud Connections plugin", - } - - err := c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, nil) - if err != nil { - return fmt.Errorf("failed to create dashboard using HTTP client: %w", err) - } - - return nil -} - -// InstallIntegration installs an integration with the given configuration using the new multi-step workflow -func (c *Client) InstallIntegration(ctx context.Context, slug string, config *InstallationConfig) error { - // Step 1: Get the integration details to get the folder name +// InstallDashboards creates the folder and dashboards for an integration. +// Used for both install and upgrade +func (c *Client) InstallDashboards(ctx context.Context, slug string, config *InstallationConfig) error { integration, err := c.GetIntegration(ctx, slug) if err != nil { return fmt.Errorf("failed to get integration details: %w", err) } - // Step 2: Post dashboards (this prepares the dashboards) dashboardsResponse, err := c.PostDashboards(ctx, slug, config) if err != nil { return fmt.Errorf("failed to post dashboards: %w", err) } - // Step 3: Create the folder folderUID := c.generateFolderUID(slug) folderTitle := integration.Data.DashboardFolder err = c.CreateFolder(ctx, folderTitle, folderUID) if err != nil { - // Check if it's a 412 error (folder already exists) - if strings.Contains(err.Error(), "412") || strings.Contains(err.Error(), "already exists") { - // Folder already exists, continue with dashboard creation - } else { + if !strings.Contains(err.Error(), "412") && !strings.Contains(err.Error(), "already exists") { return fmt.Errorf("failed to create folder: %w", err) } } - // Step 4: Add each dashboard to the folder for i, dashboard := range dashboardsResponse.Data { err = c.CreateDashboard(ctx, dashboard, folderUID) if err != nil { - // If dashboard creation fails, try to clean up the folder _ = c.DeleteFolder(ctx, folderUID) return fmt.Errorf("failed to create dashboard %d: %w", i+1, err) } } - // Step 5: Install the integration + return nil +} + +// InstallIntegration installs an integration with the given configuration +func (c *Client) InstallIntegration(ctx context.Context, slug string, config *InstallationConfig) error { + integration, err := c.GetIntegration(ctx, slug) + if err != nil { + return fmt.Errorf("failed to get integration details: %w", err) + } + + // Step 1: Create folder and dashboards + err = c.InstallDashboards(ctx, slug, config) + if err != nil { + return err + } + + folderUID := c.generateFolderUID(slug) + + // Step 2: Install rules to Grafana Alerting if applicable + if shouldInstallRulesOnInstall(integration.Data.GrafanaManagedAlertsRolloutLevel) { + err = c.InstallIntegrationRules(ctx, slug) + if err != nil { + _ = c.DeleteFolder(ctx, folderUID) + return fmt.Errorf("failed to install integration rules: %w", err) + } + } + + // Step 3: Install the integration path := fmt.Sprintf("%s/integrations/%s/install", adminBasePath, url.PathEscape(slug)) requestBody := InstallIntegrationRequest{ @@ -263,7 +260,6 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In err = c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, nil) if err != nil { - // If installation fails, try to clean up the folder _ = c.DeleteFolder(ctx, folderUID) return fmt.Errorf("failed to install integration %s: %w", slug, err) } @@ -271,24 +267,24 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In return nil } -// UninstallIntegration uninstalls an integration and deletes its folder +// UninstallIntegration uninstalls an integration and deletes its folder and rules. +// Resources are cleaned up before calling the uninstall API, matching the plugin's +// order of operations. func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { - // Step 1: Uninstall the integration - path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) + // Step 1: Delete the folder (dashboards are removed with it) + folderUID := c.generateFolderUID(slug) + _ = c.DeleteFolder(ctx, folderUID) + + // Step 2: Remove rules + _ = c.UninstallIntegrationRules(ctx, slug) + // Step 3: Call the uninstall API + path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) err := c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) if err != nil { return fmt.Errorf("failed to uninstall integration %s: %w", slug, err) } - // Step 2: Delete the folder - folderUID := c.generateFolderUID(slug) - err = c.DeleteFolder(ctx, folderUID) - if err != nil { - // Log the error but don't fail the uninstall if folder deletion fails - return fmt.Errorf("integration uninstalled but failed to delete folder %s: %w", folderUID, err) - } - return nil } @@ -302,12 +298,161 @@ func (c *Client) IsIntegrationInstalled(ctx context.Context, slug string) (bool, return integration.Data.Installation != nil, nil } +// GetIntegrationRules fetches recording and alerting rule groups for an integration +func (c *Client) GetIntegrationRules(ctx context.Context, slug string) (*IntegrationRulesData, error) { + path := fmt.Sprintf("%s/integrations/%s/rules", adminBasePath, url.PathEscape(slug)) + + var response IntegrationRulesResponse + err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) + if err != nil { + return nil, fmt.Errorf("failed to get rules for integration %s: %w", slug, err) + } + + return &response.Data, nil +} + +// resolveGrafanaRulesNamespace determines the Grafana Alerting namespace. +// Priority: dashboard_folder > rule_namespace > "Integration - {name}" +func resolveGrafanaRulesNamespace(dashboardFolder, ruleNamespace, integrationName string) string { + if dashboardFolder != "" { + return dashboardFolder + } + if ruleNamespace != "" { + return ruleNamespace + } + if integrationName != "" { + return fmt.Sprintf("Integration - %s", integrationName) + } + return "" +} + +// shouldInstallRulesOnInstall returns true if rules should be installed to +// Grafana Alerting for a new installation (rollout level >= 1). +func shouldInstallRulesOnInstall(rolloutLevel *int) bool { + return rolloutLevel != nil && *rolloutLevel >= RolloutLevelInstallOnly +} + +// shouldInstallRulesOnUpgrade returns true if rules should be (re)installed +// during an upgrade, based on whether rules already exist in Grafana Alerting +// and the integration's rollout level. +func shouldInstallRulesOnUpgrade(rulesExistInGrafana bool, rolloutLevel *int) bool { + if rolloutLevel == nil { + return false + } + level := *rolloutLevel + + if rulesExistInGrafana && level != RolloutLevelMimir { + return true + } + if !rulesExistInGrafana && level == RolloutLevelGrafana { + return true + } + return false +} + +// InstallIntegrationRules fetches rules from the integrations API and imports +// them into Grafana's native alerting system via the conversion-prometheus API. +// Source: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#compatible-endpoints +func (c *Client) InstallIntegrationRules(ctx context.Context, slug string) error { + rulesData, err := c.GetIntegrationRules(ctx, slug) + if err != nil { + return fmt.Errorf("failed to get integration rules: %w", err) + } + + integration, err := c.GetIntegration(ctx, slug) + if err != nil { + return fmt.Errorf("failed to get integration details for rules namespace: %w", err) + } + + namespace := resolveGrafanaRulesNamespace( + integration.Data.DashboardFolder, + integration.Data.RuleNamespace, + integration.Data.Name, + ) + if namespace == "" { + return nil + } + + var allGroups []RuleGroup + allGroups = append(allGroups, rulesData.RecordingRules...) + allGroups = append(allGroups, rulesData.AlertingRules...) + + if len(allGroups) == 0 { + return nil + } + + payload := map[string][]RuleGroup{ + namespace: allGroups, + } + + return c.doAPIRequestWithHeaders(ctx, http.MethodPost, rulesConvertAPIPath, payload, nil, map[string]string{ + "X-Grafana-Alerting-Datasource-UID": grafanaCloudPromUID, + }) +} + +// UninstallIntegrationRules deletes the rule namespace from Grafana +func (c *Client) UninstallIntegrationRules(ctx context.Context, slug string) error { + integration, err := c.GetIntegration(ctx, slug) + if err != nil { + return fmt.Errorf("failed to get integration details for rules namespace: %w", err) + } + + namespace := resolveGrafanaRulesNamespace( + integration.Data.DashboardFolder, + integration.Data.RuleNamespace, + integration.Data.Name, + ) + if namespace == "" { + return nil + } + + path := fmt.Sprintf("%s/%s", rulesConvertAPIPath, url.PathEscape(namespace)) + err = c.doAPIRequest(ctx, http.MethodDelete, path, nil, nil) + if err != nil { + if err == ErrNotFound { + return nil + } + return fmt.Errorf("failed to delete rule namespace %s: %w", namespace, err) + } + return nil +} + +// CheckRulesExist checks whether rules exist in Grafana for a given namespace +func (c *Client) CheckRulesExist(ctx context.Context, namespace string) (bool, error) { + path := fmt.Sprintf("%s/%s", rulesConvertAPIPath, url.PathEscape(namespace)) + err := c.doAPIRequest(ctx, http.MethodGet, path, nil, nil) + if err != nil { + if err == ErrNotFound { + return false, nil + } + return false, err + } + return true, nil +} + +// UpgradeIntegration upgrades an installed integration to its latest version. +func (c *Client) UpgradeIntegration(ctx context.Context, slug string) error { + path := fmt.Sprintf("%s/integrations/%s/upgrade", adminBasePath, url.PathEscape(slug)) + err := c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) + if err != nil { + return fmt.Errorf("failed to upgrade integration %s: %w", slug, err) + } + return nil +} + var ( ErrNotFound = fmt.Errorf("not found") ErrUnauthorized = fmt.Errorf("request not authorized") ) func (c *Client) doAPIRequest(ctx context.Context, method string, path string, body any, responseData any) error { + return c.doAPIRequestWithHeaders(ctx, method, path, body, responseData, nil) +} + +func (c *Client) doAPIRequestWithHeaders( + ctx context.Context, method, path string, body, responseData any, + extraHeaders map[string]string, +) error { parsedURL, err := url.Parse(c.grafanaAPIHost) if err != nil { return fmt.Errorf("failed to parse grafana API url: %w", err) @@ -322,7 +467,6 @@ func (c *Client) doAPIRequest(ctx context.Context, method string, path string, b reqBodyBytes = bytes.NewReader(bs) } - // Ensure no double slashes in URL construction baseURL := strings.TrimSuffix(parsedURL.String(), "/") fullURL := baseURL + path req, err := http.NewRequestWithContext(ctx, method, fullURL, reqBodyBytes) @@ -330,17 +474,17 @@ func (c *Client) doAPIRequest(ctx context.Context, method string, path string, b return fmt.Errorf("failed to create request: %w", err) } - // Add default headers for k, v := range c.defaultHeaders { req.Header.Add(k, v) } + for k, v := range extraHeaders { + req.Header.Set(k, v) + } - // Add authentication - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.authToken)) - req.Header.Add("Content-Type", "application/json") - req.Header.Add("User-Agent", c.userAgent) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.authToken)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", c.userAgent) - // Debug logging - add the full URL to error messages resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("failed to do request to %s: %w", fullURL, err) diff --git a/internal/resources/integrations/resource_integration.go b/internal/resources/integrations/resource_integration.go index 9289b14b3..a82137a7f 100644 --- a/internal/resources/integrations/resource_integration.go +++ b/internal/resources/integrations/resource_integration.go @@ -61,6 +61,11 @@ Required access policy scopes: Computed: true, Description: "The dashboard folder associated with this integration.", }, + "grafana_managed_alerts_rollout_level": { + Type: schema.TypeInt, + Computed: true, + Description: "The Grafana Managed Alerts rollout level for this integration (0=Mimir, 1=Install, 2=Grafana).", + }, "configuration": { Type: schema.TypeList, Optional: true, @@ -163,12 +168,14 @@ func readIntegration(ctx context.Context, d *schema.ResourceData, meta interface return diag.FromErr(fmt.Errorf("failed to get integration: %w", err)) } - // Set computed attributes d.Set("slug", integration.Data.Slug) d.Set("name", integration.Data.Name) d.Set("version", integration.Data.Version) d.Set("dashboard_folder", integration.Data.DashboardFolder) d.Set("installed", integration.Data.Installation != nil) + if integration.Data.GrafanaManagedAlertsRolloutLevel != nil { + d.Set("grafana_managed_alerts_rollout_level", *integration.Data.GrafanaManagedAlertsRolloutLevel) + } // Set configuration if available if integration.Data.Installation != nil && integration.Data.Installation.Configuration != nil { @@ -187,22 +194,48 @@ func updateIntegration(ctx context.Context, d *schema.ResourceData, meta interfa slug := d.Id() - // For now, we handle updates by uninstalling and reinstalling - // This is because the API doesn't seem to have an update endpoint if d.HasChange("configuration") { - // Uninstall first - err = client.UninstallIntegration(ctx, slug) + config := parseInstallationConfig(d) + + integration, err := client.GetIntegration(ctx, slug) if err != nil { - return diag.FromErr(fmt.Errorf("failed to uninstall integration for update: %w", err)) + return diag.FromErr(fmt.Errorf("failed to get integration for upgrade: %w", err)) } - // Parse new configuration - config := parseInstallationConfig(d) + // Determine which namespace to use for GMA migration + namespace := resolveGrafanaRulesNamespace( + integration.Data.DashboardFolder, + integration.Data.RuleNamespace, + integration.Data.Name, + ) + rulesExistInGrafana := false + if namespace != "" { + rulesExistInGrafana, _ = client.CheckRulesExist(ctx, namespace) + } + + // Remove old dashboards & alerts by deleting the folder + folderUID := client.generateFolderUID(slug) + _ = client.DeleteFolder(ctx, folderUID) + + // Install new dashboards + err = client.InstallDashboards(ctx, slug, config) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to install new dashboards: %w", err)) + } + + // Handle rules migration based on rollout level + rolloutLevel := integration.Data.GrafanaManagedAlertsRolloutLevel + if shouldInstallRulesOnUpgrade(rulesExistInGrafana, rolloutLevel) { + err = client.InstallIntegrationRules(ctx, slug) + if err != nil { + return diag.FromErr(fmt.Errorf("failed to install updated rules: %w", err)) + } + } - // Reinstall with new configuration - err = client.InstallIntegration(ctx, slug, config) + // Call the upgrade API + err = client.UpgradeIntegration(ctx, slug) if err != nil { - return diag.FromErr(fmt.Errorf("failed to reinstall integration with new configuration: %w", err)) + return diag.FromErr(fmt.Errorf("failed to upgrade integration: %w", err)) } } @@ -238,7 +271,7 @@ func getIntegrationsClient(meta interface{}) (*Client, error) { authToken = client.GrafanaAPIConfig.APIKey } - // Create integrations client using the same pattern as frontendo11y + // Create integrations client integrationsClient, err := NewClient( client.GrafanaAPIURL, authToken, @@ -251,6 +284,7 @@ func getIntegrationsClient(meta interface{}) (*Client, error) { } // Set the Grafana OpenAPI clients for folder and dashboard operations + // TODO: In the future we also want to use the convert-prometheus OpenAPI client for importing alerts & rules if client.GrafanaAPI != nil { integrationsClient.SetFoldersClient(client.GrafanaAPI.Folders) integrationsClient.SetDashboardsClient(client.GrafanaAPI.Dashboards) From 2175f83cbc5ccea642db27b11a567c73cdbd77ab Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Tue, 24 Mar 2026 11:30:18 +0100 Subject: [PATCH 04/28] Add config for alert disablement --- internal/resources/integrations/client.go | 10 ++++++++-- .../resources/integrations/resource_integration.go | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/resources/integrations/client.go b/internal/resources/integrations/client.go index 0648452ac..0a233738a 100644 --- a/internal/resources/integrations/client.go +++ b/internal/resources/integrations/client.go @@ -244,7 +244,7 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In // Step 2: Install rules to Grafana Alerting if applicable if shouldInstallRulesOnInstall(integration.Data.GrafanaManagedAlertsRolloutLevel) { - err = c.InstallIntegrationRules(ctx, slug) + err = c.InstallIntegrationRules(ctx, slug, config) if err != nil { _ = c.DeleteFolder(ctx, folderUID) return fmt.Errorf("failed to install integration rules: %w", err) @@ -353,7 +353,13 @@ func shouldInstallRulesOnUpgrade(rulesExistInGrafana bool, rolloutLevel *int) bo // InstallIntegrationRules fetches rules from the integrations API and imports // them into Grafana's native alerting system via the conversion-prometheus API. // Source: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#compatible-endpoints -func (c *Client) InstallIntegrationRules(ctx context.Context, slug string) error { +func (c *Client) InstallIntegrationRules(ctx context.Context, slug string, config *InstallationConfig) error { + if config != nil && + config.ConfigurableAlerts != nil && + config.ConfigurableAlerts.AlertsDisabled { + return nil + } + rulesData, err := c.GetIntegrationRules(ctx, slug) if err != nil { return fmt.Errorf("failed to get integration rules: %w", err) diff --git a/internal/resources/integrations/resource_integration.go b/internal/resources/integrations/resource_integration.go index a82137a7f..473662ddb 100644 --- a/internal/resources/integrations/resource_integration.go +++ b/internal/resources/integrations/resource_integration.go @@ -226,7 +226,7 @@ func updateIntegration(ctx context.Context, d *schema.ResourceData, meta interfa // Handle rules migration based on rollout level rolloutLevel := integration.Data.GrafanaManagedAlertsRolloutLevel if shouldInstallRulesOnUpgrade(rulesExistInGrafana, rolloutLevel) { - err = client.InstallIntegrationRules(ctx, slug) + err = client.InstallIntegrationRules(ctx, slug, config) if err != nil { return diag.FromErr(fmt.Errorf("failed to install updated rules: %w", err)) } From e8dedc084a89cc9b58ef0ec752a3be29a5522d24 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Tue, 24 Mar 2026 11:44:29 +0100 Subject: [PATCH 05/28] Fix folder UID sanitation --- internal/resources/integrations/client.go | 25 +++++++++++-------- .../integrations/resource_integration.go | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/resources/integrations/client.go b/internal/resources/integrations/client.go index 0a233738a..da69b7a6a 100644 --- a/internal/resources/integrations/client.go +++ b/internal/resources/integrations/client.go @@ -122,11 +122,9 @@ func (c *Client) PostDashboards(ctx context.Context, slug string, config *Instal } // generateFolderUID generates a folder UID from an integration slug -func (c *Client) generateFolderUID(slug string) string { - // Replace any special characters with dashes and add the integration prefix - uid := strings.ReplaceAll(slug, "_", "-") - uid = strings.ReplaceAll(uid, " ", "-") - return fmt.Sprintf("integration---%s", uid) +func (c *Client) generateFolderUID(folderName string) string { + // Take in Dashboard Folder and sanitise the whitespace with dashes + return strings.ReplaceAll(strings.ToLower(folderName), " ", "-") } // CreateFolder creates a folder @@ -207,9 +205,9 @@ func (c *Client) InstallDashboards(ctx context.Context, slug string, config *Ins return fmt.Errorf("failed to post dashboards: %w", err) } - folderUID := c.generateFolderUID(slug) - folderTitle := integration.Data.DashboardFolder - err = c.CreateFolder(ctx, folderTitle, folderUID) + dashboardFolder := integration.Data.DashboardFolder + folderUID := c.generateFolderUID(dashboardFolder) + err = c.CreateFolder(ctx, dashboardFolder, folderUID) if err != nil { if !strings.Contains(err.Error(), "412") && !strings.Contains(err.Error(), "already exists") { return fmt.Errorf("failed to create folder: %w", err) @@ -240,7 +238,7 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In return err } - folderUID := c.generateFolderUID(slug) + folderUID := c.generateFolderUID(integration.Data.DashboardFolder) // Step 2: Install rules to Grafana Alerting if applicable if shouldInstallRulesOnInstall(integration.Data.GrafanaManagedAlertsRolloutLevel) { @@ -271,8 +269,13 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In // Resources are cleaned up before calling the uninstall API, matching the plugin's // order of operations. func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { + integration, err := c.GetIntegration(ctx, slug) + if err != nil { + return fmt.Errorf("failed to get integration details: %w", err) + } + // Step 1: Delete the folder (dashboards are removed with it) - folderUID := c.generateFolderUID(slug) + folderUID := c.generateFolderUID(integration.Data.DashboardFolder) _ = c.DeleteFolder(ctx, folderUID) // Step 2: Remove rules @@ -280,7 +283,7 @@ func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { // Step 3: Call the uninstall API path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) - err := c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) + err = c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) if err != nil { return fmt.Errorf("failed to uninstall integration %s: %w", slug, err) } diff --git a/internal/resources/integrations/resource_integration.go b/internal/resources/integrations/resource_integration.go index 473662ddb..ac3a0b586 100644 --- a/internal/resources/integrations/resource_integration.go +++ b/internal/resources/integrations/resource_integration.go @@ -214,7 +214,7 @@ func updateIntegration(ctx context.Context, d *schema.ResourceData, meta interfa } // Remove old dashboards & alerts by deleting the folder - folderUID := client.generateFolderUID(slug) + folderUID := client.generateFolderUID(integration.Data.DashboardFolder) _ = client.DeleteFolder(ctx, folderUID) // Install new dashboards From d979cd181858e148b0b9c4da37da906326dde296 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 26 Mar 2026 15:49:04 +0100 Subject: [PATCH 06/28] Cleanup, add support for tracking install drift, remove autoupdate prototype --- .../resources/integrations/api-quick-doc.md | 96 ------------------- internal/resources/integrations/client.go | 10 +- .../integrations/resource_integration.go | 81 ++++++---------- 3 files changed, 32 insertions(+), 155 deletions(-) delete mode 100644 internal/resources/integrations/api-quick-doc.md diff --git a/internal/resources/integrations/api-quick-doc.md b/internal/resources/integrations/api-quick-doc.md deleted file mode 100644 index 1c92101e2..000000000 --- a/internal/resources/integrations/api-quick-doc.md +++ /dev/null @@ -1,96 +0,0 @@ -base path: `` - - -# List integrations -- method GET -- path /grafana-easystart-app/integrations-api-editor/integrations -- parameters: - - `installed=true` for only the installed one -- expected: - - code 200 - - example body: - ``` - {"data":{"docker":{"name":"Docker","slug":"docker","version":"1.3.4","overview":"Docker is a popular open-source platform that enables developers to create, deploy, and run applications in a virtualized environment called a container. It allows developers to package an application with all its dependencies, libraries, and other components required to run it, into a single container image. The Docker integration collects metrics and logs from a Docker instance and provides useful pre-built dashboards to monitor them. \n\n**Links** \n[Docker mixin](https://github.com/grafana/jsonnet-libs/tree/master/docker-mixin) \n[Docker docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-docker/) \n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png"},"type":"agent","installation":{"version":"1.3.4","installed_on":"2025-03-28T18:26:54Z"},"search_keywords":["container","deployment","docker"],"categories":["Servers and VMs"],"dashboard_folder":"Integration - Docker","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":"integrations-docker"},"linux-node":{"name":"Linux Server","slug":"linux-node","version":"1.6.1","overview":"Linux is a family of open-source Unix-like operating systems based on the Linux kernel. Linux is the leading operating system on servers, and is one of the most prominent examples of free and open-source software collaboration.\n\nLinux Server integration for Grafana Cloud enables you to collect metrics related to the operating system running on a node, including aspects like CPU usage, load average, memory usage, and disk and networking I/O using node_exporter integration. It also allows you to use Grafana Alloy to scrape logs.\n\n**Links** \n[Linux Server mixin](https://github.com/grafana/node_exporter/tree/master/docs/node-mixin/lib/linux) \n[Linux Server docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-linux-node/) \n[Learning journey: Monitor a Linux server in Grafana Cloud](https://grafana.com/docs/learning-journeys/linux-server-integration/)\n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/linux.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/linux.png"},"type":"agent","installation":{"version":"1.6.1","installed_on":"2025-04-27T17:06:08Z"},"search_keywords":["operating system","server","linux","node","linux-node"],"categories":["Servers and VMs","Operating System"],"dashboard_folder":"Integration - Linux Node","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":"integrations-linux-node"},"traefik":{"name":"Traefik","slug":"traefik","version":"0.0.6","overview":"Traefik is a dynamic load balancer designed for ease of configuration, especially in dynamic environments. It supports automatic discovery of services, metrics, tracing, and has Let's Encrypt support out of the box. Traefik provides a “ready to go” system for serving production traffic with these additions.\n\n**Links** \n[Traefik mixin](https://github.com/grafana/jsonnet-libs/tree/master/traefik-mixin) \n[Traefik docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-traefik/)\n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/traefik.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/traefik.png"},"type":"agent","installation":{"version":"0.0.6","installed_on":"2023-11-04T15:32:35Z"},"search_keywords":["load","balancer","networking","traefik"],"categories":["Networking"],"dashboard_folder":"Integration - Traefik","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":""},"windows-exporter":{"name":"Windows","slug":"windows-exporter","version":"1.1.5","overview":"Monitor Windows instances using Grafana Alloy. The integration comes with pre installed dashboards, which give an overview of your Windows' fleet at once, a single host overview, as well as additional dashboards that provide more metrics for further system performance analysis.\nThe integration also provides dashboard showing Windows event logs.\n\n**Links** \n[Windows mixin](https://github.com/grafana/jsonnet-libs/tree/master/windows-mixin) \n[Windows docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-windows-exporter/) \n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/windows.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/windows.png"},"type":"agent","installation":{"version":"1.1.5","installed_on":"2025-05-15T09:38:02Z"},"search_keywords":["operating system","windows","microsoft","windows-exporter"],"categories":["Operating System","Servers and VMs"],"dashboard_folder":"Integration - Windows Exporter","has_update":false,"metrics_check_query":"","logs_check_query":"","rule_namespace":"integrations-windows-exporter"}}} - ``` - -# Get one integration state -- method GET -- path /grafana-easystart-app/integrations-api-editor/integrations/{slug} -- parameters: - - `{slug}` the integration -- expected: - - code 200 - - example body: - ``` - {"data":{"name":"Docker","slug":"docker","version":"1.3.4","overview":"Docker is a popular open-source platform that enables developers to create, deploy, and run applications in a virtualized environment called a container. It allows developers to package an application with all its dependencies, libraries, and other components required to run it, into a single container image. The Docker integration collects metrics and logs from a Docker instance and provides useful pre-built dashboards to monitor them. \n\n**Links** \n[Docker mixin](https://github.com/grafana/jsonnet-libs/tree/master/docker-mixin) \n[Docker docs](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-docker/) \n","logo":{"dark_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png","light_theme_url":"https://storage.googleapis.com/grafanalabs-integration-logos/docker.png"},"type":"agent","installation":{"version":"1.3.4","installed_on":"2025-03-28T18:26:54Z","configuration":{"configurable_logs":{"logs_disabled":false},"configurable_alerts":{"alerts_disabled":false}}},"search_keywords":null,"categories":["Servers and VMs"],"dashboard_folder":"Integration - Docker","has_update":false,"metrics_check_query":"vector(1) and on() ((count(up{job=\"integrations/docker\"} == 1) \u003e 0) and (absent(absent(machine_scrape_error{job=\"integrations/docker\"})))) or vector(0) and on() ((count(up{job=\"integrations/docker\"}) \u003e 0) and (absent(machine_scrape_error{job=\"integrations/docker\"})))","logs_check_query":"sum(count_over_time({job=\"integrations/docker\"}[5m]))","rule_namespace":"","agent_configuration":{"supported_platforms":["linux"]},"metrics":{"status":"available","available_metrics":[{"name":"container_cpu_usage_seconds_total","type":"counter","description":"Cumulative cpu time consumed in seconds."},{"name":"container_fs_reads_total","type":"counter","description":"Cumulative count of reads completed"},{"name":"container_fs_usage_bytes","type":"gauge","description":"Number of bytes that are consumed by the container on this filesystem."},{"name":"container_fs_writes_total","type":"counter","description":"Cumulative count of writes completed"},{"name":"container_last_seen","type":"gauge","description":"Last time a container was seen by the exporter"},{"name":"container_memory_usage_bytes","type":"gauge","description":"Current memory usage in bytes, including all memory regardless of when it was accessed"},{"name":"container_network_receive_bytes_total","type":"counter","description":"Cumulative count of bytes received"},{"name":"container_network_receive_errors_total","type":"counter","description":"Cumulative count of errors encountered while receiving"},{"name":"container_network_receive_packets_dropped_total","type":"counter","description":"Cumulative count of packets dropped while receiving"},{"name":"container_network_transmit_bytes_total","type":"counter","description":"Cumulative count of bytes transmitted"},{"name":"container_network_transmit_errors_total","type":"counter","description":"Cumulative count of errors encountered while transmitting"},{"name":"container_network_transmit_packets_dropped_total","type":"counter","description":"Cumulative count of packets dropped while transmitting"},{"name":"container_spec_memory_reservation_limit_bytes","type":"gauge","description":"Memory reservation limit for the container."},{"name":"machine_memory_bytes","type":"gauge","description":"Amount of memory installed on the machine."},{"name":"machine_scrape_error","type":"gauge","description":"1 if there was an error while getting machine metrics, 0 otherwise."},{"name":"up"}]},"rules":{"status":"available","available_rules":[{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:usage"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:gauge"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:total"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:total"},{"namespace":"integrations-docker","group":"asserts-integration-docker.rules","name":"asserts:resource:gauge"}]},"alerts":{"status":"not_supported"},"logs":{"status":"available"},"traces":{"status":"not_supported"},"asserts":{"status":"available"},"dashboards":{"status":"available","screenshots":[{"url":"https://storage.googleapis.com/grafanalabs-integration-assets/docker/screenshots/docker-1.2.png","sort_order":1,"description":"Overview \u0026 Compute"},{"url":"https://storage.googleapis.com/grafanalabs-integration-assets/docker/screenshots/docker-logs-1.2.png","sort_order":2,"description":"Logs"}],"folder_name":"Integration - Docker"},"configuration_defaults":{"enable_logs_toggle":true,"enable_alerts_toggle":false,"configurable_logs_defaults":{"logs_disabled":false},"configurable_alerts_defaults":{"alerts_disabled":false}}}} - ``` - -# Install one integration -## Get the dashboards -- method POST -- path /api/plugin-proxy/grafana-easystart-app/integrations-api-admin/integrations/{slug}/dashboards -- parameters: - - `{slug}` the integration -- body example: - ``` - {"configuration":{"configurable_logs":{"logs_disabled":false}}} - ``` -- expected: - - code 200 - - example body - ``` - {"data":[{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows if metrics are being received for the selected time range.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"mappings":[{"options":{"match":"null","result":{"color":"light-red","index":0,"text":"No metrics received - Check configuration"}},"type":"special"},{"options":{"from":0,"result":{"color":"light-red","index":1,"text":"Failed to collect metrics"},"to":0},"type":"range"},{"options":{"from":1,"result":{"color":"light-green","index":2,"text":"Receiving metrics"},"to":1000000},"type":"range"}],"noValue":"No data","unit":"string"}},"gridPos":{"h":2,"w":8,"x":0,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"]}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"title":"Metrics","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the timestamp of the latest metrics received for this integration in the last 24 hours.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"No data","unit":"dateTimeFromNow"}},"gridPos":{"h":2,"w":8,"x":8,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"],"fields":"Time"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"timeFrom":"now-24h","title":"Latest metrics received","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the installed version of this integration.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"1.0.1","unit":"string"}},"gridPos":{"h":2,"w":8,"x":16,"y":0},"pluginVersion":"v10.0.0","title":"Integration version","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of clusters that are reporting metrics from ActiveMQ.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":3},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Clusters","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of broker instances across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":3},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Brokers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of message producers active on destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":3},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":3},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been sent to destinations in a cluster","fieldConfig":{"defaults":{"color":{"fixedColor":"#C8F2C2","mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":9},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Enqueue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been acknowledged (and removed) from destinations in a cluster.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":9},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Dequeue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of temporary memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":0,"y":17},"id":8,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"text":{},"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_temp_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average temporary memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of store memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":8,"y":17},"id":9,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_store_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average store memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of broker memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":16,"y":17},"id":10,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average broker memory usage","type":"bargauge"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ cluster overview","uid":"apache-activemq-cluster-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of memory used by both topics and queues across brokers.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":0},"id":2,"options":{"minVizHeight":75,"minVizWidth":75,"orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Average broker memory usage","type":"gauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of store memory used by both topics and queues across brokers.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":0},"id":3,"options":{"minVizHeight":75,"minVizWidth":75,"orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_store_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Average store memory usage","type":"gauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of temporary memory used by both topics and queues across brokers.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":0},"id":4,"options":{"minVizHeight":75,"minVizWidth":75,"orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showThresholdLabels":false,"showThresholdMarkers":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_temp_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Average temporary memory usage","type":"gauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Recent number of unacknowledged messages on the broker.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":0},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (increase(activemq_message_total{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"__auto"}],"title":"Unacknowledged messages / $__interval","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Firing alerts for Apache ActiveMQ environment.","gridPos":{"h":6,"w":12,"x":0,"y":4},"id":6,"options":{"alertInstanceLabelFilter":"{job=~\"${job:regex}\", cluster=~\"${cluster:regex}\", activemq_cluster=~\"${activemq_cluster:regex}\", instance=~\"${instance:regex}\"}","alertName":"","dashboardAlerts":false,"datasource":{"uid":"${prometheus_datasource}"},"groupBy":[],"groupMode":"default","maxItems":5,"sortOrder":1,"stateFilter":{"error":true,"firing":true,"noData":true,"normal":true,"pending":true},"viewMode":"list"},"targets":[],"title":"ActiveMQ alerts","type":"alertlist"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of producers attached to destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":6,"w":6,"x":12,"y":4},"id":7,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}) + sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to destinations on the broker.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":6,"w":6,"x":18,"y":4},"id":8,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}) + sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages on queue destinations, including any that have been dispatched but not acknowledged.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":10},"id":9,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (activemq_queue_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Queue size","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The percentage of memory being used by topic and queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"max":100,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"percent"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":10},"id":10,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (activemq_queue_memory_percent_usage{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (activemq_topic_memory_percent_usage{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Destination memory usage","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been sent to the destination.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":18},"id":11,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Enqueue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been acknowledged (and removed) from the destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":18},"id":12,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster) (increase(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster) (increase(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Dequeue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average time a message was held across all destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"ms"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":26},"id":13,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, instance, job) (activemq_queue_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, instance, job) (activemq_topic_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Average enqueue time","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages across destinations that are expired.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":26},"id":14,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_queue_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - queue"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by(instance, activemq_cluster, job) (increase(activemq_topic_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - topic"}],"title":"Expired messages / $__interval","type":"timeseries"},{"collapsed":false,"datasource":{"uid":"${prometheus_datasource}"},"gridPos":{"h":1,"w":24,"x":0,"y":34},"id":15,"targets":[],"title":"JVM resources","type":"row"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The time spent performing recent garbage collections","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]},"unit":"s"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":35},"id":16,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"jvm_gc_duration_seconds{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"}","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Garbage collection duration","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The recent increase in the number of garbage collection events for the JVM.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":35},"id":17,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"increase(jvm_gc_collection_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", name=\"G1 Young Generation\"}[$__interval:])","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}}"}],"title":"Garbage collection count / $__interval","type":"timeseries"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Instance","multi":true,"name":"instance","options":[],"query":"label_values(activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"},instance)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ instance overview","uid":"apache-activemq-instance-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"editable":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows if logs are being received for the selected time range.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"mappings":[{"options":{"match":"null","result":{"color":"light-yellow","index":0,"text":"Failed to collect logs or no logs available"}},"type":"special"},{"options":{"from":0,"result":{"color":"light-yellow","index":1,"text":"Failed to collect logs or no logs available"},"to":0},"type":"range"},{"options":{"from":1,"result":{"color":"light-green","index":2,"text":"Receiving logs"},"to":1000000},"type":"range"}],"noValue":"No data","unit":"string"}},"gridPos":{"h":2,"w":8,"x":0,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"]}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"$loki_datasource"},"expr":"sum(count_over_time({job=~\"$job\"}[5m]))\n"}],"title":"Logs","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the timestamp of the latest logs received for this integration in the last 24 hours.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"No data","unit":"dateTimeFromNow"}},"gridPos":{"h":2,"w":8,"x":8,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"],"fields":"Time"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"$loki_datasource"},"expr":"sum(count_over_time({job=~\"$job\"}[5m]))\n"}],"timeFrom":"now-24h","title":"Latest logs received","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the installed version of this integration.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"1.0.1","unit":"string"}},"gridPos":{"h":2,"w":8,"x":16,"y":0},"pluginVersion":"v10.0.0","title":"Integration version","type":"stat"},{"datasource":{"type":"loki","uid":"${loki_datasource}"},"description":"Logs volume grouped by \"level\" label.","fieldConfig":{"defaults":{"custom":{"drawStyle":"bars","fillOpacity":50,"stacking":{"mode":"normal"}},"unit":"none"},"overrides":[{"matcher":{"id":"byRegexp","options":"(E|e)merg|(F|f)atal|(A|a)lert|(C|c)rit.*"},"properties":[{"id":"color","value":{"fixedColor":"purple","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(E|e)(rr.*|RR.*)"},"properties":[{"id":"color","value":{"fixedColor":"red","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(W|w)(arn.*|ARN.*|rn|RN)"},"properties":[{"id":"color","value":{"fixedColor":"orange","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(N|n)(otice|ote)|(I|i)(nf.*|NF.*)"},"properties":[{"id":"color","value":{"fixedColor":"green","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"dbg.*|DBG.*|(D|d)(EBUG|ebug)"},"properties":[{"id":"color","value":{"fixedColor":"blue","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"(T|t)(race|RACE)"},"properties":[{"id":"color","value":{"fixedColor":"light-blue","mode":"fixed"}}]},{"matcher":{"id":"byRegexp","options":"logs"},"properties":[{"id":"color","value":{"fixedColor":"text","mode":"fixed"}}]}]},"gridPos":{"h":6,"w":24,"y":3},"id":1,"interval":"30s","options":{"tooltip":{"mode":"multi","sort":"desc"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"${loki_datasource}"},"expr":"sum by (level) (count_over_time({job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\",level=~\"$level\"}\n|~ \"$regex_search\"\n\n[$__interval]))\n","legendFormat":"{{ level }}"}],"title":"Logs volume","transformations":[{"id":"renameByRegex","options":{"regex":"Value","renamePattern":"logs"}}],"type":"timeseries"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"gridPos":{"h":18,"w":24,"y":3},"id":2,"options":{"dedupStrategy":"exact","enableLogDetails":true,"prettifyLogMessage":true,"showTime":false,"wrapLogMessage":true},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"loki","uid":"${loki_datasource}"},"expr":"{job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\",level=~\"$level\"} \n|~ \"$regex_search\"\n\n\n"}],"title":"Logs","type":"logs"}],"refresh":"30s","schemaVersion":36,"tags":["apache-activemq-integration"],"templating":{"list":[{"label":"Loki data source","name":"loki_datasource","query":"loki","regex":"(?!grafanacloud.+usage-insights|grafanacloud.+alert-state-history).+","type":"datasource"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Job","multi":true,"name":"job","query":"label_values({job=~\"integrations/apache-activemq\"}, job)","refresh":2,"sort":1,"type":"query"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Activemq_cluster","multi":true,"name":"activemq_cluster","query":"label_values({job=~\"integrations/apache-activemq\",job=~\"$job\"}, activemq_cluster)","refresh":2,"sort":1,"type":"query"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Instance","multi":true,"name":"instance","query":"label_values({job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\"}, instance)","refresh":2,"sort":1,"type":"query"},{"allValue":".*","datasource":{"type":"loki","uid":"${loki_datasource}"},"includeAll":true,"label":"Level","multi":true,"name":"level","query":"label_values({job=~\"integrations/apache-activemq\",job=~\"$job\",activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\"}, level)","refresh":2,"sort":1,"type":"query"},{"current":{"selected":false,"text":"","value":""},"label":"Regex search","name":"regex_search","options":[{"selected":true,"text":"","value":""}],"query":"","type":"textbox"}]},"time":{"from":"now-30m","to":"now"},"timezone":"utc","title":"Apache ActiveMQ logs","uid":"apache-activemq-logs"},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of queues connected with the broker instance.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":0},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_queue_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Queues","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages in queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]}},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":0},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Queue size","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of producers attached to queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":0},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":0},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages sent to queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":4},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by enqueue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have been acknowledged (and removed) from queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":4},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster) ($k_selector, rate(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by dequeue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average time a message was held on queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"ms"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":12},"id":8,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster) ($k_selector, activemq_queue_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by average enqueue time","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have expired on queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":12},"id":9,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_queue_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by expired message rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average message size on queue destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"axisSoftMin":0,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":7,"w":24,"x":0,"y":20},"id":10,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_queue_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top queues by average message size","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Summary of queues showing queue name, enqueue and dequeue rate, average enqueue time, and average message size.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"custom":{"align":"center","cellOptions":{"type":"auto"},"inspect":false},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]}},"overrides":[{"matcher":{"id":"byName","options":"Average message size"},"properties":[{"id":"unit","value":"decbytes"}]},{"matcher":{"id":"byName","options":"Enqueue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Dequeue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Average enqueue time"},"properties":[{"id":"unit","value":"ms"}]},{"matcher":{"id":"byName","options":"ActiveMQ cluster"},"properties":[{"id":"links","value":[{"title":"Cluster link","url":"d/apache-activemq-cluster-overview?var-activemq_cluster=${__data.fields.activemq_cluster}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]},{"matcher":{"id":"byName","options":"Instance"},"properties":[{"id":"links","value":[{"title":"Instance link","url":"d/apache-activemq-instance-overview?var-instance=${__data.fields.instance}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]}]},"gridPos":{"h":7,"w":24,"x":0,"y":27},"id":11,"options":{"cellHeight":"sm","footer":{"countRows":false,"fields":"","reducer":["sum"],"show":false},"showHeader":true,"sortBy":[]},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval:])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}[$__rate_interval:])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_queue_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_queue_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"}],"title":"Queue summary","transformations":[{"id":"joinByField","options":{"byField":"destination","mode":"outer"}},{"id":"organize","options":{"indexByName":{},"renameByName":{"Time 1":"","Value #A":"Enqueue rate","Value #B":"Dequeue rate","Value #C":"Average enqueue time","Value #D":"Average message size","activemq_cluster 1":"ActiveMQ cluster","destination":"Destination","instance 1":"Instance"}}},{"id":"filterFieldsByName","options":{"include":{"names":["ActiveMQ cluster","Instance","Enqueue rate","Dequeue rate","Average enqueue time","Average message size","Destination"]}}}],"type":"table"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Instance","multi":true,"name":"instance","options":[],"query":"label_values(activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"},instance)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":"","current":{"text":"4","value":"4"},"hide":0,"includeAll":false,"label":"Top queue count","multi":false,"name":"k_selector","options":[{"text":"2","value":"2"},{"text":"4","value":"4"},{"text":"6","value":"6"},{"text":"8","value":"8"},{"text":"10","value":"10"}],"query":"2,4,6,8,10","refresh":0,"type":"custom"},{"current":{"selected":false,"text":"","value":""},"label":"Queue by name","name":"name","query":"","type":"textbox"}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ queue overview","uid":"apache-activemq-queue-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true},{"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of topics connected with the broker instance.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":0},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_topic_queue_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Topics","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of producers attached to topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":0},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":0},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average number of consumers per topic.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":0},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Average consumers per topic","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages are sent to topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":4},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by enqueue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have been acknowledged (and removed) from topic destinations","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":4},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by dequeue rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average time a message was held across all topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"ms"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":12},"id":8,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_topic_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by average enqueue time","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The rate messages have expired on topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"mps"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":12},"id":9,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, rate(activemq_topic_expired_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval]))","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by expired message rate","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to the most active/used topics.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":20},"id":10,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":true,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by consumers","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average message size on topic destinations.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"axisSoftMin":0,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"decbytes"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":20},"id":11,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"topk by(instance, activemq_cluster, job) ($k_selector, activemq_topic_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}} - {{instance}} - {{destination}}"}],"title":"Top topics by average message size","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Summary of topics showing topic name, enqueue and dequeue rate, average enqueue time, and average message size.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"custom":{"align":"left","cellOptions":{"type":"auto"},"inspect":false},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green"}]}},"overrides":[{"matcher":{"id":"byName","options":"Enqueue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Dequeue rate"},"properties":[{"id":"unit","value":"mps"}]},{"matcher":{"id":"byName","options":"Average enqueue time"},"properties":[{"id":"unit","value":"ms"}]},{"matcher":{"id":"byName","options":"Average message size"},"properties":[{"id":"unit","value":"decbytes"}]},{"matcher":{"id":"byName","options":"ActiveMQ cluster"},"properties":[{"id":"links","value":[{"title":"Cluster link","url":"d/apache-activemq-cluster-overview?var-activemq_cluster=${__data.fields.activemq_cluster}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]},{"matcher":{"id":"byName","options":"Instance"},"properties":[{"id":"links","value":[{"title":"Instance link","url":"d/apache-activemq-instance-overview?var-instance=${__data.fields.instance}\u0026${__url_time_range}\u0026var-datasource=${datasource}"}]}]}]},"gridPos":{"h":8,"w":24,"x":0,"y":28},"id":12,"options":{"cellHeight":"sm","footer":{"countRows":false,"fields":"","reducer":["sum"],"show":false},"showHeader":true},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"rate(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}[$__rate_interval])","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_topic_average_enqueue_time{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"},{"datasource":{"uid":"${prometheus_datasource}"},"expr":"activemq_topic_average_message_size{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", instance=~\"$instance\", destination!~\"ActiveMQ.Advisory.*\", destination=~\".*$name.*\"}","format":"table","intervalFactor":2,"legendFormat":"{{instance}}"}],"title":"Topic summary","transformations":[{"id":"joinByField","options":{"byField":"destination","mode":"outer"}},{"id":"organize","options":{"indexByName":{},"renameByName":{"Time 3":"","Value #A":"Enqueue rate","Value #B":"Dequeue rate","Value #C":"Average enqueue time","Value #D":"Average message size","activemq_cluster 1":"ActiveMQ cluster","destination":"Destination","instance 1":"Instance"}}},{"id":"filterFieldsByName","options":{"include":{"names":["ActiveMQ cluster","Instance","Enqueue rate","Dequeue rate","Average enqueue time","Average message size","Destination"]}}}],"type":"table"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Instance","multi":true,"name":"instance","options":[],"query":"label_values(activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\",activemq_cluster=~\"$activemq_cluster\"},instance)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":"","current":{"text":"4","value":"4"},"hide":0,"includeAll":false,"label":"Top topic count","multi":false,"name":"k_selector","options":[{"text":"2","value":"2"},{"text":"4","value":"4"},{"text":"6","value":"6"},{"text":"8","value":"8"},{"text":"10","value":"10"}],"query":"2,4,6,8,10","refresh":0,"type":"custom"},{"current":{"selected":false,"text":"","value":""},"label":"Topic by name","name":"name","query":"","type":"textbox"}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ topic overview","uid":"apache-activemq-topic-overview","version":0},"folder_name":"Integration - Apache ActiveMQ","overwrite":true}]} - ``` - -## Create the folders -- method POST -- path /api/folders -- body example: - ``` - {"title":"Integration - Apache ActiveMQ","uid":"integration---apache-activemq"} - ``` -- expected: - - code 200 - - body example - ``` - {"id":224,"uid":"integration---apache-activemq","orgId":1,"title":"Integration - Apache ActiveMQ","url":"/dashboards/f/integration---apache-activemq/integration-apache-activemq","hasAcl":false,"canSave":true,"canEdit":true,"canAdmin":true,"canDelete":true,"createdBy":"clement2b5f","created":"2025-05-23T15:17:59Z","updatedBy":"clement2b5f","updated":"2025-05-23T15:17:59Z","version":1} - ``` - -## Add the dashboard (Iterate for each dashboard of the integration) -- method POST -- path /api/dashboards/db -- body example: - ``` - {"dashboard":{"__inputs":[],"__requires":[],"annotations":{"list":[]},"description":"","editable":false,"gnetId":null,"graphTooltip":0,"hideControls":false,"id":null,"links":[{"asDropdown":false,"icon":"external link","includeVars":true,"keepTime":true,"tags":["apache-activemq-integration"],"targetBlank":false,"title":"Other Apache ActiveMQ dashboards","type":"dashboards","url":""}],"panels":[{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows if metrics are being received for the selected time range.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"mappings":[{"options":{"match":"null","result":{"color":"light-red","index":0,"text":"No metrics received - Check configuration"}},"type":"special"},{"options":{"from":0,"result":{"color":"light-red","index":1,"text":"Failed to collect metrics"},"to":0},"type":"range"},{"options":{"from":1,"result":{"color":"light-green","index":2,"text":"Receiving metrics"},"to":1000000},"type":"range"}],"noValue":"No data","unit":"string"}},"gridPos":{"h":2,"w":8,"x":0,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"]}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"title":"Metrics","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the timestamp of the latest metrics received for this integration in the last 24 hours.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"No data","unit":"dateTimeFromNow"}},"gridPos":{"h":2,"w":8,"x":8,"y":0},"options":{"colorMode":"background","graphMode":"none","reduceOptions":{"calcs":["lastNotNull"],"fields":"Time"}},"pluginVersion":"v10.0.0","targets":[{"datasource":{"type":"prometheus","uid":"$prometheus_datasource"},"expr":"sum(up{job=~\"$job\"})\n"}],"timeFrom":"now-24h","title":"Latest metrics received","type":"stat"},{"datasource":{"type":"datasource","uid":"-- Mixed --"},"description":"Shows the installed version of this integration.","fieldConfig":{"defaults":{"color":{"fixedColor":"text","mode":"fixed"},"noValue":"1.0.1","unit":"string"}},"gridPos":{"h":2,"w":8,"x":16,"y":0},"pluginVersion":"v10.0.0","title":"Integration version","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of clusters that are reporting metrics from ActiveMQ.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":0,"y":3},"id":2,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Clusters","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of broker instances across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":6,"y":3},"id":3,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"count (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Brokers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of message producers active on destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":12,"y":3},"id":4,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_producer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Producers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"The number of consumers subscribed to destinations across clusters.","fieldConfig":{"defaults":{"color":{"mode":"fixed"},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":4,"w":6,"x":18,"y":3},"id":5,"options":{"colorMode":"none","graphMode":"area","justifyMode":"auto","orientation":"auto","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"textMode":"auto"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum (activemq_queue_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}) + sum (activemq_topic_consumer_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\",destination!~\"ActiveMQ.Advisory.*\"})","format":"time_series","intervalFactor":2,"legendFormat":"__auto"}],"title":"Consumers","type":"stat"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been sent to destinations in a cluster","fieldConfig":{"defaults":{"color":{"fixedColor":"#C8F2C2","mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":9},"id":6,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_enqueue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Enqueue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Number of messages that have been acknowledged (and removed) from destinations in a cluster.","fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","axisShow":false,"barAlignment":0,"drawStyle":"line","fillOpacity":25,"gradientMode":"none","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":2,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"never","spanNulls":false,"stacking":{"group":"A","mode":"normal"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null}]},"unit":"none"},"overrides":[]},"gridPos":{"h":8,"w":12,"x":12,"y":9},"id":7,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"right","showLegend":true},"tooltip":{"mode":"multi","sort":"desc"}},"targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"sum by (activemq_cluster, job) (increase(activemq_queue_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"}[$__interval:])) + sum by (activemq_cluster, job) (increase(activemq_topic_dequeue_count{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\", destination!~\"ActiveMQ.Advisory.*\"}[$__interval:]))","format":"time_series","interval":"1m","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Dequeue / $__interval","type":"timeseries"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of temporary memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":0,"y":17},"id":8,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"text":{},"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_temp_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average temporary memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of store memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":8,"y":17},"id":9,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_store_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average store memory usage","type":"bargauge"},{"datasource":{"uid":"${prometheus_datasource}"},"description":"Average percentage of broker memory used across clusters.","fieldConfig":{"defaults":{"color":{"mode":"thresholds"},"mappings":[],"max":1,"min":0,"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"#EAB839","value":50},{"color":"red","value":70}]},"unit":"percentunit"},"overrides":[]},"gridPos":{"h":10,"w":8,"x":16,"y":17},"id":10,"options":{"displayMode":"gradient","minVizHeight":10,"minVizWidth":0,"orientation":"horizontal","reduceOptions":{"calcs":["lastNotNull"],"fields":"","values":false},"showUnfilled":false,"valueMode":"color"},"pluginVersion":"10.2.0-60139","targets":[{"datasource":{"uid":"${prometheus_datasource}"},"expr":"avg by (activemq_cluster, job) (activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\", activemq_cluster=~\"$activemq_cluster\"})","format":"time_series","intervalFactor":2,"legendFormat":"{{activemq_cluster}}"}],"title":"Average broker memory usage","type":"bargauge"}],"refresh":"30s","rows":[],"schemaVersion":14,"style":"dark","tags":["apache-activemq-integration"],"templating":{"list":[{"current":{},"hide":0,"label":"Data source","name":"prometheus_datasource","options":[],"query":"prometheus","refresh":1,"regex":"(?!grafanacloud-usage|grafanacloud-ml-metrics).+","type":"datasource"},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Job","multi":true,"name":"job","options":[],"query":"label_values(activemq_topic_producer_count,job)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".*","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"Cluster","multi":true,"name":"cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false},{"allValue":".+","current":{},"datasource":{"uid":"${prometheus_datasource}"},"hide":0,"includeAll":true,"label":"ActiveMQ cluster","multi":true,"name":"activemq_cluster","options":[],"query":"label_values(activemq_memory_usage_ratio{job=~\"$job\", cluster=~\"$cluster\"},activemq_cluster)","refresh":2,"regex":"","sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-30m","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],"time_options":["5m","15m","1h","6h","12h","24h","2d","7d","30d"]},"timezone":"default","title":"Apache ActiveMQ cluster overview","uid":"apache-activemq-cluster-overview","version":0},"folderUid":"integration---apache-activemq","overwrite":true,"message":"creating dashboard from the Cloud Connections plugin"} - ``` -- expected: - - code 200 - - example body - ``` - {"folderUid":"integration---apache-activemq","id":235,"slug":"apache-activemq-cluster-overview","status":"success","uid":"apache-activemq-cluster-overview","url":"/d/apache-activemq-cluster-overview/apache-activemq-cluster-overview","version":1} - ``` - -## Install the integration -- method POST -- path /api/plugin-proxy/grafana-easystart-app/integrations-api-admin/integrations/{slug}/install -- parameters: - - `{slug}` the integration -- body example: - ``` - {"configuration":{"configurable_logs":{"logs_disabled":false},"configurable_alerts":{"alerts_disabled":false}}} - ``` -- expected: - - code 204 - - no body - -# Uninstall one integration -- method POST -- path /api/plugin-proxy/grafana-easystart-app/integrations-api-admin/integrations/{slug}/uninstall -- parameters: - - `{slug}` the integration -- expected: - - code 204 - - no body - - -> WARNING. I have a doubt that the webclient is actually the one deleting the folders \ No newline at end of file diff --git a/internal/resources/integrations/client.go b/internal/resources/integrations/client.go index da69b7a6a..43ea89ecc 100644 --- a/internal/resources/integrations/client.go +++ b/internal/resources/integrations/client.go @@ -274,14 +274,12 @@ func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { return fmt.Errorf("failed to get integration details: %w", err) } - // Step 1: Delete the folder (dashboards are removed with it) + // Clean up dashboards and alerts folderUID := c.generateFolderUID(integration.Data.DashboardFolder) _ = c.DeleteFolder(ctx, folderUID) - - // Step 2: Remove rules _ = c.UninstallIntegrationRules(ctx, slug) - // Step 3: Call the uninstall API + // Remove install status in API (legacy behaviour) path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) err = c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) if err != nil { @@ -335,9 +333,7 @@ func shouldInstallRulesOnInstall(rolloutLevel *int) bool { return rolloutLevel != nil && *rolloutLevel >= RolloutLevelInstallOnly } -// shouldInstallRulesOnUpgrade returns true if rules should be (re)installed -// during an upgrade, based on whether rules already exist in Grafana Alerting -// and the integration's rollout level. +// shouldInstallRulesOnUpgrade returns true if rules are managed by Grafana Alerting func shouldInstallRulesOnUpgrade(rulesExistInGrafana bool, rolloutLevel *int) bool { if rolloutLevel == nil { return false diff --git a/internal/resources/integrations/resource_integration.go b/internal/resources/integrations/resource_integration.go index ac3a0b586..1b830daa7 100644 --- a/internal/resources/integrations/resource_integration.go +++ b/internal/resources/integrations/resource_integration.go @@ -25,7 +25,6 @@ Required access policy scopes: **Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. `, - CreateContext: createIntegration, ReadContext: readIntegration, UpdateContext: updateIntegration, @@ -41,15 +40,15 @@ Required access policy scopes: ForceNew: true, Description: "The slug of the integration to install (e.g., 'docker', 'linux-node').", }, - "installed": { - Type: schema.TypeBool, + "installed_version": { + Type: schema.TypeString, Computed: true, - Description: "Whether the integration is currently installed.", + Description: "The version of the installed integration.", }, - "version": { + "latest_version": { Type: schema.TypeString, Computed: true, - Description: "The version of the installed integration.", + Description: "Latest version available. Change the config or destroy and recreate to upgrade.", }, "name": { Type: schema.TypeString, @@ -61,11 +60,6 @@ Required access policy scopes: Computed: true, Description: "The dashboard folder associated with this integration.", }, - "grafana_managed_alerts_rollout_level": { - Type: schema.TypeInt, - Computed: true, - Description: "The Grafana Managed Alerts rollout level for this integration (0=Mimir, 1=Install, 2=Grafana).", - }, "configuration": { Type: schema.TypeList, Optional: true, @@ -127,7 +121,6 @@ func createIntegration(ctx context.Context, d *schema.ResourceData, meta interfa slug := d.Get("slug").(string) - // Check if integration already exists and is installed installed, err := client.IsIntegrationInstalled(ctx, slug) if err != nil { return diag.FromErr(fmt.Errorf("failed to check integration status: %w", err)) @@ -160,6 +153,8 @@ func readIntegration(ctx context.Context, d *schema.ResourceData, meta interface slug := d.Id() + // Get the *latest* integration data from API + // This can include a change in version, indicating an update being available integration, err := client.GetIntegration(ctx, slug) if err != nil { if err == ErrNotFound { @@ -170,11 +165,14 @@ func readIntegration(ctx context.Context, d *schema.ResourceData, meta interface d.Set("slug", integration.Data.Slug) d.Set("name", integration.Data.Name) - d.Set("version", integration.Data.Version) + d.Set("latest_version", integration.Data.Version) d.Set("dashboard_folder", integration.Data.DashboardFolder) - d.Set("installed", integration.Data.Installation != nil) - if integration.Data.GrafanaManagedAlertsRolloutLevel != nil { - d.Set("grafana_managed_alerts_rollout_level", *integration.Data.GrafanaManagedAlertsRolloutLevel) + + // Set installation data if available, otherwise unset from schema to force install + if integration.Data.Installation != nil { + d.Set("installed_version", integration.Data.Installation.Version) + } else { + schema.RemoveFromState(d, "Integration not installed") } // Set configuration if available @@ -191,52 +189,31 @@ func updateIntegration(ctx context.Context, d *schema.ResourceData, meta interfa if err != nil { return diag.FromErr(err) } - slug := d.Id() + // A manual change in config requires reinstall + // `updateIntegration` is not called on a drift between available version and installed version. if d.HasChange("configuration") { - config := parseInstallationConfig(d) - - integration, err := client.GetIntegration(ctx, slug) + err = client.UninstallIntegration(ctx, slug) if err != nil { - return diag.FromErr(fmt.Errorf("failed to get integration for upgrade: %w", err)) - } - - // Determine which namespace to use for GMA migration - namespace := resolveGrafanaRulesNamespace( - integration.Data.DashboardFolder, - integration.Data.RuleNamespace, - integration.Data.Name, - ) - rulesExistInGrafana := false - if namespace != "" { - rulesExistInGrafana, _ = client.CheckRulesExist(ctx, namespace) + if err == ErrNotFound { + // Integration is already uninstalled + return nil + } + return diag.FromErr(fmt.Errorf("failed to uninstall integration: %w", err)) } - // Remove old dashboards & alerts by deleting the folder - folderUID := client.generateFolderUID(integration.Data.DashboardFolder) - _ = client.DeleteFolder(ctx, folderUID) + // Clean re-install w. changed config + // Parse configuration + config := parseInstallationConfig(d) - // Install new dashboards - err = client.InstallDashboards(ctx, slug, config) + // Install the integration + err = client.InstallIntegration(ctx, slug, config) if err != nil { - return diag.FromErr(fmt.Errorf("failed to install new dashboards: %w", err)) - } - - // Handle rules migration based on rollout level - rolloutLevel := integration.Data.GrafanaManagedAlertsRolloutLevel - if shouldInstallRulesOnUpgrade(rulesExistInGrafana, rolloutLevel) { - err = client.InstallIntegrationRules(ctx, slug, config) - if err != nil { - return diag.FromErr(fmt.Errorf("failed to install updated rules: %w", err)) - } + return diag.FromErr(fmt.Errorf("failed to install integration: %w", err)) } - // Call the upgrade API - err = client.UpgradeIntegration(ctx, slug) - if err != nil { - return diag.FromErr(fmt.Errorf("failed to upgrade integration: %w", err)) - } + d.SetId(slug) } return readIntegration(ctx, d, meta) From 70dc430f4a8280c79963a118b4fcb3a66e3d38dd Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 26 Mar 2026 15:50:15 +0100 Subject: [PATCH 07/28] rename package --- .../resources/{integrations => cloudintegrations}/client.go | 2 +- .../resources/{integrations => cloudintegrations}/models.go | 2 +- .../resource_integration.go | 2 +- .../{integrations => cloudintegrations}/resources.go | 2 +- pkg/provider/resources.go | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename internal/resources/{integrations => cloudintegrations}/client.go (99%) rename internal/resources/{integrations => cloudintegrations}/models.go (99%) rename internal/resources/{integrations => cloudintegrations}/resource_integration.go (99%) rename internal/resources/{integrations => cloudintegrations}/resources.go (84%) diff --git a/internal/resources/integrations/client.go b/internal/resources/cloudintegrations/client.go similarity index 99% rename from internal/resources/integrations/client.go rename to internal/resources/cloudintegrations/client.go index 43ea89ecc..d3a44abe4 100644 --- a/internal/resources/integrations/client.go +++ b/internal/resources/cloudintegrations/client.go @@ -1,4 +1,4 @@ -package integrations +package cloudintegrations import ( "bytes" diff --git a/internal/resources/integrations/models.go b/internal/resources/cloudintegrations/models.go similarity index 99% rename from internal/resources/integrations/models.go rename to internal/resources/cloudintegrations/models.go index fdddbe2c4..b9f33e551 100644 --- a/internal/resources/integrations/models.go +++ b/internal/resources/cloudintegrations/models.go @@ -1,4 +1,4 @@ -package integrations +package cloudintegrations import "time" diff --git a/internal/resources/integrations/resource_integration.go b/internal/resources/cloudintegrations/resource_integration.go similarity index 99% rename from internal/resources/integrations/resource_integration.go rename to internal/resources/cloudintegrations/resource_integration.go index 1b830daa7..3a7b70dc4 100644 --- a/internal/resources/integrations/resource_integration.go +++ b/internal/resources/cloudintegrations/resource_integration.go @@ -1,4 +1,4 @@ -package integrations +package cloudintegrations import ( "context" diff --git a/internal/resources/integrations/resources.go b/internal/resources/cloudintegrations/resources.go similarity index 84% rename from internal/resources/integrations/resources.go rename to internal/resources/cloudintegrations/resources.go index 3efd013d6..fb2def87c 100644 --- a/internal/resources/integrations/resources.go +++ b/internal/resources/cloudintegrations/resources.go @@ -1,4 +1,4 @@ -package integrations +package cloudintegrations import ( "github.com/grafana/terraform-provider-grafana/v4/internal/common" diff --git a/pkg/provider/resources.go b/pkg/provider/resources.go index 63e8f922f..f43369ac3 100644 --- a/pkg/provider/resources.go +++ b/pkg/provider/resources.go @@ -7,12 +7,12 @@ import ( "github.com/grafana/terraform-provider-grafana/v4/internal/resources/appplatform" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/asserts" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/cloud" + "github.com/grafana/terraform-provider-grafana/v4/internal/resources/cloudintegrations" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/cloudprovider" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/connections" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/fleetmanagement" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/frontendo11y" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/grafana" - "github.com/grafana/terraform-provider-grafana/v4/internal/resources/integrations" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/k6" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/machinelearning" "github.com/grafana/terraform-provider-grafana/v4/internal/resources/oncall" @@ -68,7 +68,7 @@ func Resources() []*common.Resource { var resources []*common.Resource resources = append(resources, cloud.Resources...) resources = append(resources, grafana.Resources...) - resources = append(resources, integrations.Resources...) + resources = append(resources, cloudintegrations.Resources...) resources = append(resources, oncall.Resources...) resources = append(resources, machinelearning.Resources...) resources = append(resources, slo.Resources...) From 2cc0cf1a69291acd50a97d9189b77458b40cfd3b Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 26 Mar 2026 16:36:57 +0100 Subject: [PATCH 08/28] rebase cloudintegrationsapi on newer format --- internal/common/client.go | 6 +- .../cloudintegrationsapi}/client.go | 76 ++- .../cloudintegrationsapi/models}/models.go | 4 +- .../cloudintegrations/resource_integration.go | 454 ++++++++++-------- pkg/provider/configure_clients.go | 30 ++ 5 files changed, 309 insertions(+), 261 deletions(-) rename internal/{resources/cloudintegrations => common/cloudintegrationsapi}/client.go (87%) rename internal/{resources/cloudintegrations => common/cloudintegrationsapi/models}/models.go (97%) diff --git a/internal/common/client.go b/internal/common/client.go index a432e5fff..32feb0327 100644 --- a/internal/common/client.go +++ b/internal/common/client.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi" "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudproviderapi" "github.com/grafana/terraform-provider-grafana/v4/internal/common/connectionsapi" "github.com/grafana/terraform-provider-grafana/v4/internal/common/fleetmanagementapi" @@ -41,8 +42,9 @@ type Client struct { MLAPI *mlapi.Client OnCallClient *onCallAPI.Client SLOClient *slo.APIClient - CloudProviderAPI *cloudproviderapi.Client - ConnectionsAPIClient *connectionsapi.Client + CloudIntegrationsAPIClient *cloudintegrationsapi.Client + CloudProviderAPI *cloudproviderapi.Client + ConnectionsAPIClient *connectionsapi.Client FleetManagementClient *fleetmanagementapi.Client FrontendO11yAPIClient *frontendo11yapi.Client AssertsAPIClient *assertsapi.APIClient diff --git a/internal/resources/cloudintegrations/client.go b/internal/common/cloudintegrationsapi/client.go similarity index 87% rename from internal/resources/cloudintegrations/client.go rename to internal/common/cloudintegrationsapi/client.go index d3a44abe4..a4c06de6f 100644 --- a/internal/resources/cloudintegrations/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -1,4 +1,4 @@ -package cloudintegrations +package cloudintegrationsapi import ( "bytes" @@ -13,8 +13,10 @@ import ( "github.com/grafana/grafana-openapi-client-go/client/dashboards" "github.com/grafana/grafana-openapi-client-go/client/folders" - "github.com/grafana/grafana-openapi-client-go/models" + oapimodels "github.com/grafana/grafana-openapi-client-go/models" "github.com/hashicorp/go-retryablehttp" + + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi/models" ) const ( @@ -39,8 +41,8 @@ type Client struct { grafanaAPIHost string userAgent string defaultHeaders map[string]string - foldersClient folders.ClientService // Grafana OpenAPI client for folder operations - dashboardsClient dashboards.ClientService // Grafana OpenAPI client for dashboard operations + foldersClient folders.ClientService + dashboardsClient dashboards.ClientService } // NewClient creates a new integrations client @@ -53,13 +55,11 @@ func NewClient(grafanaAPIHost string, authToken string, client *http.Client, use } return &Client{ - authToken: authToken, - client: client, - grafanaAPIHost: grafanaAPIHost, - userAgent: userAgent, - defaultHeaders: defaultHeaders, - foldersClient: nil, // Will be set by the resource when available - dashboardsClient: nil, // Will be set by the resource when available + authToken: authToken, + client: client, + grafanaAPIHost: grafanaAPIHost, + userAgent: userAgent, + defaultHeaders: defaultHeaders, }, nil } @@ -74,15 +74,14 @@ func (c *Client) SetDashboardsClient(dashboardsClient dashboards.ClientService) } // ListIntegrations retrieves all integrations, optionally filtering by installed status -func (c *Client) ListIntegrations(ctx context.Context, installed bool) (*ListIntegrationsResponse, error) { +func (c *Client) ListIntegrations(ctx context.Context, installed bool) (*models.ListIntegrationsResponse, error) { path := fmt.Sprintf("%s/integrations", editorBasePath) - // Add query parameter if filtering by installed if installed { path += "?installed=true" } - var response ListIntegrationsResponse + var response models.ListIntegrationsResponse err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) if err != nil { return nil, fmt.Errorf("failed to list integrations: %w", err) @@ -92,10 +91,10 @@ func (c *Client) ListIntegrations(ctx context.Context, installed bool) (*ListInt } // GetIntegration retrieves a specific integration by slug -func (c *Client) GetIntegration(ctx context.Context, slug string) (*GetIntegrationResponse, error) { +func (c *Client) GetIntegration(ctx context.Context, slug string) (*models.GetIntegrationResponse, error) { path := fmt.Sprintf("%s/integrations/%s", editorBasePath, url.PathEscape(slug)) - var response GetIntegrationResponse + var response models.GetIntegrationResponse err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) if err != nil { return nil, fmt.Errorf("failed to get integration %s: %w", slug, err) @@ -105,14 +104,14 @@ func (c *Client) GetIntegration(ctx context.Context, slug string) (*GetIntegrati } // PostDashboards posts dashboards for an integration with the given configuration -func (c *Client) PostDashboards(ctx context.Context, slug string, config *InstallationConfig) (*GetDashboardsResponse, error) { +func (c *Client) PostDashboards(ctx context.Context, slug string, config *models.InstallationConfig) (*models.GetDashboardsResponse, error) { path := fmt.Sprintf("%s/integrations/%s/dashboards", adminBasePath, url.PathEscape(slug)) - requestBody := InstallIntegrationRequest{ + requestBody := models.InstallIntegrationRequest{ Configuration: config, } - var response GetDashboardsResponse + var response models.GetDashboardsResponse err := c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, &response) if err != nil { return nil, fmt.Errorf("failed to post dashboards for integration %s: %w", slug, err) @@ -121,19 +120,18 @@ func (c *Client) PostDashboards(ctx context.Context, slug string, config *Instal return &response, nil } -// generateFolderUID generates a folder UID from an integration slug +// generateFolderUID generates a folder UID from the given dashboard name func (c *Client) generateFolderUID(folderName string) string { - // Take in Dashboard Folder and sanitise the whitespace with dashes return strings.ReplaceAll(strings.ToLower(folderName), " ", "-") } -// CreateFolder creates a folder +// CreateFolder creates a dashboard folder func (c *Client) CreateFolder(ctx context.Context, title, uid string) error { if c.foldersClient == nil { return fmt.Errorf("folders client not available") } - body := models.CreateFolderCommand{ + body := oapimodels.CreateFolderCommand{ Title: title, UID: uid, } @@ -146,7 +144,7 @@ func (c *Client) CreateFolder(ctx context.Context, title, uid string) error { return nil } -// DeleteFolder deletes a folder +// DeleteFolder deletes a dashboard folder func (c *Client) DeleteFolder(ctx context.Context, uid string) error { if c.foldersClient == nil { return fmt.Errorf("folders client not available") @@ -164,26 +162,21 @@ func (c *Client) DeleteFolder(ctx context.Context, uid string) error { } // CreateDashboard creates a dashboard in the specified folder -func (c *Client) CreateDashboard(ctx context.Context, dashboard Dashboard, folderUID string) error { - - // Make a copy of the dashboard data to avoid modifying the original +func (c *Client) CreateDashboard(ctx context.Context, dashboard models.Dashboard, folderUID string) error { dashboardData := make(map[string]interface{}) for k, v := range dashboard.Dashboard { dashboardData[k] = v } - // Remove id from dashboard if present (similar to resource_dashboard.go) delete(dashboardData, "id") - // Convert the dashboard data to the proper format - dashboardCommand := models.SaveDashboardCommand{ + dashboardCommand := oapimodels.SaveDashboardCommand{ Dashboard: dashboardData, FolderUID: folderUID, Overwrite: dashboard.Overwrite, Message: "creating dashboard from the Cloud Connections plugin", } - // Use the OpenAPI client _, err := c.dashboardsClient.PostDashboard(&dashboardCommand) if err != nil { return fmt.Errorf("failed to create dashboard: %w", err) @@ -194,7 +187,7 @@ func (c *Client) CreateDashboard(ctx context.Context, dashboard Dashboard, folde // InstallDashboards creates the folder and dashboards for an integration. // Used for both install and upgrade -func (c *Client) InstallDashboards(ctx context.Context, slug string, config *InstallationConfig) error { +func (c *Client) InstallDashboards(ctx context.Context, slug string, config *models.InstallationConfig) error { integration, err := c.GetIntegration(ctx, slug) if err != nil { return fmt.Errorf("failed to get integration details: %w", err) @@ -226,13 +219,12 @@ func (c *Client) InstallDashboards(ctx context.Context, slug string, config *Ins } // InstallIntegration installs an integration with the given configuration -func (c *Client) InstallIntegration(ctx context.Context, slug string, config *InstallationConfig) error { +func (c *Client) InstallIntegration(ctx context.Context, slug string, config *models.InstallationConfig) error { integration, err := c.GetIntegration(ctx, slug) if err != nil { return fmt.Errorf("failed to get integration details: %w", err) } - // Step 1: Create folder and dashboards err = c.InstallDashboards(ctx, slug, config) if err != nil { return err @@ -240,7 +232,6 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In folderUID := c.generateFolderUID(integration.Data.DashboardFolder) - // Step 2: Install rules to Grafana Alerting if applicable if shouldInstallRulesOnInstall(integration.Data.GrafanaManagedAlertsRolloutLevel) { err = c.InstallIntegrationRules(ctx, slug, config) if err != nil { @@ -249,10 +240,9 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *In } } - // Step 3: Install the integration path := fmt.Sprintf("%s/integrations/%s/install", adminBasePath, url.PathEscape(slug)) - requestBody := InstallIntegrationRequest{ + requestBody := models.InstallIntegrationRequest{ Configuration: config, } @@ -274,12 +264,10 @@ func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { return fmt.Errorf("failed to get integration details: %w", err) } - // Clean up dashboards and alerts folderUID := c.generateFolderUID(integration.Data.DashboardFolder) _ = c.DeleteFolder(ctx, folderUID) _ = c.UninstallIntegrationRules(ctx, slug) - // Remove install status in API (legacy behaviour) path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) err = c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) if err != nil { @@ -300,10 +288,10 @@ func (c *Client) IsIntegrationInstalled(ctx context.Context, slug string) (bool, } // GetIntegrationRules fetches recording and alerting rule groups for an integration -func (c *Client) GetIntegrationRules(ctx context.Context, slug string) (*IntegrationRulesData, error) { +func (c *Client) GetIntegrationRules(ctx context.Context, slug string) (*models.IntegrationRulesData, error) { path := fmt.Sprintf("%s/integrations/%s/rules", adminBasePath, url.PathEscape(slug)) - var response IntegrationRulesResponse + var response models.IntegrationRulesResponse err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) if err != nil { return nil, fmt.Errorf("failed to get rules for integration %s: %w", slug, err) @@ -352,7 +340,7 @@ func shouldInstallRulesOnUpgrade(rulesExistInGrafana bool, rolloutLevel *int) bo // InstallIntegrationRules fetches rules from the integrations API and imports // them into Grafana's native alerting system via the conversion-prometheus API. // Source: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#compatible-endpoints -func (c *Client) InstallIntegrationRules(ctx context.Context, slug string, config *InstallationConfig) error { +func (c *Client) InstallIntegrationRules(ctx context.Context, slug string, config *models.InstallationConfig) error { if config != nil && config.ConfigurableAlerts != nil && config.ConfigurableAlerts.AlertsDisabled { @@ -378,7 +366,7 @@ func (c *Client) InstallIntegrationRules(ctx context.Context, slug string, confi return nil } - var allGroups []RuleGroup + var allGroups []models.RuleGroup allGroups = append(allGroups, rulesData.RecordingRules...) allGroups = append(allGroups, rulesData.AlertingRules...) @@ -386,7 +374,7 @@ func (c *Client) InstallIntegrationRules(ctx context.Context, slug string, confi return nil } - payload := map[string][]RuleGroup{ + payload := map[string][]models.RuleGroup{ namespace: allGroups, } diff --git a/internal/resources/cloudintegrations/models.go b/internal/common/cloudintegrationsapi/models/models.go similarity index 97% rename from internal/resources/cloudintegrations/models.go rename to internal/common/cloudintegrationsapi/models/models.go index b9f33e551..5f5ade067 100644 --- a/internal/resources/cloudintegrations/models.go +++ b/internal/common/cloudintegrationsapi/models/models.go @@ -1,4 +1,4 @@ -package cloudintegrations +package models import "time" @@ -37,7 +37,7 @@ type Installation struct { // InstallationConfig represents the configuration for installing an integration type InstallationConfig struct { - ConfigurableLogs *ConfigurableLogs `json:"configurable_logs,omitempty"` + ConfigurableLogs *ConfigurableLogs `json:"configurable_logs,omitempty"` ConfigurableAlerts *ConfigurableAlerts `json:"configurable_alerts,omitempty"` } diff --git a/internal/resources/cloudintegrations/resource_integration.go b/internal/resources/cloudintegrations/resource_integration.go index 3a7b70dc4..e4313cdf2 100644 --- a/internal/resources/cloudintegrations/resource_integration.go +++ b/internal/resources/cloudintegrations/resource_integration.go @@ -2,15 +2,74 @@ package cloudintegrations import ( "context" + "errors" "fmt" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/grafana/terraform-provider-grafana/v4/internal/common" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi" + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi/models" +) + +var ( + _ resource.ResourceWithConfigure = (*cloudIntegrationResource)(nil) + _ resource.ResourceWithImportState = (*cloudIntegrationResource)(nil) ) -func resourceIntegration() *common.Resource { - schema := &schema.Resource{ +var ( + resourceCloudIntegrationName = "grafana_cloud_integration" + resourceCloudIntegrationID = common.NewResourceID(common.StringIDField("slug")) +) + +func resourceCloudIntegration() *common.Resource { + return common.NewResource( + common.CategoryCloud, + resourceCloudIntegrationName, + resourceCloudIntegrationID, + &cloudIntegrationResource{}, + ) +} + +type configurableLogsModel struct { + LogsDisabled types.Bool `tfsdk:"logs_disabled"` +} + +type configurableAlertsModel struct { + AlertsDisabled types.Bool `tfsdk:"alerts_disabled"` +} + +type configurationModel struct { + ConfigurableLogs *configurableLogsModel `tfsdk:"configurable_logs"` + ConfigurableAlerts *configurableAlertsModel `tfsdk:"configurable_alerts"` +} + +type cloudIntegrationResourceModel struct { + ID types.String `tfsdk:"id"` + Slug types.String `tfsdk:"slug"` + InstalledVersion types.String `tfsdk:"installed_version"` + LatestVersion types.String `tfsdk:"latest_version"` + Name types.String `tfsdk:"name"` + DashboardFolder types.String `tfsdk:"dashboard_folder"` + Configuration *configurationModel `tfsdk:"configuration"` +} + +type cloudIntegrationResource struct { + client *cloudintegrationsapi.Client +} + +func (r *cloudIntegrationResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = resourceCloudIntegrationName +} + +func (r *cloudIntegrationResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ Description: ` Manages Grafana Cloud integrations. @@ -25,78 +84,61 @@ Required access policy scopes: **Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. `, - CreateContext: createIntegration, - ReadContext: readIntegration, - UpdateContext: updateIntegration, - DeleteContext: deleteIntegration, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - Schema: map[string]*schema.Schema{ - "slug": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The Terraform resource ID. Set to the integration slug.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "slug": schema.StringAttribute{ Description: "The slug of the integration to install (e.g., 'docker', 'linux-node').", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "installed_version": { - Type: schema.TypeString, - Computed: true, + "installed_version": schema.StringAttribute{ Description: "The version of the installed integration.", - }, - "latest_version": { - Type: schema.TypeString, Computed: true, - Description: "Latest version available. Change the config or destroy and recreate to upgrade.", }, - "name": { - Type: schema.TypeString, + "latest_version": schema.StringAttribute{ + Description: "The latest version available for this integration.", Computed: true, - Description: "The display name of the integration.", }, - "dashboard_folder": { - Type: schema.TypeString, + "name": schema.StringAttribute{ + Description: "The display name of the integration.", Computed: true, + }, + "dashboard_folder": schema.StringAttribute{ Description: "The dashboard folder associated with this integration.", + Computed: true, }, - "configuration": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, + }, + Blocks: map[string]schema.Block{ + "configuration": schema.SingleNestedBlock{ Description: "Configuration options for the integration.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "configurable_logs": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Logs configuration for the integration.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "logs_disabled": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether to disable logs collection for this integration.", - }, - }, + Blocks: map[string]schema.Block{ + "configurable_logs": schema.SingleNestedBlock{ + Description: "Logs configuration for the integration.", + Attributes: map[string]schema.Attribute{ + "logs_disabled": schema.BoolAttribute{ + Description: "Whether to disable logs collection for this integration.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), }, }, - "configurable_alerts": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Alerts configuration for the integration.", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "alerts_disabled": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether to disable alerts for this integration.", - }, - }, + }, + "configurable_alerts": schema.SingleNestedBlock{ + Description: "Alerts configuration for the integration.", + Attributes: map[string]schema.Attribute{ + "alerts_disabled": schema.BoolAttribute{ + Description: "Whether to disable alerts for this integration.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), }, }, }, @@ -104,222 +146,208 @@ Required access policy scopes: }, }, } - - return common.NewLegacySDKResource( - common.CategoryCloud, - "grafana_integration", - common.NewResourceID(common.StringIDField("slug")), - schema, - ) } -func createIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, err := getIntegrationsClient(meta) - if err != nil { - return diag.FromErr(err) +func (r *cloudIntegrationResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil || r.client != nil { + return } - slug := d.Get("slug").(string) - - installed, err := client.IsIntegrationInstalled(ctx, slug) - if err != nil { - return diag.FromErr(fmt.Errorf("failed to check integration status: %w", err)) + client, ok := req.ProviderData.(*common.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return } - if installed { - // Integration is already installed, just set the ID and read the state - d.SetId(slug) - return readIntegration(ctx, d, meta) + if client.CloudIntegrationsAPIClient == nil { + resp.Diagnostics.AddError( + "The Grafana Provider is missing a configuration for the Cloud Integrations API.", + "Ensure that url and auth are set in the provider configuration.", + ) + return } - // Parse configuration - config := parseInstallationConfig(d) + r.client = client.CloudIntegrationsAPIClient +} - // Install the integration - err = client.InstallIntegration(ctx, slug, config) - if err != nil { - return diag.FromErr(fmt.Errorf("failed to install integration: %w", err)) +func (r *cloudIntegrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan cloudIntegrationResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - d.SetId(slug) - return readIntegration(ctx, d, meta) -} + slug := plan.Slug.ValueString() -func readIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, err := getIntegrationsClient(meta) + installed, err := r.client.IsIntegrationInstalled(ctx, slug) if err != nil { - return diag.FromErr(err) + resp.Diagnostics.AddError("Failed to check integration status", err.Error()) + return } - slug := d.Id() - - // Get the *latest* integration data from API - // This can include a change in version, indicating an update being available - integration, err := client.GetIntegration(ctx, slug) - if err != nil { - if err == ErrNotFound { - return common.WarnMissing("integration", d) + if !installed { + config := toAPIConfig(plan.Configuration) + if err := r.client.InstallIntegration(ctx, slug, config); err != nil { + resp.Diagnostics.AddError("Failed to install integration", err.Error()) + return } - return diag.FromErr(fmt.Errorf("failed to get integration: %w", err)) } - d.Set("slug", integration.Data.Slug) - d.Set("name", integration.Data.Name) - d.Set("latest_version", integration.Data.Version) - d.Set("dashboard_folder", integration.Data.DashboardFolder) - - // Set installation data if available, otherwise unset from schema to force install - if integration.Data.Installation != nil { - d.Set("installed_version", integration.Data.Installation.Version) - } else { - schema.RemoveFromState(d, "Integration not installed") + integration, err := r.client.GetIntegration(ctx, slug) + if err != nil { + resp.Diagnostics.AddError("Failed to read integration after install", err.Error()) + return } - // Set configuration if available - if integration.Data.Installation != nil && integration.Data.Installation.Configuration != nil { - config := flattenInstallationConfig(integration.Data.Installation.Configuration) - d.Set("configuration", config) - } + plan.ID = plan.Slug + setModelFromAPI(&plan, integration) - return nil + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) } -func updateIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, err := getIntegrationsClient(meta) - if err != nil { - return diag.FromErr(err) +func (r *cloudIntegrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state cloudIntegrationResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - slug := d.Id() - - // A manual change in config requires reinstall - // `updateIntegration` is not called on a drift between available version and installed version. - if d.HasChange("configuration") { - err = client.UninstallIntegration(ctx, slug) - if err != nil { - if err == ErrNotFound { - // Integration is already uninstalled - return nil - } - return diag.FromErr(fmt.Errorf("failed to uninstall integration: %w", err)) - } - // Clean re-install w. changed config - // Parse configuration - config := parseInstallationConfig(d) + slug := state.Slug.ValueString() - // Install the integration - err = client.InstallIntegration(ctx, slug, config) - if err != nil { - return diag.FromErr(fmt.Errorf("failed to install integration: %w", err)) + integration, err := r.client.GetIntegration(ctx, slug) + if err != nil { + if errors.Is(err, cloudintegrationsapi.ErrNotFound) { + resp.State.RemoveResource(ctx) + return } + resp.Diagnostics.AddError("Failed to read integration", err.Error()) + return + } - d.SetId(slug) + if integration.Data.Installation == nil { + resp.State.RemoveResource(ctx) + return } - return readIntegration(ctx, d, meta) + state.ID = state.Slug + setModelFromAPI(&state, integration) + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) } -func deleteIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client, err := getIntegrationsClient(meta) - if err != nil { - return diag.FromErr(err) +func (r *cloudIntegrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan cloudIntegrationResourceModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - slug := d.Id() + slug := plan.Slug.ValueString() + plan.ID = plan.Slug - err = client.UninstallIntegration(ctx, slug) + err := r.client.UninstallIntegration(ctx, slug) if err != nil { - if err == ErrNotFound { - // Integration is already uninstalled - return nil + if errors.Is(err, cloudintegrationsapi.ErrNotFound) { + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + return } - return diag.FromErr(fmt.Errorf("failed to uninstall integration: %w", err)) + resp.Diagnostics.AddError("Failed to uninstall integration for update", err.Error()) + return } - return nil -} - -func getIntegrationsClient(meta interface{}) (*Client, error) { - client := meta.(*common.Client) - - // Get the auth token from the Grafana API config - authToken := "" - if client.GrafanaAPIConfig != nil { - authToken = client.GrafanaAPIConfig.APIKey + config := toAPIConfig(plan.Configuration) + if err := r.client.InstallIntegration(ctx, slug, config); err != nil { + resp.Diagnostics.AddError("Failed to install integration", err.Error()) + return } - // Create integrations client - integrationsClient, err := NewClient( - client.GrafanaAPIURL, - authToken, - nil, // Use default HTTP client - "terraform-provider-grafana", - nil, // No default headers for now - ) + integration, err := r.client.GetIntegration(ctx, slug) if err != nil { - return nil, fmt.Errorf("failed to create integrations client: %w", err) + resp.Diagnostics.AddError("Failed to read integration after update", err.Error()) + return + } + + setModelFromAPI(&plan, integration) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +func (r *cloudIntegrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state cloudIntegrationResourceModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - // Set the Grafana OpenAPI clients for folder and dashboard operations - // TODO: In the future we also want to use the convert-prometheus OpenAPI client for importing alerts & rules - if client.GrafanaAPI != nil { - integrationsClient.SetFoldersClient(client.GrafanaAPI.Folders) - integrationsClient.SetDashboardsClient(client.GrafanaAPI.Dashboards) + err := r.client.UninstallIntegration(ctx, state.Slug.ValueString()) + if err != nil && !errors.Is(err, cloudintegrationsapi.ErrNotFound) { + resp.Diagnostics.AddError("Failed to uninstall integration", err.Error()) } +} - return integrationsClient, nil +func (r *cloudIntegrationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("slug"), req, resp) } -func parseInstallationConfig(d *schema.ResourceData) *InstallationConfig { - configList := d.Get("configuration").([]interface{}) - if len(configList) == 0 { +func toAPIConfig(cfg *configurationModel) *models.InstallationConfig { + if cfg == nil { return nil } - configMap := configList[0].(map[string]interface{}) - config := &InstallationConfig{} + config := &models.InstallationConfig{} - // Parse configurable_logs - if logsConfigList, ok := configMap["configurable_logs"].([]interface{}); ok && len(logsConfigList) > 0 { - logsConfigMap := logsConfigList[0].(map[string]interface{}) - config.ConfigurableLogs = &ConfigurableLogs{ - LogsDisabled: logsConfigMap["logs_disabled"].(bool), + if cfg.ConfigurableLogs != nil { + config.ConfigurableLogs = &models.ConfigurableLogs{ + LogsDisabled: cfg.ConfigurableLogs.LogsDisabled.ValueBool(), } } - // Parse configurable_alerts - if alertsConfigList, ok := configMap["configurable_alerts"].([]interface{}); ok && len(alertsConfigList) > 0 { - alertsConfigMap := alertsConfigList[0].(map[string]interface{}) - config.ConfigurableAlerts = &ConfigurableAlerts{ - AlertsDisabled: alertsConfigMap["alerts_disabled"].(bool), + if cfg.ConfigurableAlerts != nil { + config.ConfigurableAlerts = &models.ConfigurableAlerts{ + AlertsDisabled: cfg.ConfigurableAlerts.AlertsDisabled.ValueBool(), } } return config } -func flattenInstallationConfig(config *InstallationConfig) []interface{} { - if config == nil { - return nil +func setModelFromAPI(model *cloudIntegrationResourceModel, integration *models.GetIntegrationResponse) { + model.Slug = types.StringValue(integration.Data.Slug) + model.Name = types.StringValue(integration.Data.Name) + model.LatestVersion = types.StringValue(integration.Data.Version) + model.DashboardFolder = types.StringValue(integration.Data.DashboardFolder) + + if integration.Data.Installation != nil { + model.InstalledVersion = types.StringValue(integration.Data.Installation.Version) } - result := make(map[string]interface{}) + if integration.Data.Installation != nil && integration.Data.Installation.Configuration != nil { + apiConfig := integration.Data.Installation.Configuration + cfg := &configurationModel{} - if config.ConfigurableLogs != nil { - result["configurable_logs"] = []interface{}{ - map[string]interface{}{ - "logs_disabled": config.ConfigurableLogs.LogsDisabled, - }, + if apiConfig.ConfigurableLogs != nil { + cfg.ConfigurableLogs = &configurableLogsModel{ + LogsDisabled: types.BoolValue(apiConfig.ConfigurableLogs.LogsDisabled), + } } - } - if config.ConfigurableAlerts != nil { - result["configurable_alerts"] = []interface{}{ - map[string]interface{}{ - "alerts_disabled": config.ConfigurableAlerts.AlertsDisabled, - }, + if apiConfig.ConfigurableAlerts != nil { + cfg.ConfigurableAlerts = &configurableAlertsModel{ + AlertsDisabled: types.BoolValue(apiConfig.ConfigurableAlerts.AlertsDisabled), + } } - } - return []interface{}{result} + model.Configuration = cfg + } } diff --git a/pkg/provider/configure_clients.go b/pkg/provider/configure_clients.go index 650cbdf2e..1712d107e 100644 --- a/pkg/provider/configure_clients.go +++ b/pkg/provider/configure_clients.go @@ -34,6 +34,7 @@ import ( "github.com/grafana/terraform-provider-grafana/v4/internal/common" + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi" "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudproviderapi" "github.com/grafana/terraform-provider-grafana/v4/internal/common/connectionsapi" "github.com/grafana/terraform-provider-grafana/v4/internal/common/fleetmanagementapi" @@ -49,6 +50,9 @@ func CreateClients(providerConfig ProviderConfig) (*common.Client, error) { if err = createGrafanaAPIClient(c, providerConfig); err != nil { return nil, err } + if err = createCloudIntegrationsClient(c, providerConfig); err != nil { + return nil, err + } if err = createGrafanaAppPlatformClient(c, providerConfig); err != nil { return nil, err } @@ -178,6 +182,32 @@ func createGrafanaAPIClient(client *common.Client, providerConfig ProviderConfig return nil } +func createCloudIntegrationsClient(client *common.Client, providerConfig ProviderConfig) error { + providerHeaders, err := getHTTPHeadersMap(providerConfig) + if err != nil { + return fmt.Errorf("failed to get provider default HTTP headers: %w", err) + } + + apiClient, err := cloudintegrationsapi.NewClient( + client.GrafanaAPIURL, + client.GrafanaAPIConfig.APIKey, + getRetryClient(providerConfig), + providerConfig.UserAgent.ValueString(), + providerHeaders, + ) + if err != nil { + return err + } + + if client.GrafanaAPI != nil { + apiClient.SetFoldersClient(client.GrafanaAPI.Folders) + apiClient.SetDashboardsClient(client.GrafanaAPI.Dashboards) + } + + client.CloudIntegrationsAPIClient = apiClient + return nil +} + func createGrafanaAppPlatformClient(client *common.Client, cfg ProviderConfig) error { rcfg := rest.Config{ UserAgent: cfg.UserAgent.ValueString(), From 2089427d009c182796a2e151102557cb0ef0419f Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 26 Mar 2026 17:07:29 +0100 Subject: [PATCH 09/28] Delete legacy doc --- docs/resources/integration.md | 123 ---------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 docs/resources/integration.md diff --git a/docs/resources/integration.md b/docs/resources/integration.md deleted file mode 100644 index c34db344a..000000000 --- a/docs/resources/integration.md +++ /dev/null @@ -1,123 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "grafana_integration Resource - terraform-provider-grafana" -subcategory: "Cloud" -description: |- - Manages Grafana Cloud integrations. - Official documentation https://grafana.com/docs/grafana-cloud/data-configuration/integrations/ - Required access policy scopes: - folders:readfolders:writedashboards:readdashboards:write - Note: This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. ---- - -# grafana_integration (Resource) - -Manages Grafana Cloud integrations. - -* [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) - -Required access policy scopes: - -* folders:read -* folders:write -* dashboards:read -* dashboards:write - -**Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. - -## Example Usage - -```terraform -# Install Docker integration with logs and alerts enabled -resource "grafana_integration" "docker" { - slug = "docker" - - configuration { - configurable_logs { - logs_disabled = false - } - configurable_alerts { - alerts_disabled = false - } - } -} - -# Install Linux Node integration with logs enabled but alerts disabled -resource "grafana_integration" "linux_node" { - slug = "linux-node" - - configuration { - configurable_logs { - logs_disabled = false - } - configurable_alerts { - alerts_disabled = true - } - } -} - -# Install Windows integration with minimal configuration -resource "grafana_integration" "windows" { - slug = "windows-exporter" -} - -# Output integration information -output "docker_integration" { - value = { - name = grafana_integration.docker.name - version = grafana_integration.docker.version - installed = grafana_integration.docker.installed - dashboard_folder = grafana_integration.docker.dashboard_folder - } -} -``` - - -## Schema - -### Required - -- `slug` (String) The slug of the integration to install (e.g., 'docker', 'linux-node'). - -### Optional - -- `configuration` (Block List, Max: 1) Configuration options for the integration. (see [below for nested schema](#nestedblock--configuration)) - -### Read-Only - -- `dashboard_folder` (String) The dashboard folder associated with this integration. -- `id` (String) The ID of this resource. -- `installed` (Boolean) Whether the integration is currently installed. -- `name` (String) The display name of the integration. -- `version` (String) The version of the installed integration. - - -### Nested Schema for `configuration` - -Optional: - -- `configurable_alerts` (Block List, Max: 1) Alerts configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_alerts)) -- `configurable_logs` (Block List, Max: 1) Logs configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_logs)) - - -### Nested Schema for `configuration.configurable_alerts` - -Optional: - -- `alerts_disabled` (Boolean) Whether to disable alerts for this integration. Defaults to `false`. - - - -### Nested Schema for `configuration.configurable_logs` - -Optional: - -- `logs_disabled` (Boolean) Whether to disable logs collection for this integration. Defaults to `false`. - -## Import - -Import is supported using the following syntax: - -```shell -terraform import grafana_integration.name "{{ slug }}" -``` From ddd27bb052e0aa81eed7cc415cfab05d9dd89596 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 26 Mar 2026 17:11:20 +0100 Subject: [PATCH 10/28] Rebase on plugin framework for cloud integration resource --- .../grafana_cloud_integration/import.sh | 1 + .../resource.tf | 14 +++++++------- .../resources/grafana_integration/import.sh | 1 - internal/common/cloudintegrationsapi/client.go | 18 ++++++++---------- ...ration.go => resource_cloud_integration.go} | 0 .../resources/cloudintegrations/resources.go | 2 +- 6 files changed, 17 insertions(+), 19 deletions(-) create mode 100644 examples/resources/grafana_cloud_integration/import.sh rename examples/resources/{grafana_integration => grafana_cloud_integration}/resource.tf (60%) delete mode 100644 examples/resources/grafana_integration/import.sh rename internal/resources/cloudintegrations/{resource_integration.go => resource_cloud_integration.go} (100%) diff --git a/examples/resources/grafana_cloud_integration/import.sh b/examples/resources/grafana_cloud_integration/import.sh new file mode 100644 index 000000000..3356b1881 --- /dev/null +++ b/examples/resources/grafana_cloud_integration/import.sh @@ -0,0 +1 @@ +terraform import grafana_cloud_integration.name "{{ slug }}" diff --git a/examples/resources/grafana_integration/resource.tf b/examples/resources/grafana_cloud_integration/resource.tf similarity index 60% rename from examples/resources/grafana_integration/resource.tf rename to examples/resources/grafana_cloud_integration/resource.tf index 4db78c6bf..3cbfc6ccc 100644 --- a/examples/resources/grafana_integration/resource.tf +++ b/examples/resources/grafana_cloud_integration/resource.tf @@ -1,5 +1,5 @@ # Install Docker integration with logs and alerts enabled -resource "grafana_integration" "docker" { +resource "grafana_cloud_integration" "docker" { slug = "docker" configuration { @@ -13,7 +13,7 @@ resource "grafana_integration" "docker" { } # Install Linux Node integration with logs enabled but alerts disabled -resource "grafana_integration" "linux_node" { +resource "grafana_cloud_integration" "linux_node" { slug = "linux-node" configuration { @@ -27,16 +27,16 @@ resource "grafana_integration" "linux_node" { } # Install Windows integration with minimal configuration -resource "grafana_integration" "windows" { +resource "grafana_cloud_integration" "windows" { slug = "windows-exporter" } # Output integration information output "docker_integration" { value = { - name = grafana_integration.docker.name - version = grafana_integration.docker.version - installed = grafana_integration.docker.installed - dashboard_folder = grafana_integration.docker.dashboard_folder + name = grafana_cloud_integration.docker.name + version = grafana_cloud_integration.docker.version + installed = grafana_cloud_integration.docker.installed + dashboard_folder = grafana_cloud_integration.docker.dashboard_folder } } diff --git a/examples/resources/grafana_integration/import.sh b/examples/resources/grafana_integration/import.sh deleted file mode 100644 index 07389126f..000000000 --- a/examples/resources/grafana_integration/import.sh +++ /dev/null @@ -1 +0,0 @@ -terraform import grafana_integration.name "{{ slug }}" diff --git a/internal/common/cloudintegrationsapi/client.go b/internal/common/cloudintegrationsapi/client.go index a4c06de6f..ff25d825e 100644 --- a/internal/common/cloudintegrationsapi/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -4,10 +4,13 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "maps" "net/http" "net/url" + "slices" "strings" "time" @@ -120,7 +123,7 @@ func (c *Client) PostDashboards(ctx context.Context, slug string, config *models return &response, nil } -// generateFolderUID generates a folder UID from the given dashboard name +// generateFolderUID generates a folder UID from the given dashboard_folder func (c *Client) generateFolderUID(folderName string) string { return strings.ReplaceAll(strings.ToLower(folderName), " ", "-") } @@ -163,10 +166,7 @@ func (c *Client) DeleteFolder(ctx context.Context, uid string) error { // CreateDashboard creates a dashboard in the specified folder func (c *Client) CreateDashboard(ctx context.Context, dashboard models.Dashboard, folderUID string) error { - dashboardData := make(map[string]interface{}) - for k, v := range dashboard.Dashboard { - dashboardData[k] = v - } + dashboardData := maps.Clone(dashboard.Dashboard) delete(dashboardData, "id") @@ -366,9 +366,7 @@ func (c *Client) InstallIntegrationRules(ctx context.Context, slug string, confi return nil } - var allGroups []models.RuleGroup - allGroups = append(allGroups, rulesData.RecordingRules...) - allGroups = append(allGroups, rulesData.AlertingRules...) + allGroups := slices.Concat(rulesData.RecordingRules, rulesData.AlertingRules) if len(allGroups) == 0 { return nil @@ -402,7 +400,7 @@ func (c *Client) UninstallIntegrationRules(ctx context.Context, slug string) err path := fmt.Sprintf("%s/%s", rulesConvertAPIPath, url.PathEscape(namespace)) err = c.doAPIRequest(ctx, http.MethodDelete, path, nil, nil) if err != nil { - if err == ErrNotFound { + if errors.Is(err, ErrNotFound) { return nil } return fmt.Errorf("failed to delete rule namespace %s: %w", namespace, err) @@ -415,7 +413,7 @@ func (c *Client) CheckRulesExist(ctx context.Context, namespace string) (bool, e path := fmt.Sprintf("%s/%s", rulesConvertAPIPath, url.PathEscape(namespace)) err := c.doAPIRequest(ctx, http.MethodGet, path, nil, nil) if err != nil { - if err == ErrNotFound { + if errors.Is(err, ErrNotFound) { return false, nil } return false, err diff --git a/internal/resources/cloudintegrations/resource_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go similarity index 100% rename from internal/resources/cloudintegrations/resource_integration.go rename to internal/resources/cloudintegrations/resource_cloud_integration.go diff --git a/internal/resources/cloudintegrations/resources.go b/internal/resources/cloudintegrations/resources.go index fb2def87c..abe95b1cb 100644 --- a/internal/resources/cloudintegrations/resources.go +++ b/internal/resources/cloudintegrations/resources.go @@ -5,5 +5,5 @@ import ( ) var Resources = []*common.Resource{ - resourceIntegration(), + resourceCloudIntegration(), } From 2ccfc31991e1b2d663dd362517f7b04b4d40bb67 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 26 Mar 2026 18:06:25 +0100 Subject: [PATCH 11/28] Bit more cleanup --- .../grafana_cloud_integration/resource.tf | 39 ++++++------ .../common/cloudintegrationsapi/client.go | 61 +------------------ .../cloudintegrationsapi/models/models.go | 36 ----------- .../resource_cloud_integration.go | 9 +++ 4 files changed, 32 insertions(+), 113 deletions(-) diff --git a/examples/resources/grafana_cloud_integration/resource.tf b/examples/resources/grafana_cloud_integration/resource.tf index 3cbfc6ccc..e861064c0 100644 --- a/examples/resources/grafana_cloud_integration/resource.tf +++ b/examples/resources/grafana_cloud_integration/resource.tf @@ -1,7 +1,7 @@ -# Install Docker integration with logs and alerts enabled -resource "grafana_cloud_integration" "docker" { - slug = "docker" - +# install linux-node integration +resource "grafana_cloud_integration" "linux-node" { + slug = "linux-node" + configuration { configurable_logs { logs_disabled = false @@ -12,10 +12,10 @@ resource "grafana_cloud_integration" "docker" { } } -# Install Linux Node integration with logs enabled but alerts disabled -resource "grafana_cloud_integration" "linux_node" { - slug = "linux-node" - +# install kafka integration w. alerts disabled +resource "grafana_cloud_integration" "kafka" { + slug = "kafka" + configuration { configurable_logs { logs_disabled = false @@ -26,17 +26,20 @@ resource "grafana_cloud_integration" "linux_node" { } } -# Install Windows integration with minimal configuration -resource "grafana_cloud_integration" "windows" { - slug = "windows-exporter" +# Output info +output "linux_node_integration" { + value = { + name = grafana_cloud_integration.linux-node.name + latest_version = grafana_cloud_integration.linux-node.latest_version + installed_version = grafana_cloud_integration.linux-node.installed_version + dashboard_folder = grafana_cloud_integration.linux-node.dashboard_folder + } } - -# Output integration information -output "docker_integration" { +output "kafka_integration" { value = { - name = grafana_cloud_integration.docker.name - version = grafana_cloud_integration.docker.version - installed = grafana_cloud_integration.docker.installed - dashboard_folder = grafana_cloud_integration.docker.dashboard_folder + name = grafana_cloud_integration.kafka.name + latest_version = grafana_cloud_integration.kafka.latest_version + installed_version = grafana_cloud_integration.kafka.installed_version + dashboard_folder = grafana_cloud_integration.kafka.dashboard_folder } } diff --git a/internal/common/cloudintegrationsapi/client.go b/internal/common/cloudintegrationsapi/client.go index ff25d825e..6317180c3 100644 --- a/internal/common/cloudintegrationsapi/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -23,8 +23,7 @@ import ( ) const ( - editorBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-editor" - adminBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-admin" + adminBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-admin" grafanaCloudPromUID = "grafanacloud-prom" rulesConvertAPIPath = "/api/convert/prometheus/config/v1/rules" @@ -76,26 +75,9 @@ func (c *Client) SetDashboardsClient(dashboardsClient dashboards.ClientService) c.dashboardsClient = dashboardsClient } -// ListIntegrations retrieves all integrations, optionally filtering by installed status -func (c *Client) ListIntegrations(ctx context.Context, installed bool) (*models.ListIntegrationsResponse, error) { - path := fmt.Sprintf("%s/integrations", editorBasePath) - - if installed { - path += "?installed=true" - } - - var response models.ListIntegrationsResponse - err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) - if err != nil { - return nil, fmt.Errorf("failed to list integrations: %w", err) - } - - return &response, nil -} - // GetIntegration retrieves a specific integration by slug func (c *Client) GetIntegration(ctx context.Context, slug string) (*models.GetIntegrationResponse, error) { - path := fmt.Sprintf("%s/integrations/%s", editorBasePath, url.PathEscape(slug)) + path := fmt.Sprintf("%s/integrations/%s", adminBasePath, url.PathEscape(slug)) var response models.GetIntegrationResponse err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &response) @@ -321,22 +303,6 @@ func shouldInstallRulesOnInstall(rolloutLevel *int) bool { return rolloutLevel != nil && *rolloutLevel >= RolloutLevelInstallOnly } -// shouldInstallRulesOnUpgrade returns true if rules are managed by Grafana Alerting -func shouldInstallRulesOnUpgrade(rulesExistInGrafana bool, rolloutLevel *int) bool { - if rolloutLevel == nil { - return false - } - level := *rolloutLevel - - if rulesExistInGrafana && level != RolloutLevelMimir { - return true - } - if !rulesExistInGrafana && level == RolloutLevelGrafana { - return true - } - return false -} - // InstallIntegrationRules fetches rules from the integrations API and imports // them into Grafana's native alerting system via the conversion-prometheus API. // Source: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#compatible-endpoints @@ -408,29 +374,6 @@ func (c *Client) UninstallIntegrationRules(ctx context.Context, slug string) err return nil } -// CheckRulesExist checks whether rules exist in Grafana for a given namespace -func (c *Client) CheckRulesExist(ctx context.Context, namespace string) (bool, error) { - path := fmt.Sprintf("%s/%s", rulesConvertAPIPath, url.PathEscape(namespace)) - err := c.doAPIRequest(ctx, http.MethodGet, path, nil, nil) - if err != nil { - if errors.Is(err, ErrNotFound) { - return false, nil - } - return false, err - } - return true, nil -} - -// UpgradeIntegration upgrades an installed integration to its latest version. -func (c *Client) UpgradeIntegration(ctx context.Context, slug string) error { - path := fmt.Sprintf("%s/integrations/%s/upgrade", adminBasePath, url.PathEscape(slug)) - err := c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) - if err != nil { - return fmt.Errorf("failed to upgrade integration %s: %w", slug, err) - } - return nil -} - var ( ErrNotFound = fmt.Errorf("not found") ErrUnauthorized = fmt.Errorf("request not authorized") diff --git a/internal/common/cloudintegrationsapi/models/models.go b/internal/common/cloudintegrationsapi/models/models.go index 5f5ade067..e6903eda4 100644 --- a/internal/common/cloudintegrationsapi/models/models.go +++ b/internal/common/cloudintegrationsapi/models/models.go @@ -51,11 +51,6 @@ type ConfigurableAlerts struct { AlertsDisabled bool `json:"alerts_disabled"` } -// ListIntegrationsResponse represents the response from the list integrations API -type ListIntegrationsResponse struct { - Data map[string]Integration `json:"data"` -} - // GetIntegrationResponse represents the response from the get integration API type GetIntegrationResponse struct { Data Integration `json:"data"` @@ -107,34 +102,3 @@ type IntegrationRulesData struct { AlertingRules []RuleGroup `json:"alerting_rules,omitempty"` } -// ImportRulesResponse is the response from POST /api/convert/prometheus/config/v1/rules -type ImportRulesResponse struct { - Message string `json:"message"` - Created int `json:"created"` - Updated int `json:"updated"` -} - -// CreateFolderRequest represents the request body for creating a folder -type CreateFolderRequest struct { - Title string `json:"title"` - UID string `json:"uid"` -} - -// CreateFolderResponse represents the response from creating a folder -type CreateFolderResponse struct { - ID int `json:"id"` - UID string `json:"uid"` - OrgID int `json:"orgId"` - Title string `json:"title"` - URL string `json:"url"` - HasACL bool `json:"hasAcl"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanAdmin bool `json:"canAdmin"` - CanDelete bool `json:"canDelete"` - CreatedBy string `json:"createdBy"` - Created time.Time `json:"created"` - UpdatedBy string `json:"updatedBy"` - Updated time.Time `json:"updated"` - Version int `json:"version"` -} diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index e4313cdf2..aaf21bda0 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -75,6 +75,14 @@ Manages Grafana Cloud integrations. * [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) +This provider lets you manage Grafana Cloud Integrations. +Configuration options include disabling logs and alerts. + +Please note: Grafana Cloud Integrations do not support in-place upgrades, and require a teardown and reapply to resolve version drift. +As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. + +Update, only triggered on config change, is implemented as a complete uninstall, then reinstall of the integration in question. + Required access policy scopes: * folders:read @@ -267,6 +275,7 @@ func (r *cloudIntegrationResource) Update(ctx context.Context, req resource.Upda config := toAPIConfig(plan.Configuration) if err := r.client.InstallIntegration(ctx, slug, config); err != nil { resp.Diagnostics.AddError("Failed to install integration", err.Error()) + resp.State.RemoveResource(ctx) return } From 25671c01fc1008681fed5440e912650b5334bf90 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 11:18:18 +0100 Subject: [PATCH 12/28] Commit integrationsapi testing --- .../client_internal_test.go | 147 +++++ .../cloudintegrationsapi/client_test.go | 586 ++++++++++++++++++ 2 files changed, 733 insertions(+) create mode 100644 internal/common/cloudintegrationsapi/client_internal_test.go create mode 100644 internal/common/cloudintegrationsapi/client_test.go diff --git a/internal/common/cloudintegrationsapi/client_internal_test.go b/internal/common/cloudintegrationsapi/client_internal_test.go new file mode 100644 index 000000000..66b60bafd --- /dev/null +++ b/internal/common/cloudintegrationsapi/client_internal_test.go @@ -0,0 +1,147 @@ +package cloudintegrationsapi + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestUnit_generateFolderUID tests that folder UID generation handles input in a similar manner to Grafana's +// integration plugin +func TestUnit_generateFolderUID(t *testing.T) { + t.Parallel() + + c := &Client{} + + tests := []struct { + name string + input string + expected string + }{ + {name: "spaces replaced with dashes", input: "Linux Node", expected: "linux-node"}, + {name: "single word lowercased", input: "Docker", expected: "docker"}, + {name: "empty string", input: "", expected: ""}, + {name: "already lowercase with dashes", input: "already-lower", expected: "already-lower"}, + {name: "multiple spaces", input: "A B C D", expected: "a-b-c-d"}, + {name: "mixed case no spaces", input: "GrafanaCloud", expected: "grafanacloud"}, + {name: "leading and trailing spaces", input: " Padded ", expected: "-padded-"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.expected, c.generateFolderUID(tt.input)) + }) + } +} + +// TestUnit_generateFolderUID_SpecialCharacters tests that non-alphanumeric characters +// are handled correctly by generateFolderUID. +func TestUnit_generateFolderUID_SpecialCharacters(t *testing.T) { + t.Parallel() + + c := &Client{} + + tests := []struct { + name string + input string + expected string + }{ + {name: "dots and slashes unchanged", input: "project.100/team02/linux-node", expected: "project.100/team02/linux-node"}, + {name: "special characters unchanged", input: "Hello @ World!", expected: "hello-@-world!"}, + {name: "consecutive spaces to dashes", input: "a b", expected: "a---b"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.expected, c.generateFolderUID(tt.input)) + }) + } +} + +// TestUnit_resolveGrafanaRulesNamespace tests the logic for determining the namespace +// used for Grafana Alerting rules when installing an integration. +// Priority: dashboard_folder > rule_namespace > "Integration - " +func TestUnit_resolveGrafanaRulesNamespace(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dashboardFolder string + ruleNamespace string + integrationName string + expected string + }{ + { + name: "dashboard_folder takes priority", + dashboardFolder: "My Folder", + ruleNamespace: "ns", + integrationName: "Docker", + expected: "My Folder", + }, + { + name: "falls back to rule_namespace", + dashboardFolder: "", + ruleNamespace: "ns", + integrationName: "Docker", + expected: "ns", + }, + { + name: "falls back to integration name", + dashboardFolder: "", + ruleNamespace: "", + integrationName: "Docker", + expected: "Integration - Docker", + }, + { + name: "returns empty when all empty", + dashboardFolder: "", + ruleNamespace: "", + integrationName: "", + expected: "", + }, + { + name: "dashboard_folder with empty others", + dashboardFolder: "Solo", + ruleNamespace: "", + integrationName: "", + expected: "Solo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := resolveGrafanaRulesNamespace(tt.dashboardFolder, tt.ruleNamespace, tt.integrationName) + assert.Equal(t, tt.expected, result) + }) + } +} + +// TestUnit_shouldInstallRulesOnInstall tests logic for determining whether rules should +// be installed to Grafana Alerting, during migration phase from Mimir to Grafana Alerting. +func TestUnit_shouldInstallRulesOnInstall(t *testing.T) { + t.Parallel() + + intPtr := func(v int) *int { return &v } + + tests := []struct { + name string + rolloutLevel *int + expected bool + }{ + {name: "nil rollout level", rolloutLevel: nil, expected: false}, + {name: "level 0 (Mimir)", rolloutLevel: intPtr(RolloutLevelMimir), expected: false}, + {name: "level 1 (InstallOnly)", rolloutLevel: intPtr(RolloutLevelInstallOnly), expected: true}, + {name: "level 2 (Grafana)", rolloutLevel: intPtr(RolloutLevelGrafana), expected: true}, + {name: "level above max", rolloutLevel: intPtr(99), expected: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.expected, shouldInstallRulesOnInstall(tt.rolloutLevel)) + }) + } +} diff --git a/internal/common/cloudintegrationsapi/client_test.go b/internal/common/cloudintegrationsapi/client_test.go new file mode 100644 index 000000000..24e8e294c --- /dev/null +++ b/internal/common/cloudintegrationsapi/client_test.go @@ -0,0 +1,586 @@ +package cloudintegrationsapi_test + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi" + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi/models" +) + +const ( + integrationsBasePath = "/api/plugin-proxy/grafana-easystart-app/integrations-api-admin" + rulesConvertPath = "/api/convert/prometheus/config/v1/rules" + cloudPromUID = "grafanacloud-prom" +) + +func newTestClient(t *testing.T, svr *httptest.Server) *cloudintegrationsapi.Client { + t.Helper() + c, err := cloudintegrationsapi.NewClient(svr.URL, "test-token", svr.Client(), "test-user-agent", map[string]string{"X-Custom": "header-value"}) + require.NoError(t, err) + return c +} + +func TestUnit_NewClient(t *testing.T) { + t.Parallel() + + t.Run("creates client with provided http.Client", func(t *testing.T) { + t.Parallel() + c, err := cloudintegrationsapi.NewClient("https://grafana.example.com", "my-token", &http.Client{}, "my-agent", map[string]string{"X-Foo": "bar"}) + require.NoError(t, err) + assert.NotNil(t, c) + }) + + t.Run("creates retry client when http.Client is nil", func(t *testing.T) { + t.Parallel() + c, err := cloudintegrationsapi.NewClient("https://grafana.example.com", "my-token", nil, "my-agent", nil) + require.NoError(t, err) + assert.NotNil(t, c) + }) +} + +func TestUnit_GetIntegration(t *testing.T) { + t.Parallel() + + t.Run("success with response deserialization", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, integrationsBasePath+"/integrations/docker", r.URL.Path) + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{ + Name: "Docker", + Slug: "docker", + Version: "2.1.0", + DashboardFolder: "Docker", + Installation: &models.Installation{Version: "2.1.0"}, + }, + }) + })) + defer svr.Close() + + c := newTestClient(t, svr) + resp, err := c.GetIntegration(context.Background(), "docker") + require.NoError(t, err) + assert.Equal(t, "Docker", resp.Data.Name) + assert.Equal(t, "docker", resp.Data.Slug) + assert.Equal(t, "2.1.0", resp.Data.Version) + assert.Equal(t, "Docker", resp.Data.DashboardFolder) + assert.NotNil(t, resp.Data.Installation) + assert.Equal(t, "2.1.0", resp.Data.Installation.Version) + }) + + t.Run("sets auth, content-type, user-agent, and default headers", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, "test-user-agent", r.Header.Get("User-Agent")) + assert.Equal(t, "header-value", r.Header.Get("X-Custom")) + _, _ = w.Write([]byte(`{"data":{}}`)) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.GetIntegration(context.Background(), "docker") + require.NoError(t, err) + }) + + t.Run("returns ErrNotFound on 404", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.GetIntegration(context.Background(), "nonexistent") + assert.Error(t, err) + assert.ErrorIs(t, err, cloudintegrationsapi.ErrNotFound) + }) + + t.Run("returns ErrUnauthorized on 401", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.GetIntegration(context.Background(), "docker") + assert.Error(t, err) + assert.ErrorIs(t, err, cloudintegrationsapi.ErrUnauthorized) + }) + + t.Run("returns error on 500", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error":"internal"}`)) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.GetIntegration(context.Background(), "docker") + assert.Error(t, err) + assert.Contains(t, err.Error(), "500") + }) +} + +func TestUnit_IsIntegrationInstalled(t *testing.T) { + t.Parallel() + + t.Run("returns true when installed", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{ + Slug: "docker", + Installation: &models.Installation{Version: "1.0.0"}, + }, + }) + })) + defer svr.Close() + + c := newTestClient(t, svr) + installed, err := c.IsIntegrationInstalled(context.Background(), "docker") + require.NoError(t, err) + assert.True(t, installed) + }) + + t.Run("returns false when not installed", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Slug: "docker"}, + }) + })) + defer svr.Close() + + c := newTestClient(t, svr) + installed, err := c.IsIntegrationInstalled(context.Background(), "docker") + require.NoError(t, err) + assert.False(t, installed) + }) + + t.Run("propagates error from GetIntegration", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.IsIntegrationInstalled(context.Background(), "docker") + assert.Error(t, err) + }) +} + +// --------------------------------------------------------------------------- +// Integrations & Rules API +// --------------------------------------------------------------------------- + +func TestUnit_GetIntegrationRules(t *testing.T) { + t.Parallel() + + t.Run("success with response deserialization", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, integrationsBasePath+"/integrations/docker/rules", r.URL.Path) + + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + Namespace: "Docker", + RecordingRules: []models.RuleGroup{ + {Name: "recording_group", Rules: []models.Rule{{Record: "job:up:sum", Expr: "sum(up)"}}}, + }, + AlertingRules: []models.RuleGroup{ + {Name: "alerting_group", Rules: []models.Rule{{Alert: "HighErrors", Expr: "rate(errors[5m]) > 0.1"}}}, + }, + }, + }) + })) + defer svr.Close() + + c := newTestClient(t, svr) + rules, err := c.GetIntegrationRules(context.Background(), "docker") + require.NoError(t, err) + assert.Equal(t, "Docker", rules.Namespace) + assert.Len(t, rules.RecordingRules, 1) + assert.Len(t, rules.AlertingRules, 1) + assert.Equal(t, "recording_group", rules.RecordingRules[0].Name) + assert.Equal(t, "alerting_group", rules.AlertingRules[0].Name) + }) + + t.Run("returns ErrNotFound on 404", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.GetIntegrationRules(context.Background(), "nonexistent") + assert.Error(t, err) + assert.ErrorIs(t, err, cloudintegrationsapi.ErrNotFound) + }) + + t.Run("returns error on 500", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error":"internal"}`)) + })) + defer svr.Close() + + c := newTestClient(t, svr) + _, err := c.GetIntegrationRules(context.Background(), "docker") + assert.Error(t, err) + assert.Contains(t, err.Error(), "500") + }) +} + +func TestUnit_UninstallIntegration(t *testing.T) { + t.Parallel() + + dockerIntegration := models.GetIntegrationResponse{ + Data: models.Integration{ + Slug: "docker", + Name: "Docker", + DashboardFolder: "Docker", + }, + } + + t.Run("success", func(t *testing.T) { + t.Parallel() + var uninstallCalled bool + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker"): + _ = json.NewEncoder(w).Encode(dockerIntegration) + case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, rulesConvertPath): + w.WriteHeader(http.StatusOK) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/integrations/docker/uninstall"): + uninstallCalled = true + w.WriteHeader(http.StatusOK) + default: + w.WriteHeader(http.StatusOK) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegration(context.Background(), "docker") + require.NoError(t, err) + assert.True(t, uninstallCalled, "uninstall API endpoint should be called") + }) + + t.Run("propagates error from GetIntegration", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`server error`)) + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegration(context.Background(), "docker") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get integration details") + }) + + t.Run("propagates error from uninstall API", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(dockerIntegration) + case r.Method == http.MethodDelete: + w.WriteHeader(http.StatusOK) + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/uninstall"): + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"error":"failed"}`)) + default: + w.WriteHeader(http.StatusOK) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegration(context.Background(), "docker") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to uninstall integration docker") + }) +} + +// --------------------------------------------------------------------------- +// Rules Installation - Temporary during migration to Grafana Alerting +// --------------------------------------------------------------------------- + +func TestUnit_InstallIntegrationRules(t *testing.T) { + t.Parallel() + + t.Run("success: fetches rules, resolves namespace, posts to convert API", func(t *testing.T) { + t.Parallel() + var convertCalled bool + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker/rules"): + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + RecordingRules: []models.RuleGroup{ + {Name: "rec", Rules: []models.Rule{{Record: "rec:metric", Expr: "sum(up)"}}}, + }, + AlertingRules: []models.RuleGroup{ + {Name: "alert", Rules: []models.Rule{{Alert: "TestAlert", Expr: "up == 0"}}}, + }, + }, + }) + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker"): + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{ + Name: "Docker", + DashboardFolder: "Docker", + }, + }) + case r.Method == http.MethodPost && r.URL.Path == rulesConvertPath: + convertCalled = true + assert.Equal(t, cloudPromUID, r.Header.Get("X-Grafana-Alerting-Datasource-UID")) + + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + var payload map[string][]models.RuleGroup + assert.NoError(t, json.Unmarshal(body, &payload)) + assert.Contains(t, payload, "Docker") + assert.Len(t, payload["Docker"], 2) + w.WriteHeader(http.StatusOK) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.InstallIntegrationRules(context.Background(), "docker", nil) + require.NoError(t, err) + assert.True(t, convertCalled, "convert API should be called") + }) + + t.Run("skips when alerts disabled", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Errorf("no requests expected when alerts are disabled, got: %s %s", r.Method, r.URL.Path) + })) + defer svr.Close() + + c := newTestClient(t, svr) + config := &models.InstallationConfig{ + ConfigurableAlerts: &models.ConfigurableAlerts{AlertsDisabled: true}, + } + err := c.InstallIntegrationRules(context.Background(), "docker", config) + require.NoError(t, err) + }) + + t.Run("skips when no rule groups returned", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/rules"): + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{}, + }) + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, + }) + default: + t.Errorf("unexpected request (no POST expected): %s %s", r.Method, r.URL.Path) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.InstallIntegrationRules(context.Background(), "docker", nil) + require.NoError(t, err) + }) + + t.Run("skips when namespace resolves to empty", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/rules"): + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + RecordingRules: []models.RuleGroup{{Name: "rec", Rules: []models.Rule{{Record: "m", Expr: "1"}}}}, + }, + }) + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "", DashboardFolder: "", RuleNamespace: ""}, + }) + default: + t.Errorf("unexpected request (no POST expected): %s %s", r.Method, r.URL.Path) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.InstallIntegrationRules(context.Background(), "test", nil) + require.NoError(t, err) + }) + + t.Run("propagates error from GetIntegrationRules", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`server error`)) + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.InstallIntegrationRules(context.Background(), "docker", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get integration rules") + }) + + t.Run("propagates error from convert API", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/rules"): + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + AlertingRules: []models.RuleGroup{{Name: "a", Rules: []models.Rule{{Alert: "X", Expr: "1"}}}}, + }, + }) + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, + }) + case r.Method == http.MethodPost && r.URL.Path == rulesConvertPath: + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`conversion failed`)) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.InstallIntegrationRules(context.Background(), "docker", nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "500") + }) +} + +func TestUnit_UninstallIntegrationRules(t *testing.T) { + t.Parallel() + + t.Run("success: deletes rule namespace", func(t *testing.T) { + t.Parallel() + var deleteCalled bool + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker"): + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, + }) + case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, rulesConvertPath): + deleteCalled = true + assert.Equal(t, rulesConvertPath+"/Docker", r.URL.Path) + w.WriteHeader(http.StatusOK) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegrationRules(context.Background(), "docker") + require.NoError(t, err) + assert.True(t, deleteCalled, "DELETE to rules convert API should be called") + }) + + t.Run("ignores 404 on DELETE", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, + }) + case r.Method == http.MethodDelete: + w.WriteHeader(http.StatusNotFound) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegrationRules(context.Background(), "docker") + require.NoError(t, err) + }) + + t.Run("skips when namespace resolves to empty", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{}, + }) + default: + t.Errorf("unexpected request (no DELETE expected): %s %s", r.Method, r.URL.Path) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegrationRules(context.Background(), "test") + require.NoError(t, err) + }) + + t.Run("propagates error from GetIntegration", func(t *testing.T) { + t.Parallel() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`server error`)) + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegrationRules(context.Background(), "docker") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get integration details") + }) + + t.Run("propagates non-404 error from DELETE", func(t *testing.T) { + t.Parallel() + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet: + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{DashboardFolder: "Docker"}, + }) + case r.Method == http.MethodDelete: + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`server error`)) + } + })) + defer svr.Close() + + c := newTestClient(t, svr) + err := c.UninstallIntegrationRules(context.Background(), "docker") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to delete rule namespace") + }) +} From e8a5329dbe735b2490c14614cb9e4a2c0cb08a95 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 11:29:01 +0100 Subject: [PATCH 13/28] Add missing folder and dashboard locks and update logic to match --- internal/common/client.go | 14 ++++++ .../common/cloudintegrationsapi/client.go | 15 +++++- .../resource_cloud_integration.go | 48 ++++++++++++++----- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/internal/common/client.go b/internal/common/client.go index 32feb0327..992091b57 100644 --- a/internal/common/client.go +++ b/internal/common/client.go @@ -84,6 +84,13 @@ func WithFolderMutex[T schema.CreateContextFunc | schema.ReadContextFunc | schem } } +// WithFolderLock runs f while holding the folder mutex. Used by Plugin Framework resources that need to serialize folder API calls. +func (c *Client) WithFolderLock(f func()) { + c.folderMutex.Lock() + defer c.folderMutex.Unlock() + f() +} + // WithDashboardMutex is a helper function that wraps a CRUD Terraform function with a mutex. func WithDashboardMutex[T schema.CreateContextFunc | schema.ReadContextFunc | schema.UpdateContextFunc | schema.DeleteContextFunc](f T) T { return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -94,6 +101,13 @@ func WithDashboardMutex[T schema.CreateContextFunc | schema.ReadContextFunc | sc } } +// WithDashboardLock runs f while holding the dashboard mutex. Used by Plugin Framework resources that need to serialize dashboard API calls. +func (c *Client) WithDashboardLock(f func()) { + c.dashboardMutex.Lock() + defer c.dashboardMutex.Unlock() + f() +} + func (c *Client) GrafanaSubpath(path string) string { path = strings.TrimPrefix(path, c.GrafanaAPIURLParsed.Path) return c.GrafanaAPIURLParsed.JoinPath(path).String() diff --git a/internal/common/cloudintegrationsapi/client.go b/internal/common/cloudintegrationsapi/client.go index 6317180c3..51e78ed82 100644 --- a/internal/common/cloudintegrationsapi/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -148,6 +148,10 @@ func (c *Client) DeleteFolder(ctx context.Context, uid string) error { // CreateDashboard creates a dashboard in the specified folder func (c *Client) CreateDashboard(ctx context.Context, dashboard models.Dashboard, folderUID string) error { + if c.dashboardsClient == nil { + return fmt.Errorf("dashboards client not available") + } + dashboardData := maps.Clone(dashboard.Dashboard) delete(dashboardData, "id") @@ -214,10 +218,17 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *mo folderUID := c.generateFolderUID(integration.Data.DashboardFolder) + var success bool + defer func() { + if !success { + _ = c.DeleteFolder(ctx, folderUID) + _ = c.UninstallIntegrationRules(ctx, slug) + } + }() + if shouldInstallRulesOnInstall(integration.Data.GrafanaManagedAlertsRolloutLevel) { err = c.InstallIntegrationRules(ctx, slug, config) if err != nil { - _ = c.DeleteFolder(ctx, folderUID) return fmt.Errorf("failed to install integration rules: %w", err) } } @@ -230,10 +241,10 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *mo err = c.doAPIRequest(ctx, http.MethodPost, path, &requestBody, nil) if err != nil { - _ = c.DeleteFolder(ctx, folderUID) return fmt.Errorf("failed to install integration %s: %w", slug, err) } + success = true return nil } diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index aaf21bda0..903db47ca 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -61,7 +61,8 @@ type cloudIntegrationResourceModel struct { } type cloudIntegrationResource struct { - client *cloudintegrationsapi.Client + client *cloudintegrationsapi.Client + commonClient *common.Client } func (r *cloudIntegrationResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -179,6 +180,7 @@ func (r *cloudIntegrationResource) Configure(_ context.Context, req resource.Con } r.client = client.CloudIntegrationsAPIClient + r.commonClient = client } func (r *cloudIntegrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { @@ -199,8 +201,14 @@ func (r *cloudIntegrationResource) Create(ctx context.Context, req resource.Crea if !installed { config := toAPIConfig(plan.Configuration) - if err := r.client.InstallIntegration(ctx, slug, config); err != nil { - resp.Diagnostics.AddError("Failed to install integration", err.Error()) + var installErr error + r.commonClient.WithFolderLock(func() { + r.commonClient.WithDashboardLock(func() { + installErr = r.client.InstallIntegration(ctx, slug, config) + }) + }) + if installErr != nil { + resp.Diagnostics.AddError("Failed to install integration", installErr.Error()) return } } @@ -261,20 +269,31 @@ func (r *cloudIntegrationResource) Update(ctx context.Context, req resource.Upda slug := plan.Slug.ValueString() plan.ID = plan.Slug - err := r.client.UninstallIntegration(ctx, slug) - if err != nil { - if errors.Is(err, cloudintegrationsapi.ErrNotFound) { + var uninstallErr error + r.commonClient.WithFolderLock(func() { + r.commonClient.WithDashboardLock(func() { + uninstallErr = r.client.UninstallIntegration(ctx, slug) + }) + }) + if uninstallErr != nil { + if errors.Is(uninstallErr, cloudintegrationsapi.ErrNotFound) { diags = resp.State.Set(ctx, plan) resp.Diagnostics.Append(diags...) return } - resp.Diagnostics.AddError("Failed to uninstall integration for update", err.Error()) + resp.Diagnostics.AddError("Failed to uninstall integration for update", uninstallErr.Error()) return } config := toAPIConfig(plan.Configuration) - if err := r.client.InstallIntegration(ctx, slug, config); err != nil { - resp.Diagnostics.AddError("Failed to install integration", err.Error()) + var installErr error + r.commonClient.WithFolderLock(func() { + r.commonClient.WithDashboardLock(func() { + installErr = r.client.InstallIntegration(ctx, slug, config) + }) + }) + if installErr != nil { + resp.Diagnostics.AddError("Failed to install integration", installErr.Error()) resp.State.RemoveResource(ctx) return } @@ -299,9 +318,14 @@ func (r *cloudIntegrationResource) Delete(ctx context.Context, req resource.Dele return } - err := r.client.UninstallIntegration(ctx, state.Slug.ValueString()) - if err != nil && !errors.Is(err, cloudintegrationsapi.ErrNotFound) { - resp.Diagnostics.AddError("Failed to uninstall integration", err.Error()) + var uninstallErr error + r.commonClient.WithFolderLock(func() { + r.commonClient.WithDashboardLock(func() { + uninstallErr = r.client.UninstallIntegration(ctx, state.Slug.ValueString()) + }) + }) + if uninstallErr != nil && !errors.Is(uninstallErr, cloudintegrationsapi.ErrNotFound) { + resp.Diagnostics.AddError("Failed to uninstall integration", uninstallErr.Error()) } } From 15d5c6907dfbed24420aab97b273a186b9df3f62 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 11:33:15 +0100 Subject: [PATCH 14/28] Add integration docs --- docs/resources/cloud_integration.md | 139 ++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/resources/cloud_integration.md diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md new file mode 100644 index 000000000..90770e9fb --- /dev/null +++ b/docs/resources/cloud_integration.md @@ -0,0 +1,139 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "grafana_cloud_integration Resource - terraform-provider-grafana" +subcategory: "Cloud" +description: |- + Manages Grafana Cloud integrations. + Official documentation https://grafana.com/docs/grafana-cloud/data-configuration/integrations/ + This provider lets you manage Grafana Cloud Integrations. + Configuration options include disabling logs and alerts. + Please note: Grafana Cloud Integrations do not support in-place upgrades, and require a teardown and reapply to resolve version drift. + As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. + Update, only triggered on config change, is implemented as a complete uninstall, then reinstall of the integration in question. + Required access policy scopes: + folders:readfolders:writedashboards:readdashboards:write + Note: This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. +--- + +# grafana_cloud_integration (Resource) + +Manages Grafana Cloud integrations. + +* [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) + +This provider lets you manage Grafana Cloud Integrations. +Configuration options include disabling logs and alerts. + +Please note: Grafana Cloud Integrations do not support in-place upgrades, and require a teardown and reapply to resolve version drift. +As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. + +Update, only triggered on config change, is implemented as a complete uninstall, then reinstall of the integration in question. + +Required access policy scopes: + +* folders:read +* folders:write +* dashboards:read +* dashboards:write + +**Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. + +## Example Usage + +```terraform +# install linux-node integration +resource "grafana_cloud_integration" "linux-node" { + slug = "linux-node" + + configuration { + configurable_logs { + logs_disabled = false + } + configurable_alerts { + alerts_disabled = false + } + } +} + +# install kafka integration w. alerts disabled +resource "grafana_cloud_integration" "kafka" { + slug = "kafka" + + configuration { + configurable_logs { + logs_disabled = false + } + configurable_alerts { + alerts_disabled = true + } + } +} + +# Output info +output "linux_node_integration" { + value = { + name = grafana_cloud_integration.linux-node.name + latest_version = grafana_cloud_integration.linux-node.latest_version + installed_version = grafana_cloud_integration.linux-node.installed_version + dashboard_folder = grafana_cloud_integration.linux-node.dashboard_folder + } +} +output "kafka_integration" { + value = { + name = grafana_cloud_integration.kafka.name + latest_version = grafana_cloud_integration.kafka.latest_version + installed_version = grafana_cloud_integration.kafka.installed_version + dashboard_folder = grafana_cloud_integration.kafka.dashboard_folder + } +} +``` + + +## Schema + +### Required + +- `slug` (String) The slug of the integration to install (e.g., 'docker', 'linux-node'). + +### Optional + +- `configuration` (Block, Optional) Configuration options for the integration. (see [below for nested schema](#nestedblock--configuration)) + +### Read-Only + +- `dashboard_folder` (String) The dashboard folder associated with this integration. +- `id` (String) The Terraform resource ID. Set to the integration slug. +- `installed_version` (String) The version of the installed integration. +- `latest_version` (String) The latest version available for this integration. +- `name` (String) The display name of the integration. + + +### Nested Schema for `configuration` + +Optional: + +- `configurable_alerts` (Block, Optional) Alerts configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_alerts)) +- `configurable_logs` (Block, Optional) Logs configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_logs)) + + +### Nested Schema for `configuration.configurable_alerts` + +Optional: + +- `alerts_disabled` (Boolean) Whether to disable alerts for this integration. + + + +### Nested Schema for `configuration.configurable_logs` + +Optional: + +- `logs_disabled` (Boolean) Whether to disable logs collection for this integration. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import grafana_cloud_integration.name "{{ slug }}" +``` From 3a88ef8cceb2079bb54b6550d05778f95c51fcfb Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 11:45:12 +0100 Subject: [PATCH 15/28] Fmt terraform files --- docs/resources/cloud_integration.md | 12 ++++++------ .../resources/grafana_cloud_integration/resource.tf | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md index 90770e9fb..f4ecf08c7 100644 --- a/docs/resources/cloud_integration.md +++ b/docs/resources/cloud_integration.md @@ -72,18 +72,18 @@ resource "grafana_cloud_integration" "kafka" { # Output info output "linux_node_integration" { value = { - name = grafana_cloud_integration.linux-node.name - latest_version = grafana_cloud_integration.linux-node.latest_version + name = grafana_cloud_integration.linux-node.name + latest_version = grafana_cloud_integration.linux-node.latest_version installed_version = grafana_cloud_integration.linux-node.installed_version - dashboard_folder = grafana_cloud_integration.linux-node.dashboard_folder + dashboard_folder = grafana_cloud_integration.linux-node.dashboard_folder } } output "kafka_integration" { value = { - name = grafana_cloud_integration.kafka.name - latest_version = grafana_cloud_integration.kafka.latest_version + name = grafana_cloud_integration.kafka.name + latest_version = grafana_cloud_integration.kafka.latest_version installed_version = grafana_cloud_integration.kafka.installed_version - dashboard_folder = grafana_cloud_integration.kafka.dashboard_folder + dashboard_folder = grafana_cloud_integration.kafka.dashboard_folder } } ``` diff --git a/examples/resources/grafana_cloud_integration/resource.tf b/examples/resources/grafana_cloud_integration/resource.tf index e861064c0..59d485e29 100644 --- a/examples/resources/grafana_cloud_integration/resource.tf +++ b/examples/resources/grafana_cloud_integration/resource.tf @@ -29,17 +29,17 @@ resource "grafana_cloud_integration" "kafka" { # Output info output "linux_node_integration" { value = { - name = grafana_cloud_integration.linux-node.name - latest_version = grafana_cloud_integration.linux-node.latest_version + name = grafana_cloud_integration.linux-node.name + latest_version = grafana_cloud_integration.linux-node.latest_version installed_version = grafana_cloud_integration.linux-node.installed_version - dashboard_folder = grafana_cloud_integration.linux-node.dashboard_folder + dashboard_folder = grafana_cloud_integration.linux-node.dashboard_folder } } output "kafka_integration" { value = { - name = grafana_cloud_integration.kafka.name - latest_version = grafana_cloud_integration.kafka.latest_version + name = grafana_cloud_integration.kafka.name + latest_version = grafana_cloud_integration.kafka.latest_version installed_version = grafana_cloud_integration.kafka.installed_version - dashboard_folder = grafana_cloud_integration.kafka.dashboard_folder + dashboard_folder = grafana_cloud_integration.kafka.dashboard_folder } } From 0e3e06ac5d3e76c5741b5c8c240ee7858223bc97 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 12:07:07 +0100 Subject: [PATCH 16/28] Address lint & fmt --- internal/common/client.go | 16 +- .../cloudintegrationsapi/client_test.go | 150 ++++++++++-------- .../cloudintegrationsapi/models/models.go | 3 +- 3 files changed, 89 insertions(+), 80 deletions(-) diff --git a/internal/common/client.go b/internal/common/client.go index 992091b57..f70bb61e8 100644 --- a/internal/common/client.go +++ b/internal/common/client.go @@ -37,17 +37,17 @@ type Client struct { GrafanaOrgID int64 GrafanaStackID int64 - GrafanaCloudAPI *gcom.APIClient - SMAPI *SMAPI.Client - MLAPI *mlapi.Client - OnCallClient *onCallAPI.Client - SLOClient *slo.APIClient + GrafanaCloudAPI *gcom.APIClient + SMAPI *SMAPI.Client + MLAPI *mlapi.Client + OnCallClient *onCallAPI.Client + SLOClient *slo.APIClient CloudIntegrationsAPIClient *cloudintegrationsapi.Client CloudProviderAPI *cloudproviderapi.Client ConnectionsAPIClient *connectionsapi.Client - FleetManagementClient *fleetmanagementapi.Client - FrontendO11yAPIClient *frontendo11yapi.Client - AssertsAPIClient *assertsapi.APIClient + FleetManagementClient *fleetmanagementapi.Client + FrontendO11yAPIClient *frontendo11yapi.Client + AssertsAPIClient *assertsapi.APIClient K6APIClient *k6.APIClient K6APIConfig *k6providerapi.K6APIConfig diff --git a/internal/common/cloudintegrationsapi/client_test.go b/internal/common/cloudintegrationsapi/client_test.go index 24e8e294c..7682a4c79 100644 --- a/internal/common/cloudintegrationsapi/client_test.go +++ b/internal/common/cloudintegrationsapi/client_test.go @@ -266,13 +266,15 @@ func TestUnit_UninstallIntegration(t *testing.T) { t.Parallel() var uninstallCalled bool svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker"): + switch r.Method { + case http.MethodGet: _ = json.NewEncoder(w).Encode(dockerIntegration) - case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, rulesConvertPath): + case http.MethodDelete: w.WriteHeader(http.StatusOK) - case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/integrations/docker/uninstall"): - uninstallCalled = true + case http.MethodPost: + if strings.HasSuffix(r.URL.Path, "/integrations/docker/uninstall") { + uninstallCalled = true + } w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusOK) @@ -303,12 +305,12 @@ func TestUnit_UninstallIntegration(t *testing.T) { t.Run("propagates error from uninstall API", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet: + switch r.Method { + case http.MethodGet: _ = json.NewEncoder(w).Encode(dockerIntegration) - case r.Method == http.MethodDelete: + case http.MethodDelete: w.WriteHeader(http.StatusOK) - case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/uninstall"): + case http.MethodPost: w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"failed"}`)) default: @@ -335,26 +337,28 @@ func TestUnit_InstallIntegrationRules(t *testing.T) { t.Parallel() var convertCalled bool svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker/rules"): - _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ - Data: models.IntegrationRulesData{ - RecordingRules: []models.RuleGroup{ - {Name: "rec", Rules: []models.Rule{{Record: "rec:metric", Expr: "sum(up)"}}}, + switch r.Method { + case http.MethodGet: + if strings.HasSuffix(r.URL.Path, "/integrations/docker/rules") { + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + RecordingRules: []models.RuleGroup{ + {Name: "rec", Rules: []models.Rule{{Record: "rec:metric", Expr: "sum(up)"}}}, + }, + AlertingRules: []models.RuleGroup{ + {Name: "alert", Rules: []models.Rule{{Alert: "TestAlert", Expr: "up == 0"}}}, + }, }, - AlertingRules: []models.RuleGroup{ - {Name: "alert", Rules: []models.Rule{{Alert: "TestAlert", Expr: "up == 0"}}}, + }) + } else { + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{ + Name: "Docker", + DashboardFolder: "Docker", }, - }, - }) - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker"): - _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ - Data: models.Integration{ - Name: "Docker", - DashboardFolder: "Docker", - }, - }) - case r.Method == http.MethodPost && r.URL.Path == rulesConvertPath: + }) + } + case http.MethodPost: convertCalled = true assert.Equal(t, cloudPromUID, r.Header.Get("X-Grafana-Alerting-Datasource-UID")) @@ -396,15 +400,17 @@ func TestUnit_InstallIntegrationRules(t *testing.T) { t.Run("skips when no rule groups returned", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/rules"): - _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ - Data: models.IntegrationRulesData{}, - }) - case r.Method == http.MethodGet: - _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ - Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, - }) + switch r.Method { + case http.MethodGet: + if strings.HasSuffix(r.URL.Path, "/rules") { + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{}, + }) + } else { + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, + }) + } default: t.Errorf("unexpected request (no POST expected): %s %s", r.Method, r.URL.Path) } @@ -419,17 +425,19 @@ func TestUnit_InstallIntegrationRules(t *testing.T) { t.Run("skips when namespace resolves to empty", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/rules"): - _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ - Data: models.IntegrationRulesData{ - RecordingRules: []models.RuleGroup{{Name: "rec", Rules: []models.Rule{{Record: "m", Expr: "1"}}}}, - }, - }) - case r.Method == http.MethodGet: - _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ - Data: models.Integration{Name: "", DashboardFolder: "", RuleNamespace: ""}, - }) + switch r.Method { + case http.MethodGet: + if strings.HasSuffix(r.URL.Path, "/rules") { + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + RecordingRules: []models.RuleGroup{{Name: "rec", Rules: []models.Rule{{Record: "m", Expr: "1"}}}}, + }, + }) + } else { + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "", DashboardFolder: "", RuleNamespace: ""}, + }) + } default: t.Errorf("unexpected request (no POST expected): %s %s", r.Method, r.URL.Path) } @@ -458,18 +466,20 @@ func TestUnit_InstallIntegrationRules(t *testing.T) { t.Run("propagates error from convert API", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/rules"): - _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ - Data: models.IntegrationRulesData{ - AlertingRules: []models.RuleGroup{{Name: "a", Rules: []models.Rule{{Alert: "X", Expr: "1"}}}}, - }, - }) - case r.Method == http.MethodGet: - _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ - Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, - }) - case r.Method == http.MethodPost && r.URL.Path == rulesConvertPath: + switch r.Method { + case http.MethodGet: + if strings.HasSuffix(r.URL.Path, "/rules") { + _ = json.NewEncoder(w).Encode(models.IntegrationRulesResponse{ + Data: models.IntegrationRulesData{ + AlertingRules: []models.RuleGroup{{Name: "a", Rules: []models.Rule{{Alert: "X", Expr: "1"}}}}, + }, + }) + } else { + _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ + Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, + }) + } + case http.MethodPost: w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`conversion failed`)) } @@ -490,12 +500,12 @@ func TestUnit_UninstallIntegrationRules(t *testing.T) { t.Parallel() var deleteCalled bool svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/integrations/docker"): + switch r.Method { + case http.MethodGet: _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, }) - case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, rulesConvertPath): + case http.MethodDelete: deleteCalled = true assert.Equal(t, rulesConvertPath+"/Docker", r.URL.Path) w.WriteHeader(http.StatusOK) @@ -514,12 +524,12 @@ func TestUnit_UninstallIntegrationRules(t *testing.T) { t.Run("ignores 404 on DELETE", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet: + switch r.Method { + case http.MethodGet: _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ Data: models.Integration{Name: "Docker", DashboardFolder: "Docker"}, }) - case r.Method == http.MethodDelete: + case http.MethodDelete: w.WriteHeader(http.StatusNotFound) } })) @@ -533,8 +543,8 @@ func TestUnit_UninstallIntegrationRules(t *testing.T) { t.Run("skips when namespace resolves to empty", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet: + switch r.Method { + case http.MethodGet: _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ Data: models.Integration{}, }) @@ -566,12 +576,12 @@ func TestUnit_UninstallIntegrationRules(t *testing.T) { t.Run("propagates non-404 error from DELETE", func(t *testing.T) { t.Parallel() svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet: + switch r.Method { + case http.MethodGet: _ = json.NewEncoder(w).Encode(models.GetIntegrationResponse{ Data: models.Integration{DashboardFolder: "Docker"}, }) - case r.Method == http.MethodDelete: + case http.MethodDelete: w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`server error`)) } diff --git a/internal/common/cloudintegrationsapi/models/models.go b/internal/common/cloudintegrationsapi/models/models.go index e6903eda4..ad02da0f4 100644 --- a/internal/common/cloudintegrationsapi/models/models.go +++ b/internal/common/cloudintegrationsapi/models/models.go @@ -37,7 +37,7 @@ type Installation struct { // InstallationConfig represents the configuration for installing an integration type InstallationConfig struct { - ConfigurableLogs *ConfigurableLogs `json:"configurable_logs,omitempty"` + ConfigurableLogs *ConfigurableLogs `json:"configurable_logs,omitempty"` ConfigurableAlerts *ConfigurableAlerts `json:"configurable_alerts,omitempty"` } @@ -101,4 +101,3 @@ type IntegrationRulesData struct { RecordingRules []RuleGroup `json:"recording_rules,omitempty"` AlertingRules []RuleGroup `json:"alerting_rules,omitempty"` } - From f3630f516dc9d93fe8338f30f9b96c19669e3d59 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 12:10:52 +0100 Subject: [PATCH 17/28] Add missing access policy scope for Grafana Alerting --- docs/resources/cloud_integration.md | 6 +++++- .../cloudintegrations/resource_cloud_integration.go | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md index f4ecf08c7..fecd3cb7e 100644 --- a/docs/resources/cloud_integration.md +++ b/docs/resources/cloud_integration.md @@ -11,7 +11,8 @@ description: |- As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. Update, only triggered on config change, is implemented as a complete uninstall, then reinstall of the integration in question. Required access policy scopes: - folders:readfolders:writedashboards:readdashboards:write + folders:readfolders:writedashboards:readdashboards:writerules:readrules:write + Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#import-rules-with-grafana-alerting Note: This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. --- @@ -35,6 +36,9 @@ Required access policy scopes: * folders:write * dashboards:read * dashboards:write +* rules:read +* rules:write +Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#import-rules-with-grafana-alerting **Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index 903db47ca..30fe2c2a1 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -90,6 +90,9 @@ Required access policy scopes: * folders:write * dashboards:read * dashboards:write +* rules:read +* rules:write +Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#import-rules-with-grafana-alerting **Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. `, From 51376a5a9b0ecba01415aa9c237fc90348d09cc8 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 12:21:44 +0100 Subject: [PATCH 18/28] Add resource catalog entry for grafana_cloud_integration --- catalog-info.yaml | 3 ++- .../cloudintegrations/catalog-resource.yaml | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 internal/resources/cloudintegrations/catalog-resource.yaml diff --git a/catalog-info.yaml b/catalog-info.yaml index 0998f8db8..04351d217 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -31,6 +31,7 @@ spec: - ./internal/resources/asserts/catalog-resource.yaml - ./internal/resources/cloud/catalog-resource.yaml - ./internal/resources/cloudprovider/catalog-resource.yaml + - ./internal/resources/cloudintegrations/catalog-resource.yaml - ./internal/resources/connections/catalog-resource.yaml - ./internal/resources/fleetmanagement/catalog-resource.yaml - ./internal/resources/frontendo11y/catalog-resource.yaml @@ -56,4 +57,4 @@ spec: - ./internal/resources/k6/catalog-data-source.yaml - ./internal/resources/oncall/catalog-data-source.yaml - ./internal/resources/slo/catalog-data-source.yaml - - ./internal/resources/syntheticmonitoring/catalog-data-source.yaml \ No newline at end of file + - ./internal/resources/syntheticmonitoring/catalog-data-source.yaml diff --git a/internal/resources/cloudintegrations/catalog-resource.yaml b/internal/resources/cloudintegrations/catalog-resource.yaml new file mode 100644 index 000000000..11c8571c7 --- /dev/null +++ b/internal/resources/cloudintegrations/catalog-resource.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: resource-grafana-cloud-integration + title: grafana-cloud-integration (resource) + description: | + resource `grafana-cloud-integration` in Grafana Labs' Terraform Provider +spec: + subcomponentOf: component:default/terraform-provider-grafana + type: terraform-resource + owner: group:default/cloud-integrations + lifecycle: production From 77dbdbf6b470d960a1e81166094d41e8435dfea4 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Fri, 27 Mar 2026 12:33:37 +0100 Subject: [PATCH 19/28] Let's try aligning on dashes/underscores --- internal/resources/cloudintegrations/catalog-resource.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/resources/cloudintegrations/catalog-resource.yaml b/internal/resources/cloudintegrations/catalog-resource.yaml index 11c8571c7..207e5c123 100644 --- a/internal/resources/cloudintegrations/catalog-resource.yaml +++ b/internal/resources/cloudintegrations/catalog-resource.yaml @@ -2,10 +2,10 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: resource-grafana-cloud-integration - title: grafana-cloud-integration (resource) + name: resource-grafana_cloud_integration + title: grafana_cloud_integration (resource) description: | - resource `grafana-cloud-integration` in Grafana Labs' Terraform Provider + resource `grafana_cloud_integration` in Grafana Labs' Terraform Provider spec: subcomponentOf: component:default/terraform-provider-grafana type: terraform-resource From ebf8f21d78825769b0594eeac724585c231eb460 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Mon, 30 Mar 2026 15:55:29 +0200 Subject: [PATCH 20/28] Simplify cloud integration configuration Flattens the existing configuration block by removing the unused `logs_disabled` field and only allowing toggling alerts --- docs/resources/cloud_integration.md | 44 +------ .../grafana_cloud_integration/resource.tf | 19 +-- .../resource_cloud_integration.go | 111 ++++-------------- 3 files changed, 28 insertions(+), 146 deletions(-) diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md index fecd3cb7e..9cbad0e4f 100644 --- a/docs/resources/cloud_integration.md +++ b/docs/resources/cloud_integration.md @@ -48,29 +48,12 @@ Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerti # install linux-node integration resource "grafana_cloud_integration" "linux-node" { slug = "linux-node" - - configuration { - configurable_logs { - logs_disabled = false - } - configurable_alerts { - alerts_disabled = false - } - } } # install kafka integration w. alerts disabled resource "grafana_cloud_integration" "kafka" { slug = "kafka" - - configuration { - configurable_logs { - logs_disabled = false - } - configurable_alerts { - alerts_disabled = true - } - } + alerts_enabled = false } # Output info @@ -101,7 +84,7 @@ output "kafka_integration" { ### Optional -- `configuration` (Block, Optional) Configuration options for the integration. (see [below for nested schema](#nestedblock--configuration)) +- `alerts_enabled` (Boolean) Whether alerts are enabled for this integration. ### Read-Only @@ -111,29 +94,6 @@ output "kafka_integration" { - `latest_version` (String) The latest version available for this integration. - `name` (String) The display name of the integration. - -### Nested Schema for `configuration` - -Optional: - -- `configurable_alerts` (Block, Optional) Alerts configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_alerts)) -- `configurable_logs` (Block, Optional) Logs configuration for the integration. (see [below for nested schema](#nestedblock--configuration--configurable_logs)) - - -### Nested Schema for `configuration.configurable_alerts` - -Optional: - -- `alerts_disabled` (Boolean) Whether to disable alerts for this integration. - - - -### Nested Schema for `configuration.configurable_logs` - -Optional: - -- `logs_disabled` (Boolean) Whether to disable logs collection for this integration. - ## Import Import is supported using the following syntax: diff --git a/examples/resources/grafana_cloud_integration/resource.tf b/examples/resources/grafana_cloud_integration/resource.tf index 59d485e29..509addd1a 100644 --- a/examples/resources/grafana_cloud_integration/resource.tf +++ b/examples/resources/grafana_cloud_integration/resource.tf @@ -1,29 +1,12 @@ # install linux-node integration resource "grafana_cloud_integration" "linux-node" { slug = "linux-node" - - configuration { - configurable_logs { - logs_disabled = false - } - configurable_alerts { - alerts_disabled = false - } - } } # install kafka integration w. alerts disabled resource "grafana_cloud_integration" "kafka" { slug = "kafka" - - configuration { - configurable_logs { - logs_disabled = false - } - configurable_alerts { - alerts_disabled = true - } - } + alerts_enabled = false } # Output info diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index 30fe2c2a1..555db3a83 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -37,27 +37,14 @@ func resourceCloudIntegration() *common.Resource { ) } -type configurableLogsModel struct { - LogsDisabled types.Bool `tfsdk:"logs_disabled"` -} - -type configurableAlertsModel struct { - AlertsDisabled types.Bool `tfsdk:"alerts_disabled"` -} - -type configurationModel struct { - ConfigurableLogs *configurableLogsModel `tfsdk:"configurable_logs"` - ConfigurableAlerts *configurableAlertsModel `tfsdk:"configurable_alerts"` -} - type cloudIntegrationResourceModel struct { - ID types.String `tfsdk:"id"` - Slug types.String `tfsdk:"slug"` - InstalledVersion types.String `tfsdk:"installed_version"` - LatestVersion types.String `tfsdk:"latest_version"` - Name types.String `tfsdk:"name"` - DashboardFolder types.String `tfsdk:"dashboard_folder"` - Configuration *configurationModel `tfsdk:"configuration"` + ID types.String `tfsdk:"id"` + Slug types.String `tfsdk:"slug"` + InstalledVersion types.String `tfsdk:"installed_version"` + LatestVersion types.String `tfsdk:"latest_version"` + Name types.String `tfsdk:"name"` + DashboardFolder types.String `tfsdk:"dashboard_folder"` + AlertsEnabled types.Bool `tfsdk:"alerts_enabled"` } type cloudIntegrationResource struct { @@ -127,34 +114,11 @@ Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerti Description: "The dashboard folder associated with this integration.", Computed: true, }, - }, - Blocks: map[string]schema.Block{ - "configuration": schema.SingleNestedBlock{ - Description: "Configuration options for the integration.", - Blocks: map[string]schema.Block{ - "configurable_logs": schema.SingleNestedBlock{ - Description: "Logs configuration for the integration.", - Attributes: map[string]schema.Attribute{ - "logs_disabled": schema.BoolAttribute{ - Description: "Whether to disable logs collection for this integration.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - }, - }, - "configurable_alerts": schema.SingleNestedBlock{ - Description: "Alerts configuration for the integration.", - Attributes: map[string]schema.Attribute{ - "alerts_disabled": schema.BoolAttribute{ - Description: "Whether to disable alerts for this integration.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - }, - }, - }, + "alerts_enabled": schema.BoolAttribute{ + Description: "Whether alerts are enabled for this integration.", + Computed: true, + Optional: true, + Default: booldefault.StaticBool(true), }, }, } @@ -203,7 +167,7 @@ func (r *cloudIntegrationResource) Create(ctx context.Context, req resource.Crea } if !installed { - config := toAPIConfig(plan.Configuration) + config := toAPIConfig(plan.AlertsEnabled) var installErr error r.commonClient.WithFolderLock(func() { r.commonClient.WithDashboardLock(func() { @@ -288,7 +252,7 @@ func (r *cloudIntegrationResource) Update(ctx context.Context, req resource.Upda return } - config := toAPIConfig(plan.Configuration) + config := toAPIConfig(plan.AlertsEnabled) var installErr error r.commonClient.WithFolderLock(func() { r.commonClient.WithDashboardLock(func() { @@ -336,26 +300,12 @@ func (r *cloudIntegrationResource) ImportState(ctx context.Context, req resource resource.ImportStatePassthroughID(ctx, path.Root("slug"), req, resp) } -func toAPIConfig(cfg *configurationModel) *models.InstallationConfig { - if cfg == nil { - return nil - } - - config := &models.InstallationConfig{} - - if cfg.ConfigurableLogs != nil { - config.ConfigurableLogs = &models.ConfigurableLogs{ - LogsDisabled: cfg.ConfigurableLogs.LogsDisabled.ValueBool(), - } - } - - if cfg.ConfigurableAlerts != nil { - config.ConfigurableAlerts = &models.ConfigurableAlerts{ - AlertsDisabled: cfg.ConfigurableAlerts.AlertsDisabled.ValueBool(), - } +func toAPIConfig(alertsEnabled types.Bool) *models.InstallationConfig { + return &models.InstallationConfig{ + ConfigurableAlerts: &models.ConfigurableAlerts{ + AlertsDisabled: !alertsEnabled.ValueBool(), + }, } - - return config } func setModelFromAPI(model *cloudIntegrationResourceModel, integration *models.GetIntegrationResponse) { @@ -368,22 +318,11 @@ func setModelFromAPI(model *cloudIntegrationResourceModel, integration *models.G model.InstalledVersion = types.StringValue(integration.Data.Installation.Version) } - if integration.Data.Installation != nil && integration.Data.Installation.Configuration != nil { - apiConfig := integration.Data.Installation.Configuration - cfg := &configurationModel{} - - if apiConfig.ConfigurableLogs != nil { - cfg.ConfigurableLogs = &configurableLogsModel{ - LogsDisabled: types.BoolValue(apiConfig.ConfigurableLogs.LogsDisabled), - } - } - - if apiConfig.ConfigurableAlerts != nil { - cfg.ConfigurableAlerts = &configurableAlertsModel{ - AlertsDisabled: types.BoolValue(apiConfig.ConfigurableAlerts.AlertsDisabled), - } - } - - model.Configuration = cfg + alertsEnabled := true + if integration.Data.Installation != nil && + integration.Data.Installation.Configuration != nil && + integration.Data.Installation.Configuration.ConfigurableAlerts != nil { + alertsEnabled = !integration.Data.Installation.Configuration.ConfigurableAlerts.AlertsDisabled } + model.AlertsEnabled = types.BoolValue(alertsEnabled) } From 7775a424a87b2d34481d528ef3ffd1a0e0faef0c Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Mon, 30 Mar 2026 15:57:11 +0200 Subject: [PATCH 21/28] Ah the woes of linting --- docs/resources/cloud_integration.md | 2 +- examples/resources/grafana_cloud_integration/resource.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md index 9cbad0e4f..183e4c1dc 100644 --- a/docs/resources/cloud_integration.md +++ b/docs/resources/cloud_integration.md @@ -52,7 +52,7 @@ resource "grafana_cloud_integration" "linux-node" { # install kafka integration w. alerts disabled resource "grafana_cloud_integration" "kafka" { - slug = "kafka" + slug = "kafka" alerts_enabled = false } diff --git a/examples/resources/grafana_cloud_integration/resource.tf b/examples/resources/grafana_cloud_integration/resource.tf index 509addd1a..60b798986 100644 --- a/examples/resources/grafana_cloud_integration/resource.tf +++ b/examples/resources/grafana_cloud_integration/resource.tf @@ -5,7 +5,7 @@ resource "grafana_cloud_integration" "linux-node" { # install kafka integration w. alerts disabled resource "grafana_cloud_integration" "kafka" { - slug = "kafka" + slug = "kafka" alerts_enabled = false } From 743ee08fa428ce104dcb8734b991101a3f2a7678 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 2 Apr 2026 13:21:56 +0200 Subject: [PATCH 22/28] Updated to use proper `CategoryCloudIntegrations` in resource_cloud_integration.go and resource.go --- docs/resources/cloud_integration.md | 2 +- internal/common/resource.go | 2 +- .../resources/cloudintegrations/resource_cloud_integration.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md index 183e4c1dc..a015c9fa8 100644 --- a/docs/resources/cloud_integration.md +++ b/docs/resources/cloud_integration.md @@ -1,7 +1,7 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "grafana_cloud_integration Resource - terraform-provider-grafana" -subcategory: "Cloud" +subcategory: "CloudIntegrations" description: |- Manages Grafana Cloud integrations. Official documentation https://grafana.com/docs/grafana-cloud/data-configuration/integrations/ diff --git a/internal/common/resource.go b/internal/common/resource.go index 125f93e37..650032cde 100644 --- a/internal/common/resource.go +++ b/internal/common/resource.go @@ -28,7 +28,7 @@ var ( CategoryFrontendO11y ResourceCategory = "Frontend Observability" CategoryAsserts ResourceCategory = "Knowledge Graph" CategoryK6 ResourceCategory = "k6" - CategoryIntegration ResourceCategory = "Integration" + CategoryCloudIntegrations ResourceCategory = "CloudIntegrations" ) type ResourceCommon struct { diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index 555db3a83..dee3ed5a8 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -30,7 +30,7 @@ var ( func resourceCloudIntegration() *common.Resource { return common.NewResource( - common.CategoryCloud, + common.CategoryCloudIntegrations, resourceCloudIntegrationName, resourceCloudIntegrationID, &cloudIntegrationResource{}, From b35d8f1d122fdaf3b945970c8aa9ad1be06971ca Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 2 Apr 2026 16:37:13 +0200 Subject: [PATCH 23/28] Add Cloud Integrations to examples_test.go and fix typo in category name --- docs/resources/cloud_integration.md | 7 ++++--- internal/common/resource.go | 2 +- .../cloudintegrations/resource_cloud_integration.go | 5 ++++- internal/resources/examples_test.go | 6 ++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/resources/cloud_integration.md b/docs/resources/cloud_integration.md index a015c9fa8..61dbabe72 100644 --- a/docs/resources/cloud_integration.md +++ b/docs/resources/cloud_integration.md @@ -1,12 +1,12 @@ --- # generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "grafana_cloud_integration Resource - terraform-provider-grafana" -subcategory: "CloudIntegrations" +subcategory: "Cloud Integrations" description: |- Manages Grafana Cloud integrations. Official documentation https://grafana.com/docs/grafana-cloud/data-configuration/integrations/ This provider lets you manage Grafana Cloud Integrations. - Configuration options include disabling logs and alerts. + Alerts can optionally be disabled. Please note: Grafana Cloud Integrations do not support in-place upgrades, and require a teardown and reapply to resolve version drift. As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. Update, only triggered on config change, is implemented as a complete uninstall, then reinstall of the integration in question. @@ -23,7 +23,7 @@ Manages Grafana Cloud integrations. * [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) This provider lets you manage Grafana Cloud Integrations. -Configuration options include disabling logs and alerts. +Alerts can optionally be disabled. Please note: Grafana Cloud Integrations do not support in-place upgrades, and require a teardown and reapply to resolve version drift. As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. @@ -38,6 +38,7 @@ Required access policy scopes: * dashboards:write * rules:read * rules:write + Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#import-rules-with-grafana-alerting **Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. diff --git a/internal/common/resource.go b/internal/common/resource.go index 650032cde..4beda6fdc 100644 --- a/internal/common/resource.go +++ b/internal/common/resource.go @@ -28,7 +28,7 @@ var ( CategoryFrontendO11y ResourceCategory = "Frontend Observability" CategoryAsserts ResourceCategory = "Knowledge Graph" CategoryK6 ResourceCategory = "k6" - CategoryCloudIntegrations ResourceCategory = "CloudIntegrations" + CategoryCloudIntegrations ResourceCategory = "Cloud Integrations" ) type ResourceCommon struct { diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index dee3ed5a8..e9340cf9e 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -64,7 +64,7 @@ Manages Grafana Cloud integrations. * [Official documentation](https://grafana.com/docs/grafana-cloud/data-configuration/integrations/) This provider lets you manage Grafana Cloud Integrations. -Configuration options include disabling logs and alerts. +Alerts can optionally be disabled. Please note: Grafana Cloud Integrations do not support in-place upgrades, and require a teardown and reapply to resolve version drift. As such it is recommended to have a separate TF plan for integrations to cleanly destroy them as needed. @@ -79,6 +79,7 @@ Required access policy scopes: * dashboards:write * rules:read * rules:write + Based on: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/alerting-migration/#import-rules-with-grafana-alerting **Note:** This resource creates folders and dashboards as part of the integration installation process, which requires additional permissions beyond the basic integration scopes. @@ -303,6 +304,8 @@ func (r *cloudIntegrationResource) ImportState(ctx context.Context, req resource func toAPIConfig(alertsEnabled types.Bool) *models.InstallationConfig { return &models.InstallationConfig{ ConfigurableAlerts: &models.ConfigurableAlerts{ + // Note, while API exposes logsDisabled, this is not truly acted upon by the backend. + // As such, we don't expose this to users via Terraform AlertsDisabled: !alertsEnabled.ValueBool(), }, } diff --git a/internal/resources/examples_test.go b/internal/resources/examples_test.go index fce943e06..1e01dd078 100644 --- a/internal/resources/examples_test.go +++ b/internal/resources/examples_test.go @@ -178,6 +178,12 @@ func TestAccExamples(t *testing.T) { testutils.CheckCloudInstanceTestsEnabled(t) }, }, + { + category: "Cloud Integrations", + testCheck: func(t *testing.T, filename string) { + testutils.CheckCloudInstanceTestsEnabled(t) + }, + }, } { // Get all the filenames for all resource examples for this category filenames := []string{} From 854101ff21f60d94dce4be9147bc78bda7163dfa Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Thu, 2 Apr 2026 16:37:51 +0200 Subject: [PATCH 24/28] Add resource_cloud_integration_test.go with basic resource management tests --- .../resource_cloud_integration_test.go | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 internal/resources/cloudintegrations/resource_cloud_integration_test.go diff --git a/internal/resources/cloudintegrations/resource_cloud_integration_test.go b/internal/resources/cloudintegrations/resource_cloud_integration_test.go new file mode 100644 index 000000000..565227b6b --- /dev/null +++ b/internal/resources/cloudintegrations/resource_cloud_integration_test.go @@ -0,0 +1,167 @@ +package cloudintegrations_test + +import ( + "context" + "fmt" + "testing" + + "github.com/grafana/terraform-provider-grafana/v4/internal/common" + "github.com/grafana/terraform-provider-grafana/v4/internal/testutils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +var ( + cloudIntegrationResourceDefaultConfig = ` +resource "grafana_cloud_integration" "test" { + slug = "%s" +} +` + + cloudIntegrationResourceAlertsDisabledConfig = ` +resource "grafana_cloud_integration" "test" { + slug = "%s" + alerts_enabled = false +} +` + + cloudIntegrationResourceAlertsEnabledConfig = ` +resource "grafana_cloud_integration" "test" { + slug = "%s" + alerts_enabled = true +} +` +) + +func TestAccCloudIntegrationResource(t *testing.T) { + testutils.CheckCloudInstanceTestsEnabled(t) + + ctx := context.Background() + resourceName := "grafana_cloud_integration.test" + slug := "docker" + + // Note, we don't actually expose installed dashboards/alerts to the client, + // so the best we can test here is success in changes + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Create with only required fields (alerts_enabled defaults to true) + { + Config: fmt.Sprintf(cloudIntegrationResourceDefaultConfig, slug), + Check: resource.ComposeTestCheckFunc( + testAccCloudIntegrationResourceExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "id", slug), + resource.TestCheckResourceAttr(resourceName, "slug", slug), + resource.TestCheckResourceAttr(resourceName, "alerts_enabled", "true"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "installed_version"), + resource.TestCheckResourceAttrSet(resourceName, "latest_version"), + resource.TestCheckResourceAttrSet(resourceName, "dashboard_folder"), + ), + }, + // Import state with only required fields + { + ResourceName: resourceName, + ImportState: true, + ImportStateId: slug, + ImportStateVerify: true, + }, + // Update with alerts disabled + { + Config: fmt.Sprintf(cloudIntegrationResourceAlertsDisabledConfig, slug), + Check: resource.ComposeTestCheckFunc( + testAccCloudIntegrationResourceExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "id", slug), + resource.TestCheckResourceAttr(resourceName, "slug", slug), + resource.TestCheckResourceAttr(resourceName, "alerts_enabled", "false"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "installed_version"), + resource.TestCheckResourceAttrSet(resourceName, "latest_version"), + resource.TestCheckResourceAttrSet(resourceName, "dashboard_folder"), + ), + }, + // Import state with alerts disabled + { + ResourceName: resourceName, + ImportState: true, + ImportStateId: slug, + ImportStateVerify: true, + }, + // Update back to alerts enabled + { + Config: fmt.Sprintf(cloudIntegrationResourceAlertsEnabledConfig, slug), + Check: resource.ComposeTestCheckFunc( + testAccCloudIntegrationResourceExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "id", slug), + resource.TestCheckResourceAttr(resourceName, "slug", slug), + resource.TestCheckResourceAttr(resourceName, "alerts_enabled", "true"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "installed_version"), + resource.TestCheckResourceAttrSet(resourceName, "latest_version"), + resource.TestCheckResourceAttrSet(resourceName, "dashboard_folder"), + ), + }, + // Update with only required fields (defaults back to alerts_enabled=true) + { + Config: fmt.Sprintf(cloudIntegrationResourceDefaultConfig, slug), + Check: resource.ComposeTestCheckFunc( + testAccCloudIntegrationResourceExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "id", slug), + resource.TestCheckResourceAttr(resourceName, "slug", slug), + resource.TestCheckResourceAttr(resourceName, "alerts_enabled", "true"), + resource.TestCheckResourceAttrSet(resourceName, "name"), + resource.TestCheckResourceAttrSet(resourceName, "installed_version"), + resource.TestCheckResourceAttrSet(resourceName, "latest_version"), + resource.TestCheckResourceAttrSet(resourceName, "dashboard_folder"), + ), + }, + }, + // Delete + CheckDestroy: testAccCloudIntegrationResourceCheckDestroy(ctx, slug), + }) +} + +func testAccCloudIntegrationResourceExists(ctx context.Context, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s\n %#v", resourceName, s.RootModule().Resources) + } + + slug, ok := resourceState.Primary.Attributes["slug"] + if !ok { + return fmt.Errorf("slug not set") + } + + client := testutils.Provider.Meta().(*common.Client).CloudIntegrationsAPIClient + + integration, err := client.GetIntegration(ctx, slug) + if err != nil { + return fmt.Errorf("error getting integration: %v", err) + } + + if integration.Data.Installation == nil { + return fmt.Errorf("integration %s is not installed", slug) + } + + return nil + } +} + +func testAccCloudIntegrationResourceCheckDestroy(ctx context.Context, slug string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testutils.Provider.Meta().(*common.Client).CloudIntegrationsAPIClient + + integration, err := client.GetIntegration(ctx, slug) + if err != nil { + // Integration not found is acceptable for destroy + return nil + } + + if integration.Data.Installation != nil { + return fmt.Errorf("integration %s is still installed", slug) + } + + return nil + } +} From 83b87b5c9a666e1d7ee8371ddd456941e2f788fe Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Tue, 7 Apr 2026 17:11:15 +0200 Subject: [PATCH 25/28] Replace rollout level int with typed value --- internal/common/cloudintegrationsapi/client.go | 8 ++++---- .../cloudintegrationsapi/client_internal_test.go | 13 +++++++------ .../common/cloudintegrationsapi/models/models.go | 5 ++++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/internal/common/cloudintegrationsapi/client.go b/internal/common/cloudintegrationsapi/client.go index 51e78ed82..2edfd4304 100644 --- a/internal/common/cloudintegrationsapi/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -31,9 +31,9 @@ const ( defaultRetries = 3 defaultTimeout = 90 * time.Second - RolloutLevelMimir = 0 - RolloutLevelInstallOnly = 1 - RolloutLevelGrafana = 2 + RolloutLevelMimir models.RolloutLevel = 0 + RolloutLevelInstallOnly models.RolloutLevel = 1 + RolloutLevelGrafana models.RolloutLevel = 2 ) // Client wraps the HTTP client for integrations API calls @@ -310,7 +310,7 @@ func resolveGrafanaRulesNamespace(dashboardFolder, ruleNamespace, integrationNam // shouldInstallRulesOnInstall returns true if rules should be installed to // Grafana Alerting for a new installation (rollout level >= 1). -func shouldInstallRulesOnInstall(rolloutLevel *int) bool { +func shouldInstallRulesOnInstall(rolloutLevel *models.RolloutLevel) bool { return rolloutLevel != nil && *rolloutLevel >= RolloutLevelInstallOnly } diff --git a/internal/common/cloudintegrationsapi/client_internal_test.go b/internal/common/cloudintegrationsapi/client_internal_test.go index 66b60bafd..ad7cc6d97 100644 --- a/internal/common/cloudintegrationsapi/client_internal_test.go +++ b/internal/common/cloudintegrationsapi/client_internal_test.go @@ -3,6 +3,7 @@ package cloudintegrationsapi import ( "testing" + "github.com/grafana/terraform-provider-grafana/v4/internal/common/cloudintegrationsapi/models" "github.com/stretchr/testify/assert" ) @@ -124,18 +125,18 @@ func TestUnit_resolveGrafanaRulesNamespace(t *testing.T) { func TestUnit_shouldInstallRulesOnInstall(t *testing.T) { t.Parallel() - intPtr := func(v int) *int { return &v } + rolloutLevelPtr := func(v models.RolloutLevel) *models.RolloutLevel { return &v } tests := []struct { name string - rolloutLevel *int + rolloutLevel *models.RolloutLevel expected bool }{ {name: "nil rollout level", rolloutLevel: nil, expected: false}, - {name: "level 0 (Mimir)", rolloutLevel: intPtr(RolloutLevelMimir), expected: false}, - {name: "level 1 (InstallOnly)", rolloutLevel: intPtr(RolloutLevelInstallOnly), expected: true}, - {name: "level 2 (Grafana)", rolloutLevel: intPtr(RolloutLevelGrafana), expected: true}, - {name: "level above max", rolloutLevel: intPtr(99), expected: true}, + {name: "level 0 (Mimir)", rolloutLevel: rolloutLevelPtr(RolloutLevelMimir), expected: false}, + {name: "level 1 (InstallOnly)", rolloutLevel: rolloutLevelPtr(RolloutLevelInstallOnly), expected: true}, + {name: "level 2 (Grafana)", rolloutLevel: rolloutLevelPtr(RolloutLevelGrafana), expected: true}, + {name: "level above max", rolloutLevel: rolloutLevelPtr(99), expected: true}, } for _, tt := range tests { diff --git a/internal/common/cloudintegrationsapi/models/models.go b/internal/common/cloudintegrationsapi/models/models.go index ad02da0f4..b7759b390 100644 --- a/internal/common/cloudintegrationsapi/models/models.go +++ b/internal/common/cloudintegrationsapi/models/models.go @@ -2,6 +2,9 @@ package models import "time" +// RolloutLevel represents the rollout level for Grafana-managed alerts migration. +type RolloutLevel int + // Integration represents an integration from the API type Integration struct { Name string `json:"name"` @@ -19,7 +22,7 @@ type Integration struct { LogsCheckQuery string `json:"logs_check_query"` RuleNamespace string `json:"rule_namespace"` - GrafanaManagedAlertsRolloutLevel *int `json:"grafana_managed_alerts_rollout_level,omitempty"` + GrafanaManagedAlertsRolloutLevel *RolloutLevel `json:"grafana_managed_alerts_rollout_level,omitempty"` } // Logo represents the logo URLs for an integration From ed33be60ba027266a6881eeae9083cb55dbbdfdd Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Wed, 8 Apr 2026 15:41:23 +0200 Subject: [PATCH 26/28] Convert to typed response code check & add warn logs --- .../common/cloudintegrationsapi/client.go | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/common/cloudintegrationsapi/client.go b/internal/common/cloudintegrationsapi/client.go index 2edfd4304..38d72eada 100644 --- a/internal/common/cloudintegrationsapi/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "maps" "net/http" "net/url" @@ -14,6 +15,7 @@ import ( "strings" "time" + "github.com/go-openapi/runtime" "github.com/grafana/grafana-openapi-client-go/client/dashboards" "github.com/grafana/grafana-openapi-client-go/client/folders" oapimodels "github.com/grafana/grafana-openapi-client-go/models" @@ -188,7 +190,9 @@ func (c *Client) InstallDashboards(ctx context.Context, slug string, config *mod folderUID := c.generateFolderUID(dashboardFolder) err = c.CreateFolder(ctx, dashboardFolder, folderUID) if err != nil { - if !strings.Contains(err.Error(), "412") && !strings.Contains(err.Error(), "already exists") { + var respStatus runtime.ClientResponseStatus + isFolderExists := errors.As(err, &respStatus) && (respStatus.IsCode(409) || respStatus.IsCode(412)) + if !isFolderExists { return fmt.Errorf("failed to create folder: %w", err) } } @@ -221,8 +225,12 @@ func (c *Client) InstallIntegration(ctx context.Context, slug string, config *mo var success bool defer func() { if !success { - _ = c.DeleteFolder(ctx, folderUID) - _ = c.UninstallIntegrationRules(ctx, slug) + if err := c.DeleteFolder(ctx, folderUID); err != nil { + log.Printf("[WARN] failed to delete folder %s during install cleanup for integration %s: %v", folderUID, slug, err) + } + if err := c.UninstallIntegrationRules(ctx, slug); err != nil { + log.Printf("[WARN] failed to uninstall rules during install cleanup for integration %s: %v", slug, err) + } } }() @@ -258,8 +266,12 @@ func (c *Client) UninstallIntegration(ctx context.Context, slug string) error { } folderUID := c.generateFolderUID(integration.Data.DashboardFolder) - _ = c.DeleteFolder(ctx, folderUID) - _ = c.UninstallIntegrationRules(ctx, slug) + if err := c.DeleteFolder(ctx, folderUID); err != nil { + log.Printf("[WARN] failed to delete folder %s during uninstall of integration %s: %v", folderUID, slug, err) + } + if err := c.UninstallIntegrationRules(ctx, slug); err != nil { + log.Printf("[WARN] failed to uninstall rules during uninstall of integration %s: %v", slug, err) + } path := fmt.Sprintf("%s/integrations/%s/uninstall", adminBasePath, url.PathEscape(slug)) err = c.doAPIRequest(ctx, http.MethodPost, path, nil, nil) From 2e4779f17d31430db937e95651a3e859df71abf1 Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Wed, 8 Apr 2026 15:53:06 +0200 Subject: [PATCH 27/28] Use OpenAPI client calls with ctx --- internal/common/cloudintegrationsapi/client.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/common/cloudintegrationsapi/client.go b/internal/common/cloudintegrationsapi/client.go index 38d72eada..9e66e101a 100644 --- a/internal/common/cloudintegrationsapi/client.go +++ b/internal/common/cloudintegrationsapi/client.go @@ -123,7 +123,8 @@ func (c *Client) CreateFolder(ctx context.Context, title, uid string) error { UID: uid, } - _, err := c.foldersClient.CreateFolder(&body) + params := folders.NewCreateFolderParamsWithContext(ctx).WithBody(&body) + _, err := c.foldersClient.CreateFolderWithParams(params) if err != nil { return fmt.Errorf("failed to create folder %s: %w", title, err) } @@ -138,7 +139,7 @@ func (c *Client) DeleteFolder(ctx context.Context, uid string) error { } force := true - params := folders.NewDeleteFolderParams().WithFolderUID(uid) + params := folders.NewDeleteFolderParamsWithContext(ctx).WithFolderUID(uid) params.WithForceDeleteRules(&force) _, err := c.foldersClient.DeleteFolder(params) if err != nil { @@ -165,7 +166,8 @@ func (c *Client) CreateDashboard(ctx context.Context, dashboard models.Dashboard Message: "creating dashboard from the Cloud Connections plugin", } - _, err := c.dashboardsClient.PostDashboard(&dashboardCommand) + params := dashboards.NewPostDashboardParamsWithContext(ctx).WithBody(&dashboardCommand) + _, err := c.dashboardsClient.PostDashboardWithParams(params) if err != nil { return fmt.Errorf("failed to create dashboard: %w", err) } From 385ff48162744dbce71c89d9c12ba45a9c3f88fa Mon Sep 17 00:00:00 2001 From: Emily Rager Date: Wed, 8 Apr 2026 15:57:54 +0200 Subject: [PATCH 28/28] Keep state on failure to update --- .../resources/cloudintegrations/resource_cloud_integration.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/resources/cloudintegrations/resource_cloud_integration.go b/internal/resources/cloudintegrations/resource_cloud_integration.go index e9340cf9e..521125e24 100644 --- a/internal/resources/cloudintegrations/resource_cloud_integration.go +++ b/internal/resources/cloudintegrations/resource_cloud_integration.go @@ -261,8 +261,7 @@ func (r *cloudIntegrationResource) Update(ctx context.Context, req resource.Upda }) }) if installErr != nil { - resp.Diagnostics.AddError("Failed to install integration", installErr.Error()) - resp.State.RemoveResource(ctx) + resp.Diagnostics.AddError("Failed to install integration. Orphaned alerts and dashboards may be present.", installErr.Error()) return }