diff --git a/cmd/mavend/ecosystem.go b/cmd/mavend/ecosystem.go index 317142a..21287a5 100644 --- a/cmd/mavend/ecosystem.go +++ b/cmd/mavend/ecosystem.go @@ -17,6 +17,30 @@ import ( "github.com/kami/maven/internal/config" ) +// ecosystemCorrelationKey carries a per-call correlation ID through context +// so every ecosystem client (Nexus, Praxis, Hexis) tags its request with the +// same ID, letting a single Maven-initiated action be traced end to end. +type ecosystemCorrelationKey struct{} + +func withCorrelationID(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, ecosystemCorrelationKey{}, id) +} + +func correlationIDFromCtx(ctx context.Context) string { + id, _ := ctx.Value(ecosystemCorrelationKey{}).(string) + return id +} + +// setEcosystemHeaders stamps the version and correlation headers common to +// every outgoing ecosystem request. +func setEcosystemHeaders(req *http.Request, ctx context.Context, versionHeader string) { + req.Header.Set("Content-Type", "application/json") + req.Header.Set(versionHeader, "v1") + if id := correlationIDFromCtx(ctx); id != "" { + req.Header.Set("X-Correlation-ID", id) + } +} + type nexusClient struct { baseURL string httpClient *http.Client @@ -83,7 +107,7 @@ func (c *nexusClient) Resolve(ctx context.Context, query string, types []string) if err != nil { return nil, fmt.Errorf("create request: %w", err) } - req.Header.Set("Content-Type", "application/json") + setEcosystemHeaders(req, ctx, "X-Nexus-Version") resp, err := c.httpClient.Do(req) if err != nil { @@ -108,6 +132,7 @@ func (c *nexusClient) Health(ctx context.Context) error { if err != nil { return err } + setEcosystemHeaders(req, ctx, "X-Nexus-Version") resp, err := c.httpClient.Do(req) if err != nil { return err @@ -140,6 +165,7 @@ func (c *praxisClient) getJSON(ctx context.Context, path string, out any) error if err != nil { return err } + setEcosystemHeaders(req, ctx, "X-Praxis-Version") resp, err := c.httpClient.Do(req) if err != nil { return err @@ -191,7 +217,7 @@ func (c *praxisClient) postItemAction(ctx context.Context, path, itemID string) if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/json") + setEcosystemHeaders(req, ctx, "X-Praxis-Version") resp, err := c.httpClient.Do(req) if err != nil { return nil, err @@ -232,7 +258,7 @@ func (c *praxisClient) Pin(ctx context.Context, itemID string, pinned bool) (*pr if err != nil { return nil, err } - req.Header.Set("Content-Type", "application/json") + setEcosystemHeaders(req, ctx, "X-Praxis-Version") resp, err := c.httpClient.Do(req) if err != nil { return nil, err @@ -308,6 +334,9 @@ func (w *ecosystemWiring) resolveEntityReference(ctx context.Context, text strin if w == nil || w.nexus == nil { return "", "", nil, nil } + if correlationIDFromCtx(ctx) == "" { + ctx = withCorrelationID(ctx, newCorrelationID()) + } result, err := w.nexus.Resolve(ctx, text, entityTypes) if err != nil { log.Printf("ecosystem: nexus resolve error: %v", err) @@ -353,6 +382,8 @@ func (w *ecosystemWiring) executeCapability(ctx context.Context, capabilityID, t return "", fmt.Errorf("hexis not configured") } correlationID = newCorrelationID() + ctx = withCorrelationID(ctx, correlationID) + ctx = hexisclient.WithCorrelationID(ctx, correlationID) req := hexisclient.ExecuteRequest{ CapabilityID: capabilityID, TargetEntityID: targetEntityID, diff --git a/vendor/github.com/kami/hexis/pkg/client/client.go b/vendor/github.com/kami/hexis/pkg/client/client.go index dbfc404..ec06047 100644 --- a/vendor/github.com/kami/hexis/pkg/client/client.go +++ b/vendor/github.com/kami/hexis/pkg/client/client.go @@ -10,6 +10,35 @@ import ( "time" ) +// APIVersion is sent as X-Hexis-Version on every request so the server can +// negotiate/reject unsupported client versions. +const APIVersion = "v1" + +type correlationKey struct{} +type causationKey struct{} + +// WithCorrelationID returns a context that carries a correlation ID to be +// sent as X-Correlation-ID on every Hexis request made with it. +func WithCorrelationID(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, correlationKey{}, id) +} + +// WithCausationID returns a context that carries a causation ID to be sent +// as X-Causation-ID on every Hexis request made with it. +func WithCausationID(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, causationKey{}, id) +} + +func correlationIDFrom(ctx context.Context) string { + id, _ := ctx.Value(correlationKey{}).(string) + return id +} + +func causationIDFrom(ctx context.Context) string { + id, _ := ctx.Value(causationKey{}).(string) + return id +} + type Client struct { baseURL string httpClient *http.Client @@ -37,6 +66,13 @@ func (c *Client) do(ctx context.Context, method, path string, body, result any) return fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Hexis-Version", APIVersion) + if id := correlationIDFrom(ctx); id != "" { + req.Header.Set("X-Correlation-ID", id) + } + if id := causationIDFrom(ctx); id != "" { + req.Header.Set("X-Causation-ID", id) + } resp, err := c.httpClient.Do(req) if err != nil { @@ -83,6 +119,7 @@ type Execution struct { Error string `json:"error,omitempty"` RequestedBy map[string]string `json:"requested_by,omitempty"` CorrelationID string `json:"correlation_id,omitempty"` + CausationID string `json:"causation_id,omitempty"` IdempotencyKey string `json:"idempotency_key,omitempty"` } @@ -94,6 +131,7 @@ type ExecuteRequest struct { Origin map[string]string `json:"origin,omitempty"` IdempotencyKey string `json:"idempotency_key,omitempty"` CorrelationID string `json:"correlation_id,omitempty"` + CausationID string `json:"causation_id,omitempty"` } type CreateCapabilityRequest struct {