Skip to content
Merged
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
3 changes: 2 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ HADO 本体リポジトリは、root に `go.mod` を置く単一 Go module と
│ ├── standard # Readiness Standard の読み込みと検証
│ ├── coverage # coverage artifact の adapter と正規化 metric
│ ├── gate # gate 評価と最終 decision
│ └── integration # 外部システム連携の契約・基盤(予定: observability / Datadog など)
│ ├── integration # 外部 SaaS API の薄いラッパ。manifest や gate ID を import しない
│ │ └── datadog # Datadog Monitor 一覧など。公式 datadog-api-client-go を利用
├── modules
│ ├── github
Expand Down
12 changes: 11 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ module github.com/keyskey/hado
go 1.22

require (
github.com/DataDog/datadog-api-client-go/v2 v2.54.0
golang.org/x/term v0.27.0
gopkg.in/yaml.v3 v3.0.1
)

require golang.org/x/sys v0.28.0 // indirect
require (
github.com/DataDog/zstd v1.5.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
30 changes: 30 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
github.com/DataDog/datadog-api-client-go/v2 v2.54.0 h1:bLSwX1D7JA7hAHxpo8Aa2+d8F2wzD8sNOJszL89yGyU=
github.com/DataDog/datadog-api-client-go/v2 v2.54.0/go.mod h1:d3tOEgUd2kfsr9uuHQdY+nXrWp4uikgTgVCPdKNK30U=
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
154 changes: 154 additions & 0 deletions internal/integration/datadog/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package datadog

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"

dd "github.com/DataDog/datadog-api-client-go/v2/api/datadog"
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV1"
)

const defaultSite = "datadoghq.com"

// Config holds API authentication and site. Keys are never read from disk by this package.
type Config struct {
APIKey string
AppKey string
Site string
// APIBaseURL, if non-empty, is a full base URL (e.g. httptest server) used to override
// the API host via Configuration.Scheme/Host (for tests).
APIBaseURL string
}

// Client lists Datadog monitors via the official datadog-api-client-go.
type Client struct {
cfg Config
site string
monitors *datadogV1.MonitorsApi
}

// NewClient returns a Client with the given config and HTTP client.
func NewClient(cfg Config, httpClient *http.Client) (*Client, error) {
if strings.TrimSpace(cfg.APIKey) == "" {
return nil, fmt.Errorf("datadog: api key is empty")
}
if strings.TrimSpace(cfg.AppKey) == "" {
return nil, fmt.Errorf("datadog: application key is empty")
}
site := strings.TrimSpace(cfg.Site)
if site == "" {
site = defaultSite
}

configuration := dd.NewConfiguration()
if httpClient != nil {
configuration.HTTPClient = httpClient
}
if base := strings.TrimSpace(cfg.APIBaseURL); base != "" {
parsed, err := url.Parse(base)
if err != nil {
return nil, fmt.Errorf("datadog: parse api base url: %w", err)
}
if parsed.Host != "" {
if parsed.Scheme != "" {
configuration.Scheme = parsed.Scheme
} else {
configuration.Scheme = "http"
}
configuration.Host = parsed.Host
}
}

apiClient := dd.NewAPIClient(configuration)
return &Client{
cfg: Config{
APIKey: strings.TrimSpace(cfg.APIKey),
AppKey: strings.TrimSpace(cfg.AppKey),
Site: site,
APIBaseURL: strings.TrimSpace(cfg.APIBaseURL),
},
site: site,
monitors: datadogV1.NewMonitorsApi(apiClient),
}, nil
}

// NewClientFromEnv builds a client using DD_API_KEY, DD_APP_KEY, and optional DD_SITE.
func NewClientFromEnv(httpClient *http.Client) (*Client, error) {
return NewClient(Config{
APIKey: os.Getenv("DD_API_KEY"),
AppKey: os.Getenv("DD_APP_KEY"),
Site: os.Getenv("DD_SITE"),
}, httpClient)
}

// ListMonitors returns all monitors; call filters the result. Pagination is handled by the generated client.
func (c *Client) ListMonitors(ctx context.Context) ([]Monitor, error) {
ctx = c.apiContext(ctx)
ch, cancel := c.monitors.ListMonitorsWithPagination(ctx)
defer cancel()

var v1mons []datadogV1.Monitor
for result := range ch {
if result.Error != nil {
return nil, formatListMonitorsError(result.Error)
}
v1mons = append(v1mons, result.Item)
}

out := make([]Monitor, 0, len(v1mons))
for _, m := range v1mons {
if m.Id == nil {
continue
}
name := ""
if m.Name != nil {
name = *m.Name
}
tags := m.Tags
if tags == nil {
tags = []string{}
}
out = append(out, Monitor{
ID: *m.Id,
Name: name,
Tags: tags,
})
}
return out, nil
}

