Skip to content
Open
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
133 changes: 133 additions & 0 deletions docs/components/Dash0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,100 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
## Actions

<CardGrid>
<LinkCard title="Create Synthetic Check" href="#create-synthetic-check" description="Create a synthetic check in Dash0 configuration API" />
<LinkCard title="Get Check Details" href="#get-check-details" description="Get detailed information for a Dash0 check by ID" />
<LinkCard title="List Issues" href="#list-issues" description="Query Dash0 to get a list of all current issues using the metric dash0.issue.status" />
<LinkCard title="Query Prometheus" href="#query-prometheus" description="Execute a PromQL query against Dash0 Prometheus API and return the response data" />
<LinkCard title="Send Log Event" href="#send-log-event" description="Send one or more workflow log records to Dash0 OTLP HTTP ingestion" />
</CardGrid>

<a id="create-synthetic-check"></a>

## Create Synthetic Check

The Create Synthetic Check component creates a Dash0 synthetic check using the configuration API.

### Use Cases

- **Service onboarding**: Create synthetic checks when new services are deployed
- **Environment bootstrap**: Provision baseline uptime checks in new environments
- **Automation workflows**: Create checks from CI/CD or incident workflows

### Configuration

- **Origin or ID (Optional)**: Custom synthetic check identifier. If omitted, SuperPlane generates one.
- **Name**: Human-readable synthetic check name
- **Enabled**: Whether the synthetic check is enabled
- **Plugin Kind**: Synthetic check plugin type (currently HTTP)
- **Method**: HTTP method for request checks
- **URL**: Target URL for the synthetic check
- **Headers (Optional)**: Request header key/value pairs
- **Request Body (Optional)**: HTTP request body (useful for POST/PUT/PATCH)

### Output

Emits:
- **originOrId**: Synthetic check identifier used for the API request
- **response**: Raw Dash0 API response

### Example Output

```json
{
"data": {
"originOrId": "superplane-synthetic-ab12cd34",
"response": {
"id": "superplane-synthetic-ab12cd34",
"status": "updated"
}
},
"timestamp": "2026-02-09T12:00:00Z",
"type": "dash0.synthetic.check.created"
}
```

<a id="get-check-details"></a>

## Get Check Details

The Get Check Details component fetches full context for a Dash0 check by ID.

### Use Cases

- **Alert enrichment**: Expand webhook payloads with full check context before notifying responders
- **Workflow branching**: Use check attributes (severity, thresholds, services) in downstream conditions
- **Incident automation**: Add rich check details to incident tickets or chat messages

### Configuration

- **Check ID**: The Dash0 check identifier to retrieve
- **Include History**: Include additional history data when supported by the Dash0 API

### Output

Emits a payload containing:
- **checkId**: Check identifier used in the request
- **details**: Raw details response from Dash0

### Example Output

```json
{
"data": {
"checkId": "check-123",
"details": {
"currentValue": 0.92,
"id": "check-123",
"name": "Checkout API latency",
"query": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service=\"checkout\"}[5m])) by (le))",
"severity": "critical"
}
},
"timestamp": "2026-02-09T12:00:00Z",
"type": "dash0.check.details.retrieved"
}
```

<a id="list-issues"></a>

