Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Logs Timeline Chart resource #448

Merged
merged 5 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions signalfx/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func Provider() *schema.Provider {
"signalfx_victor_ops_integration": integrationVictorOpsResource(),
"signalfx_webhook_integration": integrationWebhookResource(),
"signalfx_log_view": logViewResource(),
"signalfx_log_timeline": logTimelineResource(),
"signalfx_table_chart": tableChartResource(),
"signalfx_metric_ruleset": metricRulesetResource(),
},
Expand Down
217 changes: 217 additions & 0 deletions signalfx/resource_signalfx_log_timeline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package signalfx

import (
"context"
"encoding/json"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/signalfx/signalfx-go/chart"
"log"
)

func logTimelineResource() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Name of the chart",
},
"program_text": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: "Signalflow program text for the chart. More info at \"https://developers.signalfx.com/docs/signalflow-overview\"",
ValidateFunc: validation.StringLenBetween(16, 50000),
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "Description of the chart (Optional)",
},
"time_range": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Description: "Seconds to display in the visualization. This is a rolling range from the current time. Example: 3600 = `-1h`",
ConflictsWith: []string{"start_time", "end_time"},
},
"start_time": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Description: "Seconds since epoch to start the visualization",
ConflictsWith: []string{"time_range"},
},
"end_time": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Description: "Seconds since epoch to end the visualization",
ConflictsWith: []string{"time_range"},
},
"default_connection": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Description: "default connection that the dashboard uses",
},
"url": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Description: "URL of the chart",
},
},

Create: logTimelineCreate,
Read: logTimelineRead,
Update: logTimelineUpdate,
Delete: logTimelineDelete,
Exists: chartExists,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
}
}

func getPayloadLogTimeline(d *schema.ResourceData) *chart.CreateUpdateChartRequest {
var timeOptions *chart.TimeDisplayOptions
if val, ok := d.GetOk("time_range"); ok {
r := int64(val.(int) * 1000)
timeOptions = &chart.TimeDisplayOptions{
Range: &r,
Type: "relative",
}
}

if val, ok := d.GetOk("start_time"); ok {
start := int64(val.(int) * 1000)
timeOptions = &chart.TimeDisplayOptions{
Start: &start,
Type: "absolute",
}
if val, ok := d.GetOk("end_time"); ok {
end := int64(val.(int) * 1000)
timeOptions.End = &end
}
}

return &chart.CreateUpdateChartRequest{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
ProgramText: d.Get("program_text").(string),
Options: &chart.Options{
Time: timeOptions,
Type: "LogsTimeSeriesChart",
DefaultConnection: d.Get("default_connection").(string),
},
}
}

func logTimelineAPIToTF(d *schema.ResourceData, c *chart.Chart) error {
debugOutput, _ := json.Marshal(c)
log.Printf("[DEBUG] SignalFx: Got Log Timeline to enState: %s", string(debugOutput))

if err := d.Set("name", c.Name); err != nil {
return err
}
if err := d.Set("description", c.Description); err != nil {
return err
}
if err := d.Set("program_text", c.ProgramText); err != nil {
return err
}

options := c.Options

if options.Time != nil {
if options.Time.Type == "relative" {
if options.Time.Range != nil {
if err := d.Set("time_range", *options.Time.Range/1000); err != nil {
return err
}
}
} else {
if options.Time.Start != nil {
if err := d.Set("start_time", *options.Time.Start/1000); err != nil {
return err
}
}
if options.Time.End != nil {
if err := d.Set("end_time", *options.Time.End/1000); err != nil {
return err
}
}
}

}

if options.DefaultConnection != "" {
if err := d.Set("default_connection", options.DefaultConnection); err != nil {
return err
}
}

return nil
}

func logTimelineCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*signalfxConfig)
payload := getPayloadLogTimeline(d)

debugOutput, _ := json.Marshal(payload)
log.Printf("[DEBUG] SignalFx: Create Log Timeline Payload: %s", string(debugOutput))

c, err := config.Client.CreateChart(context.TODO(), payload)
if err != nil {
return err
}
// Since things worked, set the URL and move on
appURL, err := buildAppURL(config.CustomAppURL, CHART_APP_PATH+c.Id)
if err != nil {
return err
}
if err := d.Set("url", appURL); err != nil {
return err
}
d.SetId(c.Id)
log.Printf("[DEBUG] appURL in create: %s", string(appURL))

return logTimelineAPIToTF(d, c)
}

func logTimelineRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*signalfxConfig)
c, err := config.Client.GetChart(context.TODO(), d.Id())
if err != nil {
return err
}

appURL, err := buildAppURL(config.CustomAppURL, CHART_APP_PATH+c.Id)
if err != nil {
return err
}
if err := d.Set("url", appURL); err != nil {
return err
}
log.Printf("[DEBUG] appURL in read: %s", string(appURL))

return logTimelineAPIToTF(d, c)
}

func logTimelineUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*signalfxConfig)
payload := getPayloadLogTimeline(d)
debugOutput, _ := json.Marshal(payload)
log.Printf("[DEBUG] SignalFx: Update Log Tiemline Payload: %s", string(debugOutput))

