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

<CardGrid>
<LinkCard title="Create Silence" href="#create-silence" description="Create a silence in Alertmanager" />
<LinkCard title="Expire Silence" href="#expire-silence" description="Expire (delete) a silence in Alertmanager" />
<LinkCard title="Get Alert" href="#get-alert" description="Get a Prometheus alert by name" />
<LinkCard title="Get Silence" href="#get-silence" description="Get a silence by ID from Alertmanager" />
<LinkCard title="Query" href="#query" description="Execute an instant query against Prometheus" />
<LinkCard title="Query Range" href="#query-range" description="Execute a range query against Prometheus" />
</CardGrid>

## Instructions
Expand Down Expand Up @@ -98,6 +103,74 @@ After updating Alertmanager config, reload it (for example `POST /-/reload` when
}
```

<a id="create-silence"></a>

## Create Silence

The Create Silence component creates a silence in Alertmanager to suppress matching alerts for a given duration.

### Configuration

- **Matchers**: Required list of label matchers for the silence
- **Duration**: Required duration for the silence (e.g., `1h`, `30m`, `2h30m`)
- **Created By**: Required author of the silence (supports expressions)
- **Comment**: Required reason for the silence (supports expressions)

### Output

Emits one `prometheus.silence` payload with the silence ID, matchers, timing, and author fields.

### Example Output

```json
{
"data": {
"comment": "Silenced by SuperPlane workflow",
"createdBy": "SuperPlane",
"endsAt": "2026-02-12T17:00:00Z",
"matchers": [
{
"isEqual": true,
"isRegex": false,
"name": "alertname",
"value": "SuperplaneTestAlert"
}
],
"silenceID": "a1b4c5d6-e7f8-9012-3456-789abcdef012",
"startsAt": "2026-02-12T16:00:00Z"
},
"timestamp": "2026-02-12T16:00:01.123456789Z",
"type": "prometheus.silence"
}
```

<a id="expire-silence"></a>

## Expire Silence

The Expire Silence component expires an active silence in Alertmanager, causing suppressed alerts to fire again.

### Configuration

- **Silence ID**: Required ID of the silence to expire (supports expressions)

### Output

Emits one `prometheus.silence` payload confirming the expired silence ID.

### Example Output

```json
{
"data": {
"silenceID": "a1b4c5d6-e7f8-9012-3456-789abcdef012",
"state": "expired"
},
"timestamp": "2026-02-12T17:00:01.123456789Z",
"type": "prometheus.silence"
}
```

<a id="get-alert"></a>

## Get Alert
Expand Down Expand Up @@ -135,3 +208,134 @@ Emits one `prometheus.alert` payload with labels, annotations, state, and timing
}
```

<a id="get-silence"></a>

## Get Silence

The Get Silence component retrieves a silence from Alertmanager by its ID.

### Configuration

- **Silence ID**: Required ID of the silence to retrieve (supports expressions)

### Output

Emits one `prometheus.silence` payload with the full silence details including matchers, timing, state, and author fields.

### Example Output

```json
{
"data": {
"comment": "Silenced by SuperPlane workflow",
"createdBy": "SuperPlane",
"endsAt": "2026-02-12T17:00:00Z",
"matchers": [
{
"isEqual": true,
"isRegex": false,
"name": "alertname",
"value": "SuperplaneTestAlert"
}
],
"silenceID": "a1b4c5d6-e7f8-9012-3456-789abcdef012",
"startsAt": "2026-02-12T16:00:00Z",
"state": "active"
},
"timestamp": "2026-02-12T16:30:00.123456789Z",
"type": "prometheus.silence"
}
```

<a id="query"></a>

## Query

The Query component executes an instant PromQL query against the Prometheus API (`/api/v1/query`).

### Configuration

- **Query**: Required PromQL expression to evaluate (supports expressions)

### Output

Emits one `prometheus.queryResult` payload with the query result data including resultType and result array.

### Example Output

```json
{
"data": {
"query": "up",
"result": [
{
"metric": {
"__name__": "up",
"instance": "localhost:9090",
"job": "prometheus"
},
"value": [
1707753600,
"1"
]
}
],
"resultType": "vector"
},
"timestamp": "2026-02-12T16:00:01.123456789Z",
"type": "prometheus.queryResult"
}
```

<a id="query-range"></a>

## Query Range

The Query Range component executes a range PromQL query against the Prometheus API (`/api/v1/query_range`).

### Configuration

- **Query**: Required PromQL expression to evaluate (supports expressions)
- **Start**: Required start timestamp in RFC3339 format or relative duration (supports expressions)
- **End**: Required end timestamp in RFC3339 format or relative duration (supports expressions)
- **Step**: Required query resolution step width as a duration (e.g., `15s`, `1m`, `5m`)

### Output

Emits one `prometheus.queryResult` payload with the range query result data including resultType and result array.

### Example Output

