Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -329,12 +329,18 @@ github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc/go.
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vertica/vertica-sql-go v1.3.5 h1:IrfH2WIgzZ45yDHyjVFrXU2LuKNIjF5Nwi90a6cfgUI=
github.com/vertica/vertica-sql-go v1.3.5/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37 h1:kUXMT/fM/DpDT66WQgRUf3I8VOAWjypkMf52W5PChwA=
github.com/ydb-platform/ydb-go-genproto v0.0.0-20260128080146-c4ed16b24b37/go.mod h1:Er+FePu1dNUieD+XTMDduGpQuCPssK5Q4BjF+IIXJ3I=
github.com/ydb-platform/ydb-go-sdk/v3 v3.127.0 h1:OfHS9ZkZgCy6y/CJ9N8123DXrgaY2BPxWsQiQ8e3wC8=
github.com/ydb-platform/ydb-go-sdk/v3 v3.127.0/go.mod h1:stS1mQYjbJvwwYaYzKyFY9eMiuVXWWXQA6T+SpOLg9c=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
Expand Down
3 changes: 3 additions & 0 deletions packages/engine/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ require (
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/redis/go-redis/v9 v9.20.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
Expand All @@ -141,12 +142,14 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
Expand Down
6 changes: 6 additions & 0 deletions packages/engine/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0=
github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
Expand Down Expand Up @@ -340,6 +342,8 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
Expand All @@ -362,6 +366,8 @@ go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
Expand Down
25 changes: 2 additions & 23 deletions packages/engine/internal/connector/airtable.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (c *AirtableListConnector) apiURL(path string) string {
}

func (c *AirtableListConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
token, err := extractAirtableToken(params)
token, err := extractBearerToken(params)
if err != nil {
return nil, fmt.Errorf("airtable/list: %w", err)
}
Expand Down Expand Up @@ -115,7 +115,7 @@ func (c *AirtableCreateRecordConnector) apiURL(path string) string {
}

func (c *AirtableCreateRecordConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
token, err := extractAirtableToken(params)
token, err := extractBearerToken(params)
if err != nil {
return nil, fmt.Errorf("airtable/create_record: %w", err)
}
Expand Down Expand Up @@ -179,24 +179,3 @@ func (c *AirtableCreateRecordConnector) Execute(ctx context.Context, params map[
}, nil
}

func extractAirtableToken(params map[string]any) (string, error) {
raw, ok := params["_credential"]
if !ok || raw == nil {
return "", fmt.Errorf("credential is required")
}
delete(params, "_credential")

var token string
switch cred := raw.(type) {
case map[string]string:
token = cred["token"]
case map[string]any:
token, _ = cred["token"].(string)
default:
return "", fmt.Errorf("credential is required")
}
if token == "" {
return "", fmt.Errorf("credential must contain a 'token' field")
}
return token, nil
}
25 changes: 2 additions & 23 deletions packages/engine/internal/connector/asana.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (c *AsanaCreateTaskConnector) apiURL(path string) string {
}

func (c *AsanaCreateTaskConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
token, err := extractAsanaToken(params)
token, err := extractBearerToken(params)
if err != nil {
return nil, fmt.Errorf("asana/create_task: %w", err)
}
Expand Down Expand Up @@ -120,7 +120,7 @@ func (c *AsanaSearchConnector) apiURL(path string) string {
}

func (c *AsanaSearchConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
token, err := extractAsanaToken(params)
token, err := extractBearerToken(params)
if err != nil {
return nil, fmt.Errorf("asana/search: %w", err)
}
Expand Down Expand Up @@ -188,24 +188,3 @@ func (c *AsanaSearchConnector) Execute(ctx context.Context, params map[string]an
}, nil
}

func extractAsanaToken(params map[string]any) (string, error) {
raw, ok := params["_credential"]
if !ok || raw == nil {
return "", fmt.Errorf("credential is required")
}
delete(params, "_credential")

var token string
switch cred := raw.(type) {
case map[string]string:
token = cred["token"]
case map[string]any:
token, _ = cred["token"].(string)
default:
return "", fmt.Errorf("credential is required")
}
if token == "" {
return "", fmt.Errorf("credential must contain a 'token' field")
}
return token, nil
}
11 changes: 11 additions & 0 deletions packages/engine/internal/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ func NewRegistry() *Registry {
r.Register("twilio/call", &TwilioCallConnector{})
r.Register("asana/create_task", &AsanaCreateTaskConnector{})
r.Register("asana/search", &AsanaSearchConnector{})
r.Register("discord/send", &DiscordSendConnector{})
r.Register("discord/embed", &DiscordEmbedConnector{})
r.Register("elasticsearch/search", &ElasticsearchSearchConnector{})
r.Register("elasticsearch/index", &ElasticsearchIndexConnector{})
r.Register("datadog/submit_event", &DatadogSubmitEventConnector{})
r.Register("datadog/query_metrics", &DatadogQueryMetricsConnector{})
r.Register("redis/get", &RedisGetConnector{})
r.Register("redis/set", &RedisSetConnector{})
r.Register("redis/publish", &RedisPublishConnector{})
r.Register("mongodb/find", &MongoFindConnector{})
r.Register("mongodb/aggregate", &MongoAggregateConnector{})
return r
}

Expand Down
203 changes: 203 additions & 0 deletions packages/engine/internal/connector/datadog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package connector

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)

const datadogBaseURL = "https://api.datadoghq.com"

// DatadogSubmitEventConnector submits an event to the Datadog event stream.
type DatadogSubmitEventConnector struct {
Client *http.Client
baseURL string // override for testing
}

func (c *DatadogSubmitEventConnector) apiURL(path string) string {
base := c.baseURL
if base == "" {
base = datadogBaseURL
}
return base + path
}

func (c *DatadogSubmitEventConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
apiKey, appKey, err := extractDatadogCredential(params)
if err != nil {
return nil, fmt.Errorf("datadog/submit_event: %w", err)
}

title, _ := params["title"].(string)
if title == "" {
return nil, fmt.Errorf("datadog/submit_event: title is required")
}
text, _ := params["text"].(string)
if text == "" {
return nil, fmt.Errorf("datadog/submit_event: text is required")
}

event := map[string]any{
"title": title,
"text": text,
}
if alertType, ok := params["alert_type"].(string); ok && alertType != "" {
event["alert_type"] = alertType
}
if priority, ok := params["priority"].(string); ok && priority != "" {
event["priority"] = priority
}
if tags, ok := params["tags"].([]any); ok && len(tags) > 0 {
event["tags"] = tags
}
if host, ok := params["host"].(string); ok && host != "" {
event["host"] = host
}

reqJSON, err := json.Marshal(event)
if err != nil {
return nil, fmt.Errorf("datadog/submit_event: marshaling request: %w", err)
}

req, err := http.NewRequestWithContext(ctx, "POST", c.apiURL("/api/v1/events"), bytes.NewReader(reqJSON))
if err != nil {
return nil, fmt.Errorf("datadog/submit_event: creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("DD-API-KEY", apiKey)
if appKey != "" {
req.Header.Set("DD-APPLICATION-KEY", appKey)
}

resp, err := httpClient(c.Client).Do(req)
if err != nil {
return nil, fmt.Errorf("datadog/submit_event: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(io.LimitReader(resp.Body, DefaultMaxResponseBytes))
if err != nil {
return nil, fmt.Errorf("datadog/submit_event: reading response: %w", err)
}

if resp.StatusCode != http.StatusAccepted {
return nil, fmt.Errorf("datadog/submit_event: Datadog API returned %d: %s", resp.StatusCode, truncate(string(respBody), 500))
}

var result struct {
Status string `json:"status"`
Event struct {
ID int64 `json:"id"`
} `json:"event"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("datadog/submit_event: parsing response: %w", err)
}

return map[string]any{
"status": result.Status,
"event_id": result.Event.ID,
}, nil
}

// DatadogQueryMetricsConnector queries time-series metrics from Datadog.
type DatadogQueryMetricsConnector struct {
Client *http.Client
baseURL string // override for testing
}

func (c *DatadogQueryMetricsConnector) apiURL(path string) string {
base := c.baseURL
if base == "" {
base = datadogBaseURL
}
return base + path
}

func (c *DatadogQueryMetricsConnector) Execute(ctx context.Context, params map[string]any) (map[string]any, error) {
apiKey, appKey, err := extractDatadogCredential(params)
if err != nil {
return nil, fmt.Errorf("datadog/query_metrics: %w", err)
}

query, _ := params["query"].(string)
if query == "" {
return nil, fmt.Errorf("datadog/query_metrics: query is required")
}
from, fromOK := extractInt(params["from"])
to, toOK := extractInt(params["to"])
if !fromOK || !toOK {
return nil, fmt.Errorf("datadog/query_metrics: from and to (Unix timestamps) are required")
}

q := url.Values{}
q.Set("query", query)
q.Set("from", fmt.Sprintf("%d", from))
q.Set("to", fmt.Sprintf("%d", to))

req, err := http.NewRequestWithContext(ctx, "GET", c.apiURL("/api/v1/query?"+q.Encode()), nil)
if err != nil {
return nil, fmt.Errorf("datadog/query_metrics: creating request: %w", err)
}
req.Header.Set("DD-API-KEY", apiKey)
if appKey != "" {
req.Header.Set("DD-APPLICATION-KEY", appKey)
}

resp, err := httpClient(c.Client).Do(req)
if err != nil {
return nil, fmt.Errorf("datadog/query_metrics: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(io.LimitReader(resp.Body, DefaultMaxResponseBytes))
if err != nil {
return nil, fmt.Errorf("datadog/query_metrics: reading response: %w", err)
}

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("datadog/query_metrics: Datadog API returned %d: %s", resp.StatusCode, truncate(string(respBody), 500))
}

var result struct {
Status string `json:"status"`
Series []any `json:"series"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("datadog/query_metrics: parsing response: %w", err)
}

return map[string]any{
"status": result.Status,
"series": result.Series,
"count": len(result.Series),
}, nil
}

// extractDatadogCredential pulls api_key and optional app_key from _credential.
func extractDatadogCredential(params map[string]any) (apiKey, appKey string, err error) {
raw, ok := params["_credential"]
if !ok || raw == nil {
return "", "", fmt.Errorf("credential is required")
}
delete(params, "_credential")

switch cred := raw.(type) {
case map[string]string:
apiKey = cred["api_key"]
appKey = cred["app_key"]
case map[string]any:
apiKey, _ = cred["api_key"].(string)
appKey, _ = cred["app_key"].(string)
default:
return "", "", fmt.Errorf("credential is required")
}
if apiKey == "" {
return "", "", fmt.Errorf("credential must contain an 'api_key' field")
}
return apiKey, appKey, nil
}
Loading
Loading