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
84 changes: 83 additions & 1 deletion docs/components/Prometheus.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
## Actions

<CardGrid>
<LinkCard title="Create Silence" href="#create-silence" description="Create a silence in Alertmanager to suppress alerts" />
<LinkCard title="Expire Silence" href="#expire-silence" description="Expire an active silence in Alertmanager" />
<LinkCard title="Get Alert" href="#get-alert" description="Get a Prometheus alert by name" />
</CardGrid>

Expand All @@ -24,7 +26,8 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";

Configure this integration with:
- **Prometheus Base URL**: URL of your Prometheus server (e.g., `https://prometheus.example.com`)
- **API Auth**: `none`, `basic`, or `bearer` for Prometheus API requests
- **Alertmanager Base URL** (optional): URL of your Alertmanager instance (e.g., `https://alertmanager.example.com`). Required for Silence components. If omitted, the Prometheus Base URL is used.
- **API Auth**: `none`, `basic`, or `bearer` for API requests
- **Webhook Secret** (recommended): If set, Alertmanager must send `Authorization: Bearer <token>` on webhook requests

### Alertmanager Setup (manual)
Expand Down Expand Up @@ -98,6 +101,85 @@ 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 (`POST /api/v2/silences`) to suppress matching alerts.

### Configuration

- **Matchers**: Required list of matchers. Each matcher has:
- **Name**: Label name to match
- **Value**: Label value to match
- **Is Regex**: Whether value is a regex pattern (default: false)
- **Is Equal**: Whether to match equality (true) or inequality (false) (default: true)
- **Duration**: Required duration string (e.g. `1h`, `30m`, `2h30m`)
- **Created By**: Required name of who is creating the silence
- **Comment**: Required reason for the silence

### Output

Emits one `prometheus.silence` payload with silence ID, status, matchers, timing, and creator info.

### Example Output

```json
{
"data": {
"comment": "Scheduled maintenance window for database migration",
"createdBy": "SuperPlane",
"endsAt": "2026-02-12T17:30:00Z",
"matchers": [
{
"isEqual": true,
"isRegex": false,
"name": "alertname",
"value": "HighLatency"
},
{
"isEqual": true,
"isRegex": false,
"name": "severity",
"value": "critical"
}
],
"silenceID": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
"startsAt": "2026-02-12T16:30:00Z",
"status": "active"
},
"timestamp": "2026-02-12T16:30:05.123456789Z",
"type": "prometheus.silence"
}
```

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

## Expire Silence

The Expire Silence component expires an active silence in Alertmanager (`DELETE /api/v2/silence/{silenceID}`).

### Configuration

- **Silence ID**: Required silence ID to expire. Supports expressions so users can reference `$['Create Silence'].silenceID`.

### Output

Emits one `prometheus.silence.expired` payload with silence ID and status.

### Example Output

```json
{
"data": {
"silenceID": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
"status": "expired"
},
"timestamp": "2026-02-12T17:45:10.987654321Z",
"type": "prometheus.silence.expired"
}
```

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

## Get Alert
Expand Down
120 changes: 106 additions & 14 deletions pkg/integrations/prometheus/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import (
const MaxResponseSize = 1 * 1024 * 1024 // 1MB

type Client struct {
baseURL string
authType string
username string
password string
bearerToken string
http core.HTTPContext
baseURL string
alertmanagerURL string
authType string
username string
password string
bearerToken string
http core.HTTPContext
}

type prometheusResponse[T any] struct {
Expand All @@ -41,6 +42,25 @@ type PrometheusAlert struct {
Value string `json:"value,omitempty"`
}

type Matcher struct {
Name string `json:"name"`
Value string `json:"value"`
IsRegex bool `json:"isRegex"`
IsEqual bool `json:"isEqual"`
}

type SilencePayload struct {
Matchers []Matcher `json:"matchers"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
CreatedBy string `json:"createdBy"`
Comment string `json:"comment"`
}

type silenceResponse struct {
SilenceID string `json:"silenceID"`
}

func NewClient(httpContext core.HTTPContext, integration core.IntegrationContext) (*Client, error) {
baseURL, err := requiredConfig(integration, "baseURL")
if err != nil {
Expand All @@ -52,10 +72,13 @@ func NewClient(httpContext core.HTTPContext, integration core.IntegrationContext
return nil, err
}

alertmanagerURL := optionalConfig(integration, "alertmanagerURL")

client := &Client{
baseURL: normalizeBaseURL(baseURL),
authType: authType,
http: httpContext,
baseURL: normalizeBaseURL(baseURL),
alertmanagerURL: normalizeBaseURL(alertmanagerURL),
authType: authType,
http: httpContext,
}

switch authType {
Expand Down Expand Up @@ -87,6 +110,14 @@ func NewClient(httpContext core.HTTPContext, integration core.IntegrationContext
}
}

func optionalConfig(ctx core.IntegrationContext, name string) string {
value, err := ctx.GetConfig(name)
if err != nil {
return ""
}
return string(value)
}

func requiredConfig(ctx core.IntegrationContext, name string) (string, error) {
value, err := ctx.GetConfig(name)
if err != nil {
Expand Down Expand Up @@ -150,45 +181,106 @@ func (c *Client) Query(query string) (map[string]any, error) {
return response.Data, nil
}

func (c *Client) alertmanagerBaseURL() string {
if c.alertmanagerURL != "" {
return c.alertmanagerURL
}
return c.baseURL
}

func (c *Client) CreateSilence(silence SilencePayload) (string, error) {
jsonBody, err := json.Marshal(silence)
if err != nil {
return "", fmt.Errorf("failed to marshal silence payload: %w", err)
}

apiURL := c.alertmanagerBaseURL() + "/api/v2/silences"
body, err := c.execRequestWithBodyAndURL(http.MethodPost, apiURL, strings.NewReader(string(jsonBody)))
if err != nil {
return "", err
}

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

return response.SilenceID, nil
}

func (c *Client) ExpireSilence(silenceID string) error {
apiURL := fmt.Sprintf("%s/api/v2/silence/%s", c.alertmanagerBaseURL(), silenceID)
_, err := c.execRequestWithBodyAndURL(http.MethodDelete, apiURL, nil)
return err
}

func (c *Client) execRequest(method string, path string) ([]byte, error) {
return c.execRequestWithBody(method, path, nil)
}

func (c *Client) execRequestWithBodyAndURL(method string, fullURL string, body io.Reader) ([]byte, error) {
req, err := http.NewRequest(method, fullURL, body)
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
}

return c.doRequest(req)
}

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

req, err := http.NewRequest(method, apiURL, nil)
req, err := http.NewRequest(method, apiURL, body)
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
}

return c.doRequest(req)
}

func (c *Client) doRequest(req *http.Request) ([]byte, error) {
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)
body, err := io.ReadAll(limitedReader)
responseBody, err := io.ReadAll(limitedReader)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

if len(body) > MaxResponseSize {
if len(responseBody) > 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(body))
return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, string(responseBody))
}

return body, nil
return responseBody, nil
}

func (c *Client) setAuth(req *http.Request) error {
Expand Down
Loading