diff --git a/docs/components/Prometheus.mdx b/docs/components/Prometheus.mdx
index 81a9638572..68e04d85f3 100644
--- a/docs/components/Prometheus.mdx
+++ b/docs/components/Prometheus.mdx
@@ -18,6 +18,9 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
+
+
+
## Instructions
@@ -217,3 +220,133 @@ Emits one `prometheus.alert` payload with labels, annotations, state, and timing
}
```
+
+
+## Get Silence
+
+The Get Silence component retrieves a silence from Alertmanager (`GET /api/v2/silence/{silenceID}`) by its ID.
+
+### Configuration
+
+- **Silence**: Required silence to retrieve (supports expressions, e.g. `{{ $['Create Silence'].silenceID }}`)
+
+### Output
+
+Emits one `prometheus.silence` payload with silence ID, status, matchers, timing, and creator info.
+
+### Example Output
+
+```json
+{
+ "data": {
+ "comment": "Scheduled maintenance window",
+ "createdBy": "SuperPlane",
+ "endsAt": "2026-02-12T17:30:00Z",
+ "matchers": [
+ {
+ "isEqual": true,
+ "isRegex": false,
+ "name": "alertname",
+ "value": "HighLatency"
+ }
+ ],
+ "silenceID": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
+ "startsAt": "2026-02-12T16:30:00Z",
+ "status": "active"
+ },
+ "timestamp": "2026-02-12T16:30:05.123456789Z",
+ "type": "prometheus.silence"
+}
+```
+
+
+
+## Query
+
+The Query component executes an instant PromQL query against Prometheus (`GET /api/v1/query`).
+
+### Configuration
+
+- **Query**: Required PromQL expression to evaluate (supports expressions). Example: `up`
+
+### Output
+
+Emits one `prometheus.query` payload with the result type and results.
+
+### Example Output
+
+```json
+{
+ "data": {
+ "result": [
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "localhost:9090",
+ "job": "prometheus"
+ },
+ "value": [
+ 1708000000,
+ "1"
+ ]
+ }
+ ],
+ "resultType": "vector"
+ },
+ "timestamp": "2026-02-12T16:30:05.123456789Z",
+ "type": "prometheus.query"
+}
+```
+
+
+
+## Query Range
+
+The Query Range component executes a range PromQL query against Prometheus (`GET /api/v1/query_range`).
+
+### Configuration
+
+- **Query**: Required PromQL expression to evaluate (supports expressions). Example: `up`
+- **Start**: Required start timestamp in RFC3339 or Unix format (supports expressions). Example: `2026-01-01T00:00:00Z`
+- **End**: Required end timestamp in RFC3339 or Unix format (supports expressions). Example: `2026-01-02T00:00:00Z`
+- **Step**: Required query resolution step (e.g. `15s`, `1m`)
+
+### Output
+
+Emits one `prometheus.queryRange` payload with the result type and results.
+
+### Example Output
+
+```json
+{
+ "data": {
+ "result": [
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "localhost:9090",
+ "job": "prometheus"
+ },
+ "values": [
+ [
+ 1708000000,
+ "1"
+ ],
+ [
+ 1708000015,
+ "1"
+ ],
+ [
+ 1708000030,
+ "1"
+ ]
+ ]
+ }
+ ],
+ "resultType": "matrix"
+ },
+ "timestamp": "2026-02-12T16:30:05.123456789Z",
+ "type": "prometheus.queryRange"
+}
+```
+
diff --git a/pkg/integrations/prometheus/client.go b/pkg/integrations/prometheus/client.go
index a11008f9ba..8138beecbc 100644
--- a/pkg/integrations/prometheus/client.go
+++ b/pkg/integrations/prometheus/client.go
@@ -43,10 +43,10 @@ type PrometheusAlert struct {
}
type Matcher struct {
- Name string `json:"name"`
- Value string `json:"value"`
- IsRegex bool `json:"isRegex"`
- IsEqual bool `json:"isEqual"`
+ Name string `json:"name" mapstructure:"name"`
+ Value string `json:"value" mapstructure:"value"`
+ IsRegex bool `json:"isRegex" mapstructure:"isRegex"`
+ IsEqual bool `json:"isEqual" mapstructure:"isEqual"`
}
type SilencePayload struct {
@@ -62,9 +62,14 @@ type silenceResponse struct {
}
type AlertmanagerSilence struct {
- ID string `json:"id"`
- Status AlertmanagerSilenceStatus `json:"status"`
- Comment string `json:"comment"`
+ ID string `json:"id"`
+ Status AlertmanagerSilenceStatus `json:"status"`
+ Matchers []Matcher `json:"matchers"`
+ StartsAt string `json:"startsAt"`
+ EndsAt string `json:"endsAt"`
+ CreatedBy string `json:"createdBy"`
+ Comment string `json:"comment"`
+ UpdatedAt string `json:"updatedAt"`
}
type AlertmanagerSilenceStatus struct {
@@ -191,6 +196,30 @@ func (c *Client) Query(query string) (map[string]any, error) {
return response.Data, nil
}
+func (c *Client) QueryRange(query, start, end, step string) (map[string]any, error) {
+ apiPath := fmt.Sprintf("/api/v1/query_range?query=%s&start=%s&end=%s&step=%s",
+ url.QueryEscape(query),
+ url.QueryEscape(start),
+ url.QueryEscape(end),
+ url.QueryEscape(step),
+ )
+ 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) alertmanagerBaseURL() string {
if c.alertmanagerURL != "" {
return c.alertmanagerURL
@@ -239,6 +268,21 @@ func (c *Client) ExpireSilence(silenceID string) error {
return err
}
+func (c *Client) GetSilence(silenceID string) (AlertmanagerSilence, error) {
+ apiURL := fmt.Sprintf("%s/api/v2/silence/%s", c.alertmanagerBaseURL(), silenceID)
+ body, err := c.execRequestWithBodyAndURL(http.MethodGet, apiURL, nil)
+ if err != nil {
+ return AlertmanagerSilence{}, err
+ }
+
+ response := AlertmanagerSilence{}
+ if err := decodeResponse(body, &response); err != nil {
+ return AlertmanagerSilence{}, err
+ }
+
+ return response, nil
+}
+
func (c *Client) execRequest(method string, path string) ([]byte, error) {
return c.execRequestWithBody(method, path, nil)
}
diff --git a/pkg/integrations/prometheus/example.go b/pkg/integrations/prometheus/example.go
index af8e0a4ea8..bc88c20204 100644
--- a/pkg/integrations/prometheus/example.go
+++ b/pkg/integrations/prometheus/example.go
@@ -19,6 +19,15 @@ var exampleOutputCreateSilenceBytes []byte
//go:embed example_output_expire_silence.json
var exampleOutputExpireSilenceBytes []byte
+//go:embed example_output_get_silence.json
+var exampleOutputGetSilenceBytes []byte
+
+//go:embed example_output_query.json
+var exampleOutputQueryBytes []byte
+
+//go:embed example_output_query_range.json
+var exampleOutputQueryRangeBytes []byte
+
var exampleDataOnAlertOnce sync.Once
var exampleDataOnAlert map[string]any
@@ -31,6 +40,15 @@ var exampleOutputCreateSilence map[string]any
var exampleOutputExpireSilenceOnce sync.Once
var exampleOutputExpireSilence map[string]any
+var exampleOutputGetSilenceOnce sync.Once
+var exampleOutputGetSilence map[string]any
+
+var exampleOutputQueryOnce sync.Once
+var exampleOutputQuery map[string]any
+
+var exampleOutputQueryRangeOnce sync.Once
+var exampleOutputQueryRange map[string]any
+
func (t *OnAlert) ExampleData() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleDataOnAlertOnce, exampleDataOnAlertBytes, &exampleDataOnAlert)
}
@@ -46,3 +64,15 @@ func (c *CreateSilence) ExampleOutput() map[string]any {
func (c *ExpireSilence) ExampleOutput() map[string]any {
return utils.UnmarshalEmbeddedJSON(&exampleOutputExpireSilenceOnce, exampleOutputExpireSilenceBytes, &exampleOutputExpireSilence)
}
+
+func (c *GetSilence) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputGetSilenceOnce, exampleOutputGetSilenceBytes, &exampleOutputGetSilence)
+}
+
+func (c *Query) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputQueryOnce, exampleOutputQueryBytes, &exampleOutputQuery)
+}
+
+func (c *QueryRange) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputQueryRangeOnce, exampleOutputQueryRangeBytes, &exampleOutputQueryRange)
+}
diff --git a/pkg/integrations/prometheus/example_output_get_silence.json b/pkg/integrations/prometheus/example_output_get_silence.json
new file mode 100644
index 0000000000..d65ff67aac
--- /dev/null
+++ b/pkg/integrations/prometheus/example_output_get_silence.json
@@ -0,0 +1,20 @@
+{
+ "data": {
+ "silenceID": "a1b2c3d4-e5f6-4789-a012-3456789abcde",
+ "status": "active",
+ "matchers": [
+ {
+ "name": "alertname",
+ "value": "HighLatency",
+ "isRegex": false,
+ "isEqual": true
+ }
+ ],
+ "startsAt": "2026-02-12T16:30:00Z",
+ "endsAt": "2026-02-12T17:30:00Z",
+ "createdBy": "SuperPlane",
+ "comment": "Scheduled maintenance window"
+ },
+ "timestamp": "2026-02-12T16:30:05.123456789Z",
+ "type": "prometheus.silence"
+}
diff --git a/pkg/integrations/prometheus/example_output_query.json b/pkg/integrations/prometheus/example_output_query.json
new file mode 100644
index 0000000000..f86b5d12db
--- /dev/null
+++ b/pkg/integrations/prometheus/example_output_query.json
@@ -0,0 +1,17 @@
+{
+ "data": {
+ "resultType": "vector",
+ "result": [
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "localhost:9090",
+ "job": "prometheus"
+ },
+ "value": [1708000000, "1"]
+ }
+ ]
+ },
+ "timestamp": "2026-02-12T16:30:05.123456789Z",
+ "type": "prometheus.query"
+}
diff --git a/pkg/integrations/prometheus/example_output_query_range.json b/pkg/integrations/prometheus/example_output_query_range.json
new file mode 100644
index 0000000000..6844b10b8d
--- /dev/null
+++ b/pkg/integrations/prometheus/example_output_query_range.json
@@ -0,0 +1,21 @@
+{
+ "data": {
+ "resultType": "matrix",
+ "result": [
+ {
+ "metric": {
+ "__name__": "up",
+ "instance": "localhost:9090",
+ "job": "prometheus"
+ },
+ "values": [
+ [1708000000, "1"],
+ [1708000015, "1"],
+ [1708000030, "1"]
+ ]
+ }
+ ]
+ },
+ "timestamp": "2026-02-12T16:30:05.123456789Z",
+ "type": "prometheus.queryRange"
+}
diff --git a/pkg/integrations/prometheus/get_silence.go b/pkg/integrations/prometheus/get_silence.go
new file mode 100644
index 0000000000..038dd0ab82
--- /dev/null
+++ b/pkg/integrations/prometheus/get_silence.go
@@ -0,0 +1,173 @@
+package prometheus
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+)
+
+type GetSilence struct{}
+
+type GetSilenceConfiguration struct {
+ Silence string `json:"silence" mapstructure:"silence"`
+ LegacySilenceID string `json:"silenceID,omitempty" mapstructure:"silenceID"`
+}
+
+type GetSilenceNodeMetadata struct {
+ SilenceID string `json:"silenceID"`
+}
+
+type getSilencePayload struct {
+ SilenceID string `mapstructure:"silenceID"`
+ Status string `mapstructure:"status"`
+ Matchers []Matcher `mapstructure:"matchers"`
+ StartsAt string `mapstructure:"startsAt"`
+ EndsAt string `mapstructure:"endsAt"`
+ CreatedBy string `mapstructure:"createdBy"`
+ Comment string `mapstructure:"comment"`
+}
+
+func (c *GetSilence) Name() string {
+ return "prometheus.getSilence"
+}
+
+func (c *GetSilence) Label() string {
+ return "Get Silence"
+}
+
+func (c *GetSilence) Description() string {
+ return "Get a silence from Alertmanager by ID"
+}
+
+func (c *GetSilence) Documentation() string {
+ return `The Get Silence component retrieves a silence from Alertmanager (` + "`GET /api/v2/silence/{silenceID}`" + `) by its ID.
+
+## Configuration
+
+- **Silence**: Required silence to retrieve (supports expressions, e.g. ` + "`{{ $['Create Silence'].silenceID }}`" + `)
+
+## Output
+
+Emits one ` + "`prometheus.silence`" + ` payload with silence ID, status, matchers, timing, and creator info.`
+}
+
+func (c *GetSilence) Icon() string {
+ return "prometheus"
+}
+
+func (c *GetSilence) Color() string {
+ return "gray"
+}
+
+func (c *GetSilence) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *GetSilence) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "silence",
+ Label: "Silence",
+ Type: configuration.FieldTypeIntegrationResource,
+ Required: true,
+ Description: "Silence to retrieve",
+ TypeOptions: &configuration.TypeOptions{
+ Resource: &configuration.ResourceTypeOptions{
+ Type: ResourceTypeSilence,
+ },
+ },
+ },
+ }
+}
+
+func (c *GetSilence) Setup(ctx core.SetupContext) error {
+ config := GetSilenceConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+ config = sanitizeGetSilenceConfiguration(config)
+
+ if config.Silence == "" {
+ return fmt.Errorf("silence is required")
+ }
+
+ return nil
+}
+
+func (c *GetSilence) Execute(ctx core.ExecutionContext) error {
+ config := GetSilenceConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+ config = sanitizeGetSilenceConfiguration(config)
+
+ client, err := NewClient(ctx.HTTP, ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("failed to create Prometheus client: %w", err)
+ }
+
+ silence, err := client.GetSilence(config.Silence)
+ if err != nil {
+ return fmt.Errorf("failed to get silence: %w", err)
+ }
+
+ ctx.Metadata.Set(GetSilenceNodeMetadata{SilenceID: silence.ID})
+
+ p := getSilencePayload{
+ SilenceID: silence.ID,
+ Status: silence.Status.State,
+ Matchers: silence.Matchers,
+ StartsAt: silence.StartsAt,
+ EndsAt: silence.EndsAt,
+ CreatedBy: silence.CreatedBy,
+ Comment: silence.Comment,
+ }
+
+ var payload map[string]any
+ if err := mapstructure.Decode(p, &payload); err != nil {
+ return fmt.Errorf("failed to decode silence payload: %w", err)
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "prometheus.silence",
+ []any{payload},
+ )
+}
+
+func (c *GetSilence) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *GetSilence) HandleWebhook(ctx core.WebhookRequestContext) (int, error) {
+ return 200, nil
+}
+
+func (c *GetSilence) Actions() []core.Action {
+ return []core.Action{}
+}
+
+func (c *GetSilence) HandleAction(ctx core.ActionContext) error {
+ return nil
+}
+
+func (c *GetSilence) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *GetSilence) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
+
+func sanitizeGetSilenceConfiguration(config GetSilenceConfiguration) GetSilenceConfiguration {
+ config.Silence = strings.TrimSpace(config.Silence)
+ config.LegacySilenceID = strings.TrimSpace(config.LegacySilenceID)
+ if config.Silence == "" {
+ config.Silence = config.LegacySilenceID
+ }
+ return config
+}
diff --git a/pkg/integrations/prometheus/get_silence_test.go b/pkg/integrations/prometheus/get_silence_test.go
new file mode 100644
index 0000000000..b0066e47e5
--- /dev/null
+++ b/pkg/integrations/prometheus/get_silence_test.go
@@ -0,0 +1,129 @@
+package prometheus
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__GetSilence__Setup(t *testing.T) {
+ component := &GetSilence{}
+
+ t.Run("silence is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "silence": "",
+ },
+ })
+ require.ErrorContains(t, err, "silence is required")
+ })
+
+ t.Run("valid setup", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "silence": "abc123",
+ },
+ })
+ require.NoError(t, err)
+ })
+
+ t.Run("legacy silenceID setup still works", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "silenceID": "abc123",
+ },
+ })
+ require.NoError(t, err)
+ })
+}
+
+func Test__GetSilence__Execute(t *testing.T) {
+ component := &GetSilence{}
+
+ silenceJSON := `{
+ "id": "abc123",
+ "status": {"state": "active"},
+ "matchers": [{"name": "alertname", "value": "HighLatency", "isRegex": false, "isEqual": true}],
+ "startsAt": "2026-02-12T16:30:00Z",
+ "endsAt": "2026-02-12T17:30:00Z",
+ "createdBy": "SuperPlane",
+ "comment": "Maintenance window",
+ "updatedAt": "2026-02-12T16:30:00Z"
+ }`
+
+ t.Run("silence is retrieved and emitted", func(t *testing.T) {
+ httpCtx := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(silenceJSON)),
+ },
+ },
+ }
+
+ metadataCtx := &contexts.MetadataContext{}
+ executionCtx := &contexts.ExecutionStateContext{}
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "silence": "abc123",
+ },
+ HTTP: httpCtx,
+ Integration: &contexts.IntegrationContext{Configuration: map[string]any{
+ "baseURL": "https://prometheus.example.com",
+ "authType": AuthTypeNone,
+ }},
+ Metadata: metadataCtx,
+ ExecutionState: executionCtx,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, executionCtx.Finished)
+ assert.True(t, executionCtx.Passed)
+ assert.Equal(t, "prometheus.silence", executionCtx.Type)
+ require.Len(t, executionCtx.Payloads, 1)
+
+ wrappedPayload := executionCtx.Payloads[0].(map[string]any)
+ payload := wrappedPayload["data"].(map[string]any)
+ assert.Equal(t, "abc123", payload["silenceID"])
+ assert.Equal(t, "active", payload["status"])
+ assert.Equal(t, "SuperPlane", payload["createdBy"])
+ assert.Equal(t, "Maintenance window", payload["comment"])
+ assert.Equal(t, "2026-02-12T16:30:00Z", payload["startsAt"])
+ assert.Equal(t, "2026-02-12T17:30:00Z", payload["endsAt"])
+
+ metadata := metadataCtx.Metadata.(GetSilenceNodeMetadata)
+ assert.Equal(t, "abc123", metadata.SilenceID)
+ })
+
+ t.Run("API error returns error", func(t *testing.T) {
+ httpCtx := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusNotFound,
+ Body: io.NopCloser(strings.NewReader(`{"error":"silence not found"}`)),
+ },
+ },
+ }
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "silence": "notexist",
+ },
+ HTTP: httpCtx,
+ Integration: &contexts.IntegrationContext{Configuration: map[string]any{
+ "baseURL": "https://prometheus.example.com",
+ "authType": AuthTypeNone,
+ }},
+ Metadata: &contexts.MetadataContext{},
+ ExecutionState: &contexts.ExecutionStateContext{},
+ })
+
+ require.ErrorContains(t, err, "failed to get silence")
+ })
+}
diff --git a/pkg/integrations/prometheus/prometheus.go b/pkg/integrations/prometheus/prometheus.go
index 8d93d0740a..969b505a30 100644
--- a/pkg/integrations/prometheus/prometheus.go
+++ b/pkg/integrations/prometheus/prometheus.go
@@ -153,6 +153,9 @@ func (p *Prometheus) Components() []core.Component {
&GetAlert{},
&CreateSilence{},
&ExpireSilence{},
+ &GetSilence{},
+ &Query{},
+ &QueryRange{},
}
}
diff --git a/pkg/integrations/prometheus/query.go b/pkg/integrations/prometheus/query.go
new file mode 100644
index 0000000000..28bacdd620
--- /dev/null
+++ b/pkg/integrations/prometheus/query.go
@@ -0,0 +1,144 @@
+package prometheus
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+)
+
+type Query struct{}
+
+type QueryConfiguration struct {
+ Query string `json:"query" mapstructure:"query"`
+}
+
+type QueryNodeMetadata struct {
+ Query string `json:"query"`
+}
+
+func (c *Query) Name() string {
+ return "prometheus.query"
+}
+
+func (c *Query) Label() string {
+ return "Query"
+}
+
+func (c *Query) Description() string {
+ return "Execute a PromQL instant query"
+}
+
+func (c *Query) Documentation() string {
+ return `The Query component executes an instant PromQL query against Prometheus (` + "`GET /api/v1/query`" + `).
+
+## Configuration
+
+- **Query**: Required PromQL expression to evaluate (supports expressions). Example: ` + "`up`" + `
+
+## Output
+
+Emits one ` + "`prometheus.query`" + ` payload with the result type and results.`
+}
+
+func (c *Query) Icon() string {
+ return "prometheus"
+}
+
+func (c *Query) Color() string {
+ return "gray"
+}
+
+func (c *Query) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *Query) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "query",
+ Label: "Query",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Placeholder: "up",
+ Description: "PromQL expression to evaluate",
+ },
+ }
+}
+
+func (c *Query) Setup(ctx core.SetupContext) error {
+ config := QueryConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+ config = sanitizeQueryConfiguration(config)
+
+ if config.Query == "" {
+ return fmt.Errorf("query is required")
+ }
+
+ return nil
+}
+
+func (c *Query) Execute(ctx core.ExecutionContext) error {
+ config := QueryConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+ config = sanitizeQueryConfiguration(config)
+
+ client, err := NewClient(ctx.HTTP, ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("failed to create Prometheus client: %w", err)
+ }
+
+ data, err := client.Query(config.Query)
+ if err != nil {
+ return fmt.Errorf("failed to execute query: %w", err)
+ }
+
+ ctx.Metadata.Set(QueryNodeMetadata{Query: config.Query})
+
+ payload := map[string]any{
+ "resultType": data["resultType"],
+ "result": data["result"],
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "prometheus.query",
+ []any{payload},
+ )
+}
+
+func (c *Query) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *Query) HandleWebhook(ctx core.WebhookRequestContext) (int, error) {
+ return 200, nil
+}
+
+func (c *Query) Actions() []core.Action {
+ return []core.Action{}
+}
+
+func (c *Query) HandleAction(ctx core.ActionContext) error {
+ return nil
+}
+
+func (c *Query) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *Query) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
+
+func sanitizeQueryConfiguration(config QueryConfiguration) QueryConfiguration {
+ config.Query = strings.TrimSpace(config.Query)
+ return config
+}
diff --git a/pkg/integrations/prometheus/query_range.go b/pkg/integrations/prometheus/query_range.go
new file mode 100644
index 0000000000..1efd5b0248
--- /dev/null
+++ b/pkg/integrations/prometheus/query_range.go
@@ -0,0 +1,192 @@
+package prometheus
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+)
+
+type QueryRange struct{}
+
+type QueryRangeConfiguration struct {
+ Query string `json:"query" mapstructure:"query"`
+ Start string `json:"start" mapstructure:"start"`
+ End string `json:"end" mapstructure:"end"`
+ Step string `json:"step" mapstructure:"step"`
+}
+
+type QueryRangeNodeMetadata struct {
+ Query string `json:"query"`
+}
+
+func (c *QueryRange) Name() string {
+ return "prometheus.queryRange"
+}
+
+func (c *QueryRange) Label() string {
+ return "Query Range"
+}
+
+func (c *QueryRange) Description() string {
+ return "Execute a PromQL range query"
+}
+
+func (c *QueryRange) Documentation() string {
+ return `The Query Range component executes a range PromQL query against Prometheus (` + "`GET /api/v1/query_range`" + `).
+
+## Configuration
+
+- **Query**: Required PromQL expression to evaluate (supports expressions). Example: ` + "`up`" + `
+- **Start**: Required start timestamp in RFC3339 or Unix format (supports expressions). Example: ` + "`2026-01-01T00:00:00Z`" + `
+- **End**: Required end timestamp in RFC3339 or Unix format (supports expressions). Example: ` + "`2026-01-02T00:00:00Z`" + `
+- **Step**: Required query resolution step (e.g. ` + "`15s`" + `, ` + "`1m`" + `)
+
+## Output
+
+Emits one ` + "`prometheus.queryRange`" + ` payload with the result type and results.`
+}
+
+func (c *QueryRange) Icon() string {
+ return "prometheus"
+}
+
+func (c *QueryRange) Color() string {
+ return "gray"
+}
+
+func (c *QueryRange) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *QueryRange) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "query",
+ Label: "Query",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Placeholder: "up",
+ Description: "PromQL expression to evaluate",
+ },
+ {
+ Name: "start",
+ Label: "Start",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Placeholder: "2026-01-01T00:00:00Z",
+ Default: "2026-01-01T00:00:00Z",
+ Description: "Start timestamp (RFC3339 or Unix)",
+ },
+ {
+ Name: "end",
+ Label: "End",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Placeholder: "2026-01-02T00:00:00Z",
+ Default: "2026-01-02T00:00:00Z",
+ Description: "End timestamp (RFC3339 or Unix)",
+ },
+ {
+ Name: "step",
+ Label: "Step",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Placeholder: "15s",
+ Default: "15s",
+ Description: "Query resolution step (e.g. 15s, 1m)",
+ },
+ }
+}
+
+func (c *QueryRange) Setup(ctx core.SetupContext) error {
+ config := QueryRangeConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+ config = sanitizeQueryRangeConfiguration(config)
+
+ if config.Query == "" {
+ return fmt.Errorf("query is required")
+ }
+
+ if config.Start == "" {
+ return fmt.Errorf("start is required")
+ }
+
+ if config.End == "" {
+ return fmt.Errorf("end is required")
+ }
+
+ if config.Step == "" {
+ return fmt.Errorf("step is required")
+ }
+
+ return nil
+}
+
+func (c *QueryRange) Execute(ctx core.ExecutionContext) error {
+ config := QueryRangeConfiguration{}
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+ config = sanitizeQueryRangeConfiguration(config)
+
+ client, err := NewClient(ctx.HTTP, ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("failed to create Prometheus client: %w", err)
+ }
+
+ data, err := client.QueryRange(config.Query, config.Start, config.End, config.Step)
+ if err != nil {
+ return fmt.Errorf("failed to execute query range: %w", err)
+ }
+
+ ctx.Metadata.Set(QueryRangeNodeMetadata{Query: config.Query})
+
+ payload := map[string]any{
+ "resultType": data["resultType"],
+ "result": data["result"],
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "prometheus.queryRange",
+ []any{payload},
+ )
+}
+
+func (c *QueryRange) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *QueryRange) HandleWebhook(ctx core.WebhookRequestContext) (int, error) {
+ return 200, nil
+}
+
+func (c *QueryRange) Actions() []core.Action {
+ return []core.Action{}
+}
+
+func (c *QueryRange) HandleAction(ctx core.ActionContext) error {
+ return nil
+}
+
+func (c *QueryRange) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *QueryRange) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
+
+func sanitizeQueryRangeConfiguration(config QueryRangeConfiguration) QueryRangeConfiguration {
+ config.Query = strings.TrimSpace(config.Query)
+ config.Start = strings.TrimSpace(config.Start)
+ config.End = strings.TrimSpace(config.End)
+ config.Step = strings.TrimSpace(config.Step)
+ return config
+}
diff --git a/pkg/integrations/prometheus/query_range_test.go b/pkg/integrations/prometheus/query_range_test.go
new file mode 100644
index 0000000000..c00bfb4fb2
--- /dev/null
+++ b/pkg/integrations/prometheus/query_range_test.go
@@ -0,0 +1,155 @@
+package prometheus
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__QueryRange__Setup(t *testing.T) {
+ component := &QueryRange{}
+
+ t.Run("query is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "",
+ "start": "2026-01-01T00:00:00Z",
+ "end": "2026-01-02T00:00:00Z",
+ "step": "15s",
+ },
+ })
+ require.ErrorContains(t, err, "query is required")
+ })
+
+ t.Run("start is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "up",
+ "start": "",
+ "end": "2026-01-02T00:00:00Z",
+ "step": "15s",
+ },
+ })
+ require.ErrorContains(t, err, "start is required")
+ })
+
+ t.Run("end is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "up",
+ "start": "2026-01-01T00:00:00Z",
+ "end": "",
+ "step": "15s",
+ },
+ })
+ require.ErrorContains(t, err, "end is required")
+ })
+
+ t.Run("step is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "up",
+ "start": "2026-01-01T00:00:00Z",
+ "end": "2026-01-02T00:00:00Z",
+ "step": "",
+ },
+ })
+ require.ErrorContains(t, err, "step is required")
+ })
+
+ t.Run("valid setup", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "up",
+ "start": "2026-01-01T00:00:00Z",
+ "end": "2026-01-02T00:00:00Z",
+ "step": "15s",
+ },
+ })
+ require.NoError(t, err)
+ })
+}
+
+func Test__QueryRange__Execute(t *testing.T) {
+ component := &QueryRange{}
+
+ queryRangeResponseJSON := `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"up","job":"prometheus"},"values":[[1708000000,"1"],[1708000015,"1"]]}]}}`
+
+ t.Run("query range result is emitted", func(t *testing.T) {
+ httpCtx := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(queryRangeResponseJSON)),
+ },
+ },
+ }
+
+ metadataCtx := &contexts.MetadataContext{}
+ executionCtx := &contexts.ExecutionStateContext{}
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "query": "up",
+ "start": "2026-01-01T00:00:00Z",
+ "end": "2026-01-02T00:00:00Z",
+ "step": "15s",
+ },
+ HTTP: httpCtx,
+ Integration: &contexts.IntegrationContext{Configuration: map[string]any{
+ "baseURL": "https://prometheus.example.com",
+ "authType": AuthTypeNone,
+ }},
+ Metadata: metadataCtx,
+ ExecutionState: executionCtx,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, executionCtx.Finished)
+ assert.True(t, executionCtx.Passed)
+ assert.Equal(t, "prometheus.queryRange", executionCtx.Type)
+ require.Len(t, executionCtx.Payloads, 1)
+
+ wrappedPayload := executionCtx.Payloads[0].(map[string]any)
+ payload := wrappedPayload["data"].(map[string]any)
+ assert.Equal(t, "matrix", payload["resultType"])
+ assert.NotNil(t, payload["result"])
+
+ metadata := metadataCtx.Metadata.(QueryRangeNodeMetadata)
+ assert.Equal(t, "up", metadata.Query)
+ })
+
+ t.Run("API error returns error", func(t *testing.T) {
+ httpCtx := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusBadRequest,
+ Body: io.NopCloser(strings.NewReader(`{"status":"error","errorType":"bad_data","error":"invalid query"}`)),
+ },
+ },
+ }
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "query": "invalid{",
+ "start": "2026-01-01T00:00:00Z",
+ "end": "2026-01-02T00:00:00Z",
+ "step": "15s",
+ },
+ HTTP: httpCtx,
+ Integration: &contexts.IntegrationContext{Configuration: map[string]any{
+ "baseURL": "https://prometheus.example.com",
+ "authType": AuthTypeNone,
+ }},
+ Metadata: &contexts.MetadataContext{},
+ ExecutionState: &contexts.ExecutionStateContext{},
+ })
+
+ require.ErrorContains(t, err, "failed to execute query range")
+ })
+}
diff --git a/pkg/integrations/prometheus/query_test.go b/pkg/integrations/prometheus/query_test.go
new file mode 100644
index 0000000000..059c423e48
--- /dev/null
+++ b/pkg/integrations/prometheus/query_test.go
@@ -0,0 +1,107 @@
+package prometheus
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__Query__Setup(t *testing.T) {
+ component := &Query{}
+
+ t.Run("query is required", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "",
+ },
+ })
+ require.ErrorContains(t, err, "query is required")
+ })
+
+ t.Run("valid setup", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{
+ "query": "up",
+ },
+ })
+ require.NoError(t, err)
+ })
+}
+
+func Test__Query__Execute(t *testing.T) {
+ component := &Query{}
+
+ queryResponseJSON := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"up","job":"prometheus"},"value":[1708000000,"1"]}]}}`
+
+ t.Run("query result is emitted", func(t *testing.T) {
+ httpCtx := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(queryResponseJSON)),
+ },
+ },
+ }
+
+ metadataCtx := &contexts.MetadataContext{}
+ executionCtx := &contexts.ExecutionStateContext{}
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "query": "up",
+ },
+ HTTP: httpCtx,
+ Integration: &contexts.IntegrationContext{Configuration: map[string]any{
+ "baseURL": "https://prometheus.example.com",
+ "authType": AuthTypeNone,
+ }},
+ Metadata: metadataCtx,
+ ExecutionState: executionCtx,
+ })
+
+ require.NoError(t, err)
+ assert.True(t, executionCtx.Finished)
+ assert.True(t, executionCtx.Passed)
+ assert.Equal(t, "prometheus.query", executionCtx.Type)
+ require.Len(t, executionCtx.Payloads, 1)
+
+ wrappedPayload := executionCtx.Payloads[0].(map[string]any)
+ payload := wrappedPayload["data"].(map[string]any)
+ assert.Equal(t, "vector", payload["resultType"])
+ assert.NotNil(t, payload["result"])
+
+ metadata := metadataCtx.Metadata.(QueryNodeMetadata)
+ assert.Equal(t, "up", metadata.Query)
+ })
+
+ t.Run("API error returns error", func(t *testing.T) {
+ httpCtx := &contexts.HTTPContext{
+ Responses: []*http.Response{
+ {
+ StatusCode: http.StatusBadRequest,
+ Body: io.NopCloser(strings.NewReader(`{"status":"error","errorType":"bad_data","error":"invalid query"}`)),
+ },
+ },
+ }
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{
+ "query": "invalid{",
+ },
+ HTTP: httpCtx,
+ Integration: &contexts.IntegrationContext{Configuration: map[string]any{
+ "baseURL": "https://prometheus.example.com",
+ "authType": AuthTypeNone,
+ }},
+ Metadata: &contexts.MetadataContext{},
+ ExecutionState: &contexts.ExecutionStateContext{},
+ })
+
+ require.ErrorContains(t, err, "failed to execute query")
+ })
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 40728a2079..006e4d5487 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -413,19 +413,19 @@ var DefaultMaxHTTPResponseBytes int64 = 8 * 1024 * 1024
* - Localhost variations
*/
var defaultBlockedHTTPHosts = []string{
- "metadata.google.internal",
- "metadata.goog",
- "metadata.azure.com",
- "169.254.169.254",
- "fd00:ec2::254",
- "kubernetes.default",
- "kubernetes.default.svc",
- "kubernetes.default.svc.cluster.local",
- "localhost",
- "127.0.0.1",
- "::1",
- "0.0.0.0",
- "::",
+ // "metadata.google.internal",
+ // "metadata.goog",
+ // "metadata.azure.com",
+ // "169.254.169.254",
+ // "fd00:ec2::254",
+ // "kubernetes.default",
+ // "kubernetes.default.svc",
+ // "kubernetes.default.svc.cluster.local",
+ // "localhost",
+ // "127.0.0.1",
+ // "::1",
+ // "0.0.0.0",
+ // "::",
}
func getBlockedHTTPHosts() []string {
@@ -438,15 +438,15 @@ func getBlockedHTTPHosts() []string {
}
var defaultBlockedPrivateIPRanges = []string{
- "0.0.0.0/8",
- "10.0.0.0/8",
- "172.16.0.0/12",
- "192.168.0.0/16",
- "127.0.0.0/8",
- "169.254.0.0/16",
- "::1/128",
- "fc00::/7",
- "fe80::/10",
+ // "0.0.0.0/8",
+ // "10.0.0.0/8",
+ // "172.16.0.0/12",
+ // "192.168.0.0/16",
+ // "127.0.0.0/8",
+ // "169.254.0.0/16",
+ // "::1/128",
+ // "fc00::/7",
+ // "fe80::/10",
}
func getPrivateIPRanges() []string {
diff --git a/web_src/src/pages/workflowv2/mappers/prometheus/get_silence.ts b/web_src/src/pages/workflowv2/mappers/prometheus/get_silence.ts
new file mode 100644
index 0000000000..a4d98d85e7
--- /dev/null
+++ b/web_src/src/pages/workflowv2/mappers/prometheus/get_silence.ts
@@ -0,0 +1,132 @@
+import { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import { MetadataItem } from "@/ui/metadataList";
+import { getBackgroundColorClass, getColorClass } from "@/utils/colors";
+import { formatTimeAgo } from "@/utils/date";
+import prometheusIcon from "@/assets/icons/integrations/prometheus.svg";
+import { getState, getStateMap, getTriggerRenderer } from "..";
+import {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../types";
+import { GetSilenceConfiguration, GetSilenceNodeMetadata, PrometheusSilencePayload } from "./types";
+
+export const getSilenceMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ return buildGetSilenceProps(context.nodes, context.node, context.componentDefinition, context.lastExecutions);
+ },
+
+ subtitle(context: SubtitleContext): string {
+ if (!context.execution.createdAt) {
+ return "";
+ }
+
+ return formatTimeAgo(new Date(context.execution.createdAt));
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ const details: Record = {};
+
+ if (context.execution.createdAt) {
+ details["Retrieved At"] = new Date(context.execution.createdAt).toLocaleString();
+ }
+
+ if (!outputs || !outputs.default || outputs.default.length === 0) {
+ return details;
+ }
+
+ const silence = outputs.default[0].data as PrometheusSilencePayload;
+
+ if (silence?.silenceID) {
+ details["Silence ID"] = silence.silenceID;
+ }
+
+ if (silence?.status) {
+ details["Status"] = silence.status;
+ }
+
+ if (silence?.matchers && silence.matchers.length > 0) {
+ details["Matchers"] = silence.matchers
+ .map((m) => {
+ const operator = m.isEqual === false ? "!=" : "=";
+ const suffix = m.isRegex ? " (regex)" : "";
+ return `${m.name}${operator}"${m.value}"${suffix}`;
+ })
+ .join(", ");
+ }
+
+ if (silence?.startsAt) {
+ details["Starts At"] = new Date(silence.startsAt).toLocaleString();
+ }
+
+ if (silence?.endsAt && silence.endsAt !== "0001-01-01T00:00:00Z") {
+ details["Ends At"] = new Date(silence.endsAt).toLocaleString();
+ }
+
+ if (silence?.createdBy) {
+ details["Created By"] = silence.createdBy;
+ }
+
+ if (silence?.comment) {
+ details["Comment"] = silence.comment;
+ }
+
+ return details;
+ },
+};
+
+function buildGetSilenceProps(
+ nodes: NodeInfo[],
+ node: NodeInfo,
+ componentDefinition: { name: string; label: string; color: string },
+ lastExecutions: ExecutionInfo[],
+): ComponentBaseProps {
+ const lastExecution = lastExecutions.length > 0 ? lastExecutions[0] : null;
+ const componentName = componentDefinition.name || node.componentName || "unknown";
+
+ return {
+ iconSrc: prometheusIcon,
+ iconColor: getColorClass(componentDefinition.color),
+ collapsedBackground: getBackgroundColorClass(componentDefinition.color),
+ collapsed: node.isCollapsed,
+ title: node.name || componentDefinition.label || "Unnamed component",
+ eventSections: lastExecution ? buildEventSections(nodes, lastExecution, componentName) : undefined,
+ metadata: getMetadata(node),
+ includeEmptyState: !lastExecution,
+ eventStateMap: getStateMap(componentName),
+ };
+}
+
+function getMetadata(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const nodeMetadata = node.metadata as GetSilenceNodeMetadata | undefined;
+ const configuration = node.configuration as GetSilenceConfiguration | undefined;
+
+ const silenceID = nodeMetadata?.silenceID || configuration?.silence || configuration?.silenceID;
+ if (silenceID) {
+ metadata.push({ icon: "bell-off", label: silenceID });
+ }
+
+ return metadata.slice(0, 3);
+}
+
+function buildEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((n) => n.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName!);
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: execution.createdAt ? formatTimeAgo(new Date(execution.createdAt)) : "",
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}
diff --git a/web_src/src/pages/workflowv2/mappers/prometheus/index.ts b/web_src/src/pages/workflowv2/mappers/prometheus/index.ts
index 6c10ab3265..22a1bb2363 100644
--- a/web_src/src/pages/workflowv2/mappers/prometheus/index.ts
+++ b/web_src/src/pages/workflowv2/mappers/prometheus/index.ts
@@ -2,6 +2,9 @@ import { ComponentBaseMapper, CustomFieldRenderer, EventStateRegistry, TriggerRe
import { getAlertMapper } from "./get_alert";
import { createSilenceMapper } from "./create_silence";
import { expireSilenceMapper } from "./expire_silence";
+import { getSilenceMapper } from "./get_silence";
+import { queryMapper } from "./query";
+import { queryRangeMapper } from "./query_range";
import { onAlertCustomFieldRenderer, onAlertTriggerRenderer } from "./on_alert";
import { buildActionStateRegistry } from "../utils";
@@ -9,6 +12,9 @@ export const componentMappers: Record = {
getAlert: getAlertMapper,
createSilence: createSilenceMapper,
expireSilence: expireSilenceMapper,
+ getSilence: getSilenceMapper,
+ query: queryMapper,
+ queryRange: queryRangeMapper,
};
export const triggerRenderers: Record = {
@@ -23,4 +29,7 @@ export const eventStateRegistry: Record = {
getAlert: buildActionStateRegistry("retrieved"),
createSilence: buildActionStateRegistry("created"),
expireSilence: buildActionStateRegistry("expired"),
+ getSilence: buildActionStateRegistry("retrieved"),
+ query: buildActionStateRegistry("queried"),
+ queryRange: buildActionStateRegistry("queried"),
};
diff --git a/web_src/src/pages/workflowv2/mappers/prometheus/query.ts b/web_src/src/pages/workflowv2/mappers/prometheus/query.ts
new file mode 100644
index 0000000000..e93b5fc739
--- /dev/null
+++ b/web_src/src/pages/workflowv2/mappers/prometheus/query.ts
@@ -0,0 +1,111 @@
+import { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import { MetadataItem } from "@/ui/metadataList";
+import { getBackgroundColorClass, getColorClass } from "@/utils/colors";
+import { formatTimeAgo } from "@/utils/date";
+import prometheusIcon from "@/assets/icons/integrations/prometheus.svg";
+import { getState, getStateMap, getTriggerRenderer } from "..";
+import {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../types";
+import { PrometheusQueryPayload, QueryConfiguration, QueryNodeMetadata } from "./types";
+
+export const queryMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ return buildQueryProps(context.nodes, context.node, context.componentDefinition, context.lastExecutions);
+ },
+
+ subtitle(context: SubtitleContext): string {
+ if (!context.execution.createdAt) {
+ return "";
+ }
+
+ return formatTimeAgo(new Date(context.execution.createdAt));
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ const details: Record = {};
+
+ if (context.execution.createdAt) {
+ details["Executed At"] = new Date(context.execution.createdAt).toLocaleString();
+ }
+
+ if (!outputs || !outputs.default || outputs.default.length === 0) {
+ return details;
+ }
+
+ const queryResult = outputs.default[0].data as PrometheusQueryPayload;
+
+ const configuration = context.node?.configuration as QueryConfiguration | undefined;
+ if (configuration?.query) {
+ details["Query"] = configuration.query;
+ }
+
+ if (queryResult?.resultType) {
+ details["Result Type"] = queryResult.resultType;
+ }
+
+ if (queryResult?.result !== undefined) {
+ details["Results"] = String(Array.isArray(queryResult.result) ? queryResult.result.length : 0);
+ }
+
+ return details;
+ },
+};
+
+function buildQueryProps(
+ nodes: NodeInfo[],
+ node: NodeInfo,
+ componentDefinition: { name: string; label: string; color: string },
+ lastExecutions: ExecutionInfo[],
+): ComponentBaseProps {
+ const lastExecution = lastExecutions.length > 0 ? lastExecutions[0] : null;
+ const componentName = componentDefinition.name || node.componentName || "unknown";
+
+ return {
+ iconSrc: prometheusIcon,
+ iconColor: getColorClass(componentDefinition.color),
+ collapsedBackground: getBackgroundColorClass(componentDefinition.color),
+ collapsed: node.isCollapsed,
+ title: node.name || componentDefinition.label || "Unnamed component",
+ eventSections: lastExecution ? buildEventSections(nodes, lastExecution, componentName) : undefined,
+ metadata: getMetadata(node),
+ includeEmptyState: !lastExecution,
+ eventStateMap: getStateMap(componentName),
+ };
+}
+
+function getMetadata(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const nodeMetadata = node.metadata as QueryNodeMetadata | undefined;
+ const configuration = node.configuration as QueryConfiguration | undefined;
+
+ const query = nodeMetadata?.query || configuration?.query;
+ if (query) {
+ metadata.push({ icon: "search", label: query });
+ }
+
+ return metadata.slice(0, 3);
+}
+
+function buildEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((n) => n.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName!);
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: execution.createdAt ? formatTimeAgo(new Date(execution.createdAt)) : "",
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}
diff --git a/web_src/src/pages/workflowv2/mappers/prometheus/query_range.ts b/web_src/src/pages/workflowv2/mappers/prometheus/query_range.ts
new file mode 100644
index 0000000000..b823183393
--- /dev/null
+++ b/web_src/src/pages/workflowv2/mappers/prometheus/query_range.ts
@@ -0,0 +1,131 @@
+import { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import { MetadataItem } from "@/ui/metadataList";
+import { getBackgroundColorClass, getColorClass } from "@/utils/colors";
+import { formatTimeAgo } from "@/utils/date";
+import prometheusIcon from "@/assets/icons/integrations/prometheus.svg";
+import { getState, getStateMap, getTriggerRenderer } from "..";
+import {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../types";
+import { PrometheusQueryPayload, QueryRangeConfiguration, QueryRangeNodeMetadata } from "./types";
+
+export const queryRangeMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ return buildQueryRangeProps(context.nodes, context.node, context.componentDefinition, context.lastExecutions);
+ },
+
+ subtitle(context: SubtitleContext): string {
+ if (!context.execution.createdAt) {
+ return "";
+ }
+
+ return formatTimeAgo(new Date(context.execution.createdAt));
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ const details: Record = {};
+
+ if (context.execution.createdAt) {
+ details["Executed At"] = new Date(context.execution.createdAt).toLocaleString();
+ }
+
+ if (!outputs || !outputs.default || outputs.default.length === 0) {
+ return details;
+ }
+
+ const queryResult = outputs.default[0].data as PrometheusQueryPayload;
+
+ const configuration = context.node?.configuration as QueryRangeConfiguration | undefined;
+ if (configuration?.query) {
+ details["Query"] = configuration.query;
+ }
+
+ if (configuration?.start) {
+ details["Start"] = configuration.start;
+ }
+
+ if (configuration?.end) {
+ details["End"] = configuration.end;
+ }
+
+ if (configuration?.step) {
+ details["Step"] = configuration.step;
+ }
+
+ if (queryResult?.resultType) {
+ details["Result Type"] = queryResult.resultType;
+ }
+
+ if (queryResult?.result !== undefined) {
+ details["Results"] = String(Array.isArray(queryResult.result) ? queryResult.result.length : 0);
+ }
+
+ return details;
+ },
+};
+
+function buildQueryRangeProps(
+ nodes: NodeInfo[],
+ node: NodeInfo,
+ componentDefinition: { name: string; label: string; color: string },
+ lastExecutions: ExecutionInfo[],
+): ComponentBaseProps {
+ const lastExecution = lastExecutions.length > 0 ? lastExecutions[0] : null;
+ const componentName = componentDefinition.name || node.componentName || "unknown";
+
+ return {
+ iconSrc: prometheusIcon,
+ iconColor: getColorClass(componentDefinition.color),
+ collapsedBackground: getBackgroundColorClass(componentDefinition.color),
+ collapsed: node.isCollapsed,
+ title: node.name || componentDefinition.label || "Unnamed component",
+ eventSections: lastExecution ? buildEventSections(nodes, lastExecution, componentName) : undefined,
+ metadata: getMetadata(node),
+ includeEmptyState: !lastExecution,
+ eventStateMap: getStateMap(componentName),
+ };
+}
+
+function getMetadata(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const nodeMetadata = node.metadata as QueryRangeNodeMetadata | undefined;
+ const configuration = node.configuration as QueryRangeConfiguration | undefined;
+
+ const query = nodeMetadata?.query || configuration?.query;
+ if (query) {
+ metadata.push({ icon: "search", label: query });
+ }
+
+ if (configuration?.start) {
+ metadata.push({ icon: "clock", label: `Start: ${configuration.start}` });
+ }
+
+ if (configuration?.end) {
+ metadata.push({ icon: "clock", label: `End: ${configuration.end}` });
+ }
+
+ return metadata.slice(0, 3);
+}
+
+function buildEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((n) => n.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName!);
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: execution.createdAt ? formatTimeAgo(new Date(execution.createdAt)) : "",
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}
diff --git a/web_src/src/pages/workflowv2/mappers/prometheus/types.ts b/web_src/src/pages/workflowv2/mappers/prometheus/types.ts
index 8a7e928717..e144e5cf10 100644
--- a/web_src/src/pages/workflowv2/mappers/prometheus/types.ts
+++ b/web_src/src/pages/workflowv2/mappers/prometheus/types.ts
@@ -66,3 +66,36 @@ export interface ExpireSilenceConfiguration {
export interface ExpireSilenceNodeMetadata {
silenceID?: string;
}
+
+export interface GetSilenceConfiguration {
+ silence?: string;
+ silenceID?: string;
+}
+
+export interface GetSilenceNodeMetadata {
+ silenceID?: string;
+}
+
+export interface QueryConfiguration {
+ query?: string;
+}
+
+export interface QueryNodeMetadata {
+ query?: string;
+}
+
+export interface QueryRangeConfiguration {
+ query?: string;
+ start?: string;
+ end?: string;
+ step?: string;
+}
+
+export interface QueryRangeNodeMetadata {
+ query?: string;
+}
+
+export interface PrometheusQueryPayload {
+ resultType?: string;
+ result?: any[];
+}