diff --git a/.go-version b/.go-version
index 82eab727..f288d111 100644
--- a/.go-version
+++ b/.go-version
@@ -1 +1 @@
-1.84.4
+1.85.0
diff --git a/client/metric_dashboards.go b/client/metric_dashboards.go
index 113dac0e..038a747e 100644
--- a/client/metric_dashboards.go
+++ b/client/metric_dashboards.go
@@ -30,6 +30,7 @@ type UnifiedGroup struct {
VisibilityType string `json:"visibility_type"`
Charts []UnifiedChart `json:"charts"`
Labels []Label `json:"labels"`
+ Panels []Panel `json:"panels"`
}
type UnifiedPosition struct {
@@ -57,6 +58,14 @@ type Label struct {
Value string `json:"label_value"`
}
+type Panel struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Type string `json:"type"`
+ Position UnifiedPosition `json:"position"`
+ Body map[string]any `json:"body"`
+}
+
type YAxis struct {
Min float64 `json:"min"`
Max float64 `json:"max"`
diff --git a/docs/resources/dashboard.md b/docs/resources/dashboard.md
index 541a7efc..b43c1e00 100644
--- a/docs/resources/dashboard.md
+++ b/docs/resources/dashboard.md
@@ -160,6 +160,7 @@ Required:
Optional:
- `chart` (Block Set) (see [below for nested schema](#nestedblock--group--chart))
+- `service_health_panel` (Block Set) A dashboard panel to view the health of your services (see [below for nested schema](#nestedblock--group--service_health_panel))
- `text_panel` (Block List) (see [below for nested schema](#nestedblock--group--text_panel))
- `title` (String)
@@ -240,6 +241,34 @@ Required:
+
+### Nested Schema for `group.service_health_panel`
+
+Optional:
+
+- `height` (Number)
+- `name` (String)
+- `panel_options` (Block Set, Max: 1) custom options for the service health panel (see [below for nested schema](#nestedblock--group--service_health_panel--panel_options))
+- `width` (Number)
+- `x_pos` (Number)
+- `y_pos` (Number)
+
+Read-Only:
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `group.service_health_panel.panel_options`
+
+Optional:
+
+- `change_since` (String)
+- `percentile` (String)
+- `sort_by` (String)
+- `sort_direction` (String)
+
+
+
### Nested Schema for `group.text_panel`
diff --git a/docs/resources/metric_dashboard.md b/docs/resources/metric_dashboard.md
index da01b075..69d249a9 100644
--- a/docs/resources/metric_dashboard.md
+++ b/docs/resources/metric_dashboard.md
@@ -200,6 +200,7 @@ Required:
Optional:
- `chart` (Block Set) (see [below for nested schema](#nestedblock--group--chart))
+- `service_health_panel` (Block Set) A dashboard panel to view the health of your services (see [below for nested schema](#nestedblock--group--service_health_panel))
- `text_panel` (Block List) (see [below for nested schema](#nestedblock--group--text_panel))
- `title` (String)
@@ -297,6 +298,34 @@ Required:
+
+### Nested Schema for `group.service_health_panel`
+
+Optional:
+
+- `height` (Number)
+- `name` (String)
+- `panel_options` (Block Set, Max: 1) custom options for the service health panel (see [below for nested schema](#nestedblock--group--service_health_panel--panel_options))
+- `width` (Number)
+- `x_pos` (Number)
+- `y_pos` (Number)
+
+Read-Only:
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `group.service_health_panel.panel_options`
+
+Optional:
+
+- `change_since` (String)
+- `percentile` (String)
+- `sort_by` (String)
+- `sort_direction` (String)
+
+
+
### Nested Schema for `group.text_panel`
diff --git a/lightstep/resource_dashboard.go b/lightstep/resource_dashboard.go
index b57ed024..daa55967 100644
--- a/lightstep/resource_dashboard.go
+++ b/lightstep/resource_dashboard.go
@@ -131,7 +131,7 @@ func getQueriesFromUnifiedDashboardResourceData(
qs := map[string]interface{}{
"hidden": q.Hidden,
"display": q.Display,
- "display_type_options": displayTypeOptionsFromResourceData(q.DisplayTypeOptions),
+ "display_type_options": convertNestedMapToSchemaSet(q.DisplayTypeOptions),
"query_name": q.Name,
"query_string": q.TQLQuery,
"dependency_map_options": getDependencyMapOptions(q.DependencyMapOptions),
@@ -155,15 +155,6 @@ func getQueriesFromUnifiedDashboardResourceData(
return queries, nil
}
-func displayTypeOptionsFromResourceData(opts map[string]interface{}) *schema.Set {
- // "display_type_options" is a set that always has at most one element, so
- // the hash function is trivial
- f := func(i interface{}) int {
- return 1
- }
- return schema.NewSet(f, []interface{}{opts})
-}
-
func getDependencyMapOptions(options *client.DependencyMapOptions) []interface{} {
if options == nil {
return nil
diff --git a/lightstep/resource_metric_dashboard.go b/lightstep/resource_metric_dashboard.go
index 24105303..96104d77 100644
--- a/lightstep/resource_metric_dashboard.go
+++ b/lightstep/resource_metric_dashboard.go
@@ -6,11 +6,11 @@ import (
"net/http"
"strings"
- "github.com/lightstep/terraform-provider-lightstep/client"
-
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+
+ "github.com/lightstep/terraform-provider-lightstep/client"
)
type ChartSchemaType int
@@ -129,11 +129,12 @@ func getGroupSchema(chartSchemaType ChartSchemaType) map[string]*schema.Schema {
"text_panel": {
Type: schema.TypeList,
Optional: true,
- Computed: true, // the panels can be mutated individually; chart mutations should not trigger group updates
+ Computed: true, // the panels can be mutated individually; text panel mutations should not trigger group updates
Elem: &schema.Resource{
Schema: getTextPanelSchema(),
},
},
+ ServiceHealthPanel: getServiceHealthPanelSchema(),
}
}
@@ -168,14 +169,26 @@ func getPanelSchema(isNameRequired bool) map[string]*schema.Schema {
}
}
- return map[string]*schema.Schema{
- // Alias for what we refer to as title elsewhere
- "name": nameSchema(),
- "description": {
- Type: schema.TypeString,
- Optional: true,
- Default: "",
+ return mergeSchemas(
+ getPositionSchema(),
+ map[string]*schema.Schema{
+ // Alias for what we refer to as title elsewhere
+ "name": nameSchema(),
+ "description": {
+ Type: schema.TypeString,
+ Optional: true,
+ Default: "",
+ },
+ "id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
},
+ )
+}
+
+func getPositionSchema() map[string]*schema.Schema {
+ return map[string]*schema.Schema{
"x_pos": {
Type: schema.TypeInt,
ValidateFunc: validation.IntAtLeast(0),
@@ -200,10 +213,6 @@ func getPanelSchema(isNameRequired bool) map[string]*schema.Schema {
Default: 0,
Optional: true,
},
- "id": {
- Type: schema.TypeString,
- Computed: true,
- },
}
}
@@ -487,6 +496,10 @@ func buildGroups(groupsIn []interface{}, legacyChartsIn []interface{}) ([]client
if err != nil {
return nil, hasLegacyChartsIn, err
}
+ serviceHealthPanels, err := convertServiceHealthFromResourceToApiRequest(group[ServiceHealthPanel])
+ if err != nil {
+ return nil, hasLegacyChartsIn, err
+ }
g := client.UnifiedGroup{
ID: group["id"].(string),
@@ -494,6 +507,7 @@ func buildGroups(groupsIn []interface{}, legacyChartsIn []interface{}) ([]client
Title: group["title"].(string),
VisibilityType: group["visibility_type"].(string),
Charts: append(chartPanels, textPanels...),
+ Panels: serviceHealthPanels,
}
newGroups = append(newGroups, g)
}
@@ -663,6 +677,8 @@ func (p *resourceUnifiedDashboardImp) setResourceDataFromUnifiedDashboard(projec
group["chart"] = groupCharts
group["text_panel"] = groupTextPanels
+ group[ServiceHealthPanel] = convertServiceHealthfromApiRequestToResource(g.Panels)
+
groups = append(groups, group)
}
if err := d.Set("group", groups); err != nil {
diff --git a/lightstep/resource_metric_dashboard_test.go b/lightstep/resource_metric_dashboard_test.go
index a0430105..e1a08f4a 100644
--- a/lightstep/resource_metric_dashboard_test.go
+++ b/lightstep/resource_metric_dashboard_test.go
@@ -402,6 +402,68 @@ resource "lightstep_metric_dashboard" "test" {
})
}
+func TestAccDashboardServiceHealthPanel(t *testing.T) {
+ var dashboard client.UnifiedDashboard
+
+ dashboardConfig := `
+resource "lightstep_metric_dashboard" "test" {
+ project_name = "` + testProject + `"
+ dashboard_name = "Acceptance Test Dashboard (TestAccDashboardServiceHealthPanel)"
+ dashboard_description = "Dashboard to test the service health panel"
+
+ group {
+ rank = 0
+ visibility_type = "implicit"
+
+ service_health_panel {
+ name = "test_service_health_panel"
+
+ x_pos = 0
+ y_pos = 0
+ width = 10
+ height = 10
+
+ panel_options {
+ sort_direction = "asc"
+ sort_by = "latency"
+ }
+ }
+ }
+}
+`
+ // Change the chart name and metric name
+ updatedConfig := strings.Replace(dashboardConfig, "asc", "desc", -1)
+
+ resourceName := "lightstep_metric_dashboard.test"
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy: testGetMetricDashboardDestroy,
+ Steps: []resource.TestStep{
+ {
+ // Create the initial dashboard with a service health panel. verify the name and panel_options.percentile
+ Config: dashboardConfig,
+ Check: resource.ComposeTestCheckFunc(
+ testAccCheckMetricDashboardExists(resourceName, &dashboard),
+ resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.name", "test_service_health_panel"),
+ resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.panel_options.0.sort_by", "latency"),
+ resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.panel_options.0.sort_direction", "asc"),
+ ),
+ },
+ {
+ // Updated config will contain the new metric and chart name
+ Config: updatedConfig,
+ Check: resource.ComposeTestCheckFunc(
+ testAccCheckMetricDashboardExists(resourceName, &dashboard),
+ resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.name", "test_service_health_panel"),
+ resource.TestCheckResourceAttr(resourceName, "group.0.service_health_panel.0.panel_options.0.sort_direction", "desc"),
+ ),
+ },
+ },
+ })
+}
+
func Test_buildLabels(t *testing.T) {
tests := []struct {
name string
diff --git a/lightstep/service_health_panel.go b/lightstep/service_health_panel.go
new file mode 100644
index 00000000..3be11ae0
--- /dev/null
+++ b/lightstep/service_health_panel.go
@@ -0,0 +1,163 @@
+package lightstep
+
+import (
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+
+ "github.com/lightstep/terraform-provider-lightstep/client"
+)
+
+const (
+ ServiceHealthPanel = "service_health_panel"
+ ServiceHealthType = "service_health"
+ ServiceHealthPanelDesc = "A dashboard panel to view the health of your services"
+)
+
+func getServiceHealthPanelSchema() *schema.Schema {
+ elements := mergeSchemas(
+ getPositionSchema(),
+ map[string]*schema.Schema{
+ "name": {
+ Type: schema.TypeString,
+ Optional: true,
+ Default: "Service Health Panel",
+ },
+ "id": {
+ Type: schema.TypeString,
+ Computed: true,
+ },
+ "panel_options": {
+ Type: schema.TypeSet,
+ Optional: true,
+ Description: "custom options for the service health panel",
+ MaxItems: 1,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "sort_by": {
+ Type: schema.TypeString,
+ Optional: true,
+ ValidateFunc: validation.StringInSlice([]string{
+ "service",
+ "latency",
+ "error",
+ "rate",
+ }, false),
+ },
+ "sort_direction": {
+ Type: schema.TypeString,
+ Optional: true,
+ ValidateFunc: validation.StringInSlice([]string{
+ "asc",
+ "desc",
+ "error",
+ "rate",
+ }, false),
+ },
+ "percentile": {
+ Type: schema.TypeString,
+ Optional: true,
+ ValidateFunc: validation.StringInSlice([]string{
+ "p50",
+ "p90",
+ "p95",
+ "p99",
+ }, false),
+ },
+ "change_since": {
+ Type: schema.TypeString,
+ Optional: true,
+ ValidateFunc: validation.StringInSlice([]string{
+ "1h",
+ "1d",
+ "3d",
+ "p99",
+ }, false),
+ },
+ },
+ },
+ },
+ },
+ )
+
+ return &schema.Schema{
+ Type: schema.TypeSet,
+ Optional: true,
+ Computed: true, // the panels can be mutated individually; panel mutations should not trigger group updates
+ Description: ServiceHealthPanelDesc,
+ Elem: &schema.Resource{Schema: elements},
+ }
+}
+
+func convertServiceHealthFromResourceToApiRequest(serviceHealthPanelsIn interface{}) ([]client.Panel, error) {
+ in := serviceHealthPanelsIn.(*schema.Set).List()
+ var serviceHealthPanels []client.Panel
+
+ for _, s := range in {
+ serviceHealthPanel, ok := s.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("bad format, %v", s)
+ }
+ p := client.Panel{
+ ID: serviceHealthPanel["id"].(string),
+ Title: serviceHealthPanel["name"].(string),
+ Type: ServiceHealthType,
+ Position: buildPosition(serviceHealthPanel),
+ }
+
+ p.Body = map[string]interface{}{}
+ // N.B. panel_options are optional, so we don't return an error if not found
+ if opts, ok := serviceHealthPanel["panel_options"].(*schema.Set); ok {
+ list := opts.List()
+ count := len(list)
+ if count > 1 {
+ return nil, fmt.Errorf("panel_options must be defined only once")
+ } else if count == 1 {
+ resourceDisplayOptions, ok := list[0].(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected format for panel_options")
+ }
+ for k, v := range resourceDisplayOptions {
+ // don't want to send 'sort_by: ""' to the API etc.
+ if v == "" {
+ delete(resourceDisplayOptions, k)
+ }
+ }
+ if len(resourceDisplayOptions) > 0 {
+ p.Body["display_options"] = resourceDisplayOptions
+ }
+ }
+ }
+ serviceHealthPanels = append(serviceHealthPanels, p)
+ }
+
+ return serviceHealthPanels, nil
+}
+
+func convertServiceHealthfromApiRequestToResource(apiPanels []client.Panel) []interface{} {
+ var serviceHealthPanelResources []interface{}
+ for _, p := range apiPanels {
+ if p.Type == ServiceHealthType {
+ resource := map[string]interface{}{}
+ // Alias for what we refer to as title elsewhere
+ resource["name"] = p.Title
+ resource["x_pos"] = p.Position.XPos
+ resource["y_pos"] = p.Position.YPos
+ resource["width"] = p.Position.Width
+ resource["height"] = p.Position.Height
+ resource["id"] = p.ID
+ // N.B. the panel body might be nil. panel_options are optional for the service health panel.
+ if p.Body != nil {
+ if maybeDisplayOptions, ok := p.Body["display_options"]; ok {
+ displayOptions, ok := maybeDisplayOptions.(map[string]interface{})
+ if ok {
+ resource["panel_options"] = convertNestedMapToSchemaSet(displayOptions)
+ }
+ }
+ }
+ serviceHealthPanelResources = append(serviceHealthPanelResources, resource)
+ }
+ }
+ return serviceHealthPanelResources
+}
diff --git a/lightstep/util.go b/lightstep/util.go
index 5f8064d1..c832dff1 100644
--- a/lightstep/util.go
+++ b/lightstep/util.go
@@ -11,3 +11,13 @@ func mergeSchemas(arr ...map[string]*schema.Schema) map[string]*schema.Schema {
}
return dst
}
+
+// takes a nested map (such as display_type_options or panel_options) and converts to a schema set
+func convertNestedMapToSchemaSet(opts map[string]interface{}) *schema.Set {
+ // nested maps contain a set that always has at most one element, so
+ // the hash function is trivial
+ f := func(i interface{}) int {
+ return 1
+ }
+ return schema.NewSet(f, []interface{}{opts})
+}