## List Issues
Expand Down Expand Up @@ -150,3 +240,46 @@ Returns the Prometheus query response including:
}
```

<a id="send-log-event"></a>

## Send Log Event

The Send Log Event component sends workflow log records to Dash0 using OTLP HTTP ingestion.

### Use Cases

- **Audit trails**: Record workflow milestones in Dash0 logs
- **Change tracking**: Emit deployment, approval, and remediation events
- **Observability correlation**: Correlate workflow activity with traces and metrics

### Configuration

- **Service Name**: Service name attached to emitted records
- **Records**: One or more log records containing:
- message
- severity
- timestamp (optional, RFC3339 or unix)
- attributes (optional key/value map)

### Output

Emits:
- **serviceName**: Service name used for ingestion
- **sentCount**: Number of records sent in this request

### Example Output

```json
{
"data": {
"response": {
"status": "ok"
},
"sentCount": 2,
"serviceName": "superplane.workflow"
},
"timestamp": "2026-02-09T12:00:00Z",
"type": "dash0.log.event.sent"
}
```

191 changes: 184 additions & 7 deletions pkg/integrations/dash0/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dash0

import (
"bytes"
"encoding/json"
"fmt"
"io"
Expand All @@ -18,9 +19,11 @@ const (
)

type Client struct {
Token string
BaseURL string
http core.HTTPContext
Token string
BaseURL string
LogsIngestURL string
Dataset string
http core.HTTPContext
}

func NewClient(http core.HTTPContext, ctx core.IntegrationContext) (*Client, error) {
Expand All @@ -42,13 +45,66 @@ func NewClient(http core.HTTPContext, ctx core.IntegrationContext) (*Client, err
// Strip /api/prometheus if user included it in the base URL
baseURL = strings.TrimSuffix(baseURL, "/api/prometheus")

dataset := "default"
datasetConfig, err := ctx.GetConfig("dataset")
if err == nil && datasetConfig != nil && len(datasetConfig) > 0 {
trimmedDataset := strings.TrimSpace(string(datasetConfig))
if trimmedDataset != "" {
dataset = trimmedDataset
}
}

logsIngestURL := deriveLogsIngestURL(baseURL)

return &Client{
Token: string(apiToken),
BaseURL: baseURL,
http: http,
Token: string(apiToken),
BaseURL: baseURL,
LogsIngestURL: logsIngestURL,
Dataset: dataset,
http: http,
}, nil
}

// deriveLogsIngestURL derives the OTLP logs ingress host from the configured API base URL.
func deriveLogsIngestURL(baseURL string) string {
parsedURL, err := url.Parse(baseURL)
if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
return strings.TrimSuffix(baseURL, "/")
}

hostname := parsedURL.Hostname()
if strings.HasPrefix(hostname, "api.") {
hostname = "ingress." + strings.TrimPrefix(hostname, "api.")
}

if port := parsedURL.Port(); port != "" {
parsedURL.Host = fmt.Sprintf("%s:%s", hostname, port)
} else {
parsedURL.Host = hostname
}

parsedURL.Path = ""
parsedURL.RawPath = ""
parsedURL.RawQuery = ""
parsedURL.Fragment = ""

return strings.TrimSuffix(parsedURL.String(), "/")
}

// withDatasetQuery appends the configured dataset query parameter to a request URL.
func (c *Client) withDatasetQuery(requestURL string) (string, error) {
parsedURL, err := url.Parse(requestURL)
if err != nil {
return "", fmt.Errorf("error parsing request URL: %v", err)
}

query := parsedURL.Query()
query.Set("dataset", c.Dataset)
parsedURL.RawQuery = query.Encode()

return parsedURL.String(), nil
}

func (c *Client) execRequest(method, url string, body io.Reader, contentType string) ([]byte, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
Expand Down Expand Up @@ -168,8 +224,12 @@ type CheckRule struct {

func (c *Client) ListCheckRules() ([]CheckRule, error) {
apiURL := fmt.Sprintf("%s/api/alerting/check-rules", c.BaseURL)
requestURL, err := c.withDatasetQuery(apiURL)
if err != nil {
return nil, err
}

responseBody, err := c.execRequest(http.MethodGet, apiURL, nil, "")
responseBody, err := c.execRequest(http.MethodGet, requestURL, nil, "")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -207,3 +267,120 @@ func (c *Client) ListCheckRules() ([]CheckRule, error) {

return checkRules, nil
}

// SendLogEvents sends OTLP log batches to Dash0 ingestion endpoint.
func (c *Client) SendLogEvents(request OTLPLogsRequest) (map[string]any, error) {
requestURL := fmt.Sprintf("%s/v1/logs", c.LogsIngestURL)

body, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("error marshaling logs request: %v", err)
}

responseBody, err := c.execRequest(http.MethodPost, requestURL, bytes.NewReader(body), "application/json")
if err != nil {
return nil, err
}

parsed, err := parseJSONResponse(responseBody)
if err != nil {
return nil, fmt.Errorf("error parsing send log event response: %v", err)
}

return parsed, nil
}

// GetCheckDetails fetches check context by failed-check ID with check-rules fallback.
func (c *Client) GetCheckDetails(checkID string, includeHistory bool) (map[string]any, error) {
trimmedCheckID := strings.TrimSpace(checkID)
if trimmedCheckID == "" {
return nil, fmt.Errorf("check id is required")
}

querySuffix := ""
if includeHistory {
querySuffix = "?include_history=true"
}

escapedID := url.PathEscape(trimmedCheckID)
requestURL := fmt.Sprintf("%s/api/alerting/failed-checks/%s%s", c.BaseURL, escapedID, querySuffix)

responseBody, err := c.execRequest(http.MethodGet, requestURL, nil, "")
if err != nil {
if strings.Contains(err.Error(), "request got 404 code") {
fallbackURL := fmt.Sprintf("%s/api/alerting/check-rules/%s%s", c.BaseURL, escapedID, querySuffix)
responseBody, err = c.execRequest(http.MethodGet, fallbackURL, nil, "")
if err != nil {
return nil, fmt.Errorf("fallback check-rules lookup failed: %v", err)
}
} else {
return nil, err
}
}

parsed, err := parseJSONResponse(responseBody)
if err != nil {
return nil, fmt.Errorf("error parsing check details response: %v", err)
}

if _, ok := parsed["checkId"]; !ok {
parsed["checkId"] = trimmedCheckID
}

return parsed, nil
}

// UpsertSyntheticCheck creates or updates a synthetic check by origin/id.
func (c *Client) UpsertSyntheticCheck(originOrID string, specification map[string]any) (map[string]any, error) {
trimmedOriginOrID := strings.TrimSpace(originOrID)
if trimmedOriginOrID == "" {
return nil, fmt.Errorf("origin/id is required")
}

requestURL := fmt.Sprintf("%s/api/synthetic-checks/%s", c.BaseURL, url.PathEscape(trimmedOriginOrID))
requestURL, err := c.withDatasetQuery(requestURL)
if err != nil {
return nil, err
}

body, err := json.Marshal(specification)
if err != nil {
return nil, fmt.Errorf("error marshaling request: %v", err)
}

responseBody, err := c.execRequest(http.MethodPut, requestURL, bytes.NewReader(body), "application/json")
if err != nil {
return nil, err
}

parsed, err := parseJSONResponse(responseBody)
if err != nil {
return nil, fmt.Errorf("error parsing upsert synthetic check response: %v", err)
}

if _, ok := parsed["originOrId"]; !ok {
parsed["originOrId"] = trimmedOriginOrID
}

return parsed, nil
}

// parseJSONResponse normalizes object or array JSON responses into a map.
func parseJSONResponse(responseBody []byte) (map[string]any, error) {
trimmedBody := strings.TrimSpace(string(responseBody))
if trimmedBody == "" {
return map[string]any{}, nil
}

var parsed map[string]any
if err := json.Unmarshal(responseBody, &parsed); err == nil {
return parsed, nil
}

var parsedArray []any
if err := json.Unmarshal(responseBody, &parsedArray); err == nil {
return map[string]any{"items": parsedArray}, nil
}

return nil, fmt.Errorf("unexpected response payload shape")
}
Loading