```json
{
"data": {
"end": "2026-02-12T16:00:00Z",
"query": "rate(http_requests_total[5m])",
"result": [
{
"metric": {
"__name__": "http_requests_total",
"instance": "localhost:9090",
"job": "prometheus"
},
"values": [
[
1707750000,
"0.5"
],
[
1707750015,
"0.6"
]
]
}
],
"resultType": "matrix",
"start": "2026-02-12T15:00:00Z",
"step": "15s"
},
"timestamp": "2026-02-12T16:00:01.123456789Z",
"type": "prometheus.queryResult"
}
```

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

import (
"bytes"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -150,6 +151,167 @@ func (c *Client) Query(query string) (map[string]any, error) {
return response.Data, nil
}

// AlertmanagerSilence represents a silence object from the Alertmanager API.
type AlertmanagerSilence struct {
ID string `json:"id"`
Status *SilenceStatus `json:"status,omitempty"`
Matchers []SilenceMatcher `json:"matchers"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
CreatedBy string `json:"createdBy"`
Comment string `json:"comment"`
UpdatedAt string `json:"updatedAt,omitempty"`
}

// SilenceStatus represents the status of a silence.
type SilenceStatus struct {
State string `json:"state"`
}

// SilenceMatcher represents a matcher within a silence.
type SilenceMatcher struct {
Name string `json:"name"`
Value string `json:"value"`
IsRegex bool `json:"isRegex"`
IsEqual bool `json:"isEqual"`
}

// CreateSilenceRequest is the request body for creating a silence.
type CreateSilenceRequest struct {
Matchers []SilenceMatcher `json:"matchers"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
CreatedBy string `json:"createdBy"`
Comment string `json:"comment"`
}

// CreateSilenceResponse is the response from creating a silence.
type CreateSilenceResponse struct {
SilenceID string `json:"silenceID"`
}

// CreateSilence creates a silence in Alertmanager.
func (c *Client) CreateSilence(request CreateSilenceRequest) (string, error) {
body, err := json.Marshal(request)
if err != nil {
return "", fmt.Errorf("failed to marshal request: %w", err)
}

respBody, err := c.execRequestWithBody(http.MethodPost, "/api/v2/silences", body)
if err != nil {
return "", err
}

response := CreateSilenceResponse{}
if err := decodeResponse(respBody, &response); err != nil {
return "", err
}

if response.SilenceID == "" {
return "", fmt.Errorf("empty silence ID in response")
}

return response.SilenceID, nil
}

// ExpireSilence expires (deletes) a silence in Alertmanager.
func (c *Client) ExpireSilence(silenceID string) error {
path := fmt.Sprintf("/api/v2/silence/%s", url.PathEscape(silenceID))
_, err := c.execRequestWithBody(http.MethodDelete, path, nil)
return err
}

// GetSilence retrieves a silence by ID from Alertmanager.
func (c *Client) GetSilence(silenceID string) (*AlertmanagerSilence, error) {
path := fmt.Sprintf("/api/v2/silence/%s", url.PathEscape(silenceID))
body, err := c.execRequest(http.MethodGet, path)
if err != nil {
return nil, err
}

silence := AlertmanagerSilence{}
if err := decodeResponse(body, &silence); err != nil {
return nil, err
}

return &silence, nil
}

// QueryRange executes a range query against the Prometheus API.
func (c *Client) QueryRange(query, start, end, step string) (map[string]any, error) {
params := url.Values{}
params.Set("query", query)
params.Set("start", start)
params.Set("end", end)
params.Set("step", step)

apiPath := fmt.Sprintf("/api/v1/query_range?%s", params.Encode())
body, err := c.execRequest(http.MethodGet, apiPath)
if err != nil {
return nil, err
}

response := prometheusResponse[map[string]any]{}
if err := decodeResponse(body, &response); err != nil {
return nil, err
}

if response.Status != "success" {
return nil, formatPrometheusError(response.ErrorType, response.Error)
}

return response.Data, nil
}

func (c *Client) execRequestWithBody(method string, path string, body []byte) ([]byte, error) {
apiURL := c.baseURL
if strings.HasPrefix(path, "/") {
apiURL += path
} else {
apiURL += "/" + path
}

var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewReader(body)
}

req, err := http.NewRequest(method, apiURL, bodyReader)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if err := c.setAuth(req); err != nil {
return nil, err
}

res, err := c.http.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer res.Body.Close()

limitedReader := io.LimitReader(res.Body, MaxResponseSize+1)
respBody, err := io.ReadAll(limitedReader)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

if len(respBody) > MaxResponseSize {
return nil, fmt.Errorf("response too large: exceeds maximum size of %d bytes", MaxResponseSize)
}

if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, string(respBody))
}

return respBody, nil
}

func (c *Client) execRequest(method string, path string) ([]byte, error) {
apiURL := c.baseURL
if strings.HasPrefix(path, "/") {
Expand Down
Loading