c, err := config.Client.UpdateChart(context.TODO(), d.Id(), payload)
if err != nil {
return err
}
log.Printf("[DEBUG] SignalFx: Update Log Timeline Response: %v", c)

d.SetId(c.Id)
return logTimelineAPIToTF(d, c)
}

func logTimelineDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*signalfxConfig)

return config.Client.DeleteChart(context.TODO(), d.Id())
}
106 changes: 106 additions & 0 deletions signalfx/resource_signalfx_log_timeline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package signalfx

import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"testing"
)

const newLogTimelineConfig = `
resource "signalfx_log_timeline" "mychart1" {
name = "Chart Name"
description = "Chart Description"
program_text = "logs(index=['history','main','o11yhipster','splunklogger','summary']).publish()"

time_range = 900
default_connection = "Cosmicbat"
}
`

const updatedLogTimelineConfig = `
resource "signalfx_log_timeline" "mychart1" {
name = "Chart Name NEW"
description = "Chart Description NEW"
program_text = "logs().publish()"

start_time = 1657647022
end_time = 1657648042
}
`

func TestAccCreateUpdateLogTimeline(t *testing.T) {

resource.Test(t, resource.TestCase{
PreCheck: func() {
fmt.Printf("HERE")
testAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccLogTimelineDestroy,
Steps: []resource.TestStep{
// Create It
{
Config: newLogTimelineConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckLogTimelineResourceExists,
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "name", "Chart Name"),
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "description", "Chart Description"),
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "default_connection", "Cosmicbat"),
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "time_range", "900"),
),
},
{
ResourceName: "signalfx_log_timeline.mychart1",
ImportState: true,
ImportStateIdFunc: testAccStateIdFunc("signalfx_log_timeline.mychart1"),
ImportStateVerify: true,
},
// Update Everything
{
Config: updatedLogTimelineConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckLogTimelineResourceExists,
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "name", "Chart Name NEW"),
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "description", "Chart Description NEW"),
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "start_time", "1657647022"),
resource.TestCheckResourceAttr("signalfx_log_timeline.mychart1", "end_time", "1657648042"),
),
},
},
})
}

func testAccCheckLogTimelineResourceExists(s *terraform.State) error {
client := newTestClient()

for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "signalfx_log_timeline":
chart, err := client.GetChart(context.TODO(), rs.Primary.ID)
if chart.Id != rs.Primary.ID || err != nil {
return fmt.Errorf("error finding chart %s: %s", rs.Primary.ID, err)
}
default:
return fmt.Errorf("unexpected resource of type: %s", rs.Type)
}
}
return nil
}

func testAccLogTimelineDestroy(s *terraform.State) error {
client := newTestClient()
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "signalfx_log_timeline":
chart, _ := client.GetChart(context.TODO(), rs.Primary.ID)
if chart != nil {
return fmt.Errorf("found deleted chart %s", rs.Primary.ID)
}
default:
return fmt.Errorf("unexpected resource of type: %s", rs.Type)
}
}
return nil
}
47 changes: 47 additions & 0 deletions website/docs/r/log_timeline.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
layout: "signalfx"
page_title: "SignalFx: signalfx_log_timeline"
sidebar_current: "docs-signalfx-resource-log-timeline"
description: |-
Allows Terraform to create and manage log timelines
---

# Resource: signalfx_log_timeline

You can add logs data to your Observability Cloud dashboards without turning your logs into metrics first.
A log timeline chart displays timeline visualization in a dashboard and shows you in detail what is happening and why.

## Example Usage

```tf
resource "signalfx_log_timeline" "my_log_timeline" {
name = "Sample Log Timeline"
description = "Lorem ipsum dolor sit amet, laudem tibique iracundia at mea. Nam posse dolores ex, nec cu adhuc putent honestatis"

program_text = <<-EOF
logs(filter=field('message') == 'Transaction processed' and field('service.name') == 'paymentservice').publish()
EOF

time_range = 900

}
```

## Argument Reference

The following arguments are supported in the resource block:

* `name` - (Required) Name of the log timeline.
* `program_text` - (Required) Signalflow program text for the log timeline. More info at https://dev.splunk.com/observability/docs/.
* `description` - (Optional) Description of the log timeline.
* `time_range` - (Optional) From when to display data. SignalFx time syntax (e.g. `"-5m"`, `"-1h"`). Conflicts with `start_time` and `end_time`.
* `start_time` - (Optional) Seconds since epoch. Used for visualization. Conflicts with `time_range`.
* `end_time` - (Optional) Seconds since epoch. Used for visualization. Conflicts with `time_range`.
* `default_connection` - (Optional) The connection that the log timeline uses to fetch data. This could be Splunk Enterprise, Splunk Enterprise Cloud or Observability Cloud.

## Attributes Reference

In a addition to all arguments above, the following attributes are exported:

* `id` - The ID of the log timeline.
* `url` - The URL of the log timeline.
Loading