// Site returns the normalized Datadog site suffix (for example datadoghq.com).
func (c *Client) Site() string {
return c.cfg.Site
}

func (c *Client) apiContext(ctx context.Context) context.Context {
ctx = context.WithValue(ctx, dd.ContextAPIKeys, map[string]dd.APIKey{
"apiKeyAuth": {Key: c.cfg.APIKey},
"appKeyAuth": {Key: c.cfg.AppKey},
})
// Test hosts override URL host via Configuration; default servers still need DD_SITE.
if strings.TrimSpace(c.cfg.APIBaseURL) == "" {
ctx = context.WithValue(ctx, dd.ContextServerVariables, map[string]string{
"site": c.site,
})
}
return ctx
}

func formatListMonitorsError(err error) error {
var openAPIErr dd.GenericOpenAPIError
if errors.As(err, &openAPIErr) {
msg := strings.TrimSpace(string(openAPIErr.Body()))
if msg == "" {
return fmt.Errorf("datadog: list monitors: %s", openAPIErr.Error())
}
return fmt.Errorf("datadog: list monitors: %s: %s", openAPIErr.Error(), msg)
}
return fmt.Errorf("datadog: list monitors: %w", err)
}
56 changes: 56 additions & 0 deletions internal/integration/datadog/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package datadog

import (
"context"
"net/http"
"net/http/httptest"
"testing"
)

func TestListMonitors_decode(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/monitor" {
t.Fatalf("path = %s", r.URL.Path)
}
if r.Header.Get("DD-API-KEY") != "api" || r.Header.Get("DD-APPLICATION-KEY") != "app" {
t.Fatalf("missing auth headers")
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[{"id":42,"name":"test","type":"query alert","query":"","tags":["env:prod","hado:foo"]}]`))
}))
defer ts.Close()

c, err := NewClient(Config{
APIKey: "api",
AppKey: "app",
Site: "datadoghq.com",
APIBaseURL: ts.URL,
}, ts.Client())
if err != nil {
t.Fatal(err)
}

mons, err := c.ListMonitors(context.Background())
if err != nil {
t.Fatal(err)
}
if len(mons) != 1 || mons[0].ID != 42 || mons[0].Name != "test" {
t.Fatalf("got %+v", mons)
}
}

func TestMonitorAppURL_defaultSite(t *testing.T) {
got := MonitorAppURL("", 7)
want := "https://app.datadoghq.com/monitors/7"
if got != want {
t.Fatalf("got %q want %q", got, want)
}
}

func TestMonitorAppURL_eu(t *testing.T) {
got := MonitorAppURL("datadoghq.eu", 9)
want := "https://app.datadoghq.eu/monitors/9"
if got != want {
t.Fatalf("got %q want %q", got, want)
}
}
3 changes: 3 additions & 0 deletions internal/integration/datadog/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package datadog wraps the official Datadog HTTP API client for monitor listing and URL helpers.
// It does not import manifest types or gate IDs; callers own discovery rules and manifest updates.
package datadog
8 changes: 8 additions & 0 deletions internal/integration/datadog/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package datadog

// Monitor is a subset of Datadog monitor API fields needed for discovery and linking.
type Monitor struct {
ID int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
}
13 changes: 13 additions & 0 deletions internal/integration/datadog/url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package datadog

import "fmt"

// MonitorAppURL returns the web UI URL for a monitor id. site is the Datadog site suffix
// (for example datadoghq.com, datadoghq.eu) matching DD_SITE.
func MonitorAppURL(site string, monitorID int64) string {
s := site
if s == "" {
s = defaultSite
}
return fmt.Sprintf("https://app.%s/monitors/%d", s, monitorID)
}
5 changes: 3 additions & 2 deletions internal/integration/doc.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package integration defines contracts and shared plumbing for external
// system integrations (for example observability readiness and Datadog).
// Package integration holds thin HTTP/API clients for external systems.
// Vendor-specific code lives in subpackages (for example datadog).
// HADO manifest schemas and gate IDs must not be imported from here.
package integration
Loading