 9bdcbe0447
			
		
	
	9bdcbe0447
	
	
	
		
			
			Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			327 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package integration
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/config"
 | |
| )
 | |
| 
 | |
| // SlurpClient handles HTTP communication with SLURP endpoints
 | |
| type SlurpClient struct {
 | |
| 	baseURL    string
 | |
| 	apiKey     string
 | |
| 	timeout    time.Duration
 | |
| 	retryCount int
 | |
| 	retryDelay time.Duration
 | |
| 	httpClient *http.Client
 | |
| }
 | |
| 
 | |
| // SlurpEvent represents a SLURP event structure
 | |
| type SlurpEvent struct {
 | |
| 	EventType   string                 `json:"event_type"`
 | |
| 	Path        string                 `json:"path"`
 | |
| 	Content     string                 `json:"content"`
 | |
| 	Severity    int                    `json:"severity"`
 | |
| 	CreatedBy   string                 `json:"created_by"`
 | |
| 	Metadata    map[string]interface{} `json:"metadata"`
 | |
| 	Tags        []string               `json:"tags,omitempty"`
 | |
| 	Timestamp   time.Time              `json:"timestamp"`
 | |
| }
 | |
| 
 | |
| // EventResponse represents the response from SLURP API
 | |
| type EventResponse struct {
 | |
| 	Success   bool   `json:"success"`
 | |
| 	EventID   string `json:"event_id,omitempty"`
 | |
| 	Message   string `json:"message,omitempty"`
 | |
| 	Error     string `json:"error,omitempty"`
 | |
| 	Timestamp time.Time `json:"timestamp"`
 | |
| }
 | |
| 
 | |
| // BatchEventRequest represents a batch of events to be sent to SLURP
 | |
| type BatchEventRequest struct {
 | |
| 	Events []SlurpEvent `json:"events"`
 | |
| 	Source string       `json:"source"`
 | |
| }
 | |
| 
 | |
| // BatchEventResponse represents the response for batch event creation
 | |
| type BatchEventResponse struct {
 | |
| 	Success      bool              `json:"success"`
 | |
| 	ProcessedCount int             `json:"processed_count"`
 | |
| 	FailedCount    int             `json:"failed_count"`
 | |
| 	EventIDs     []string          `json:"event_ids,omitempty"`
 | |
| 	Errors       []string          `json:"errors,omitempty"`
 | |
| 	Message      string            `json:"message,omitempty"`
 | |
| 	Timestamp    time.Time         `json:"timestamp"`
 | |
| }
 | |
| 
 | |
| // HealthResponse represents SLURP service health status
 | |
| type HealthResponse struct {
 | |
| 	Status      string    `json:"status"`
 | |
| 	Version     string    `json:"version,omitempty"`
 | |
| 	Uptime      string    `json:"uptime,omitempty"`
 | |
| 	Timestamp   time.Time `json:"timestamp"`
 | |
| }
 | |
| 
 | |
| // NewSlurpClient creates a new SLURP API client
 | |
| func NewSlurpClient(config config.SlurpConfig) *SlurpClient {
 | |
| 	return &SlurpClient{
 | |
| 		baseURL:    strings.TrimSuffix(config.BaseURL, "/"),
 | |
| 		apiKey:     config.APIKey,
 | |
| 		timeout:    config.Timeout,
 | |
| 		retryCount: config.RetryCount,
 | |
| 		retryDelay: config.RetryDelay,
 | |
| 		httpClient: &http.Client{
 | |
| 			Timeout: config.Timeout,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // CreateEvent sends a single event to SLURP
 | |
| func (c *SlurpClient) CreateEvent(ctx context.Context, event SlurpEvent) (*EventResponse, error) {
 | |
| 	url := fmt.Sprintf("%s/api/events", c.baseURL)
 | |
| 	
 | |
| 	eventData, err := json.Marshal(event)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to marshal event: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	var lastErr error
 | |
| 	for attempt := 0; attempt <= c.retryCount; attempt++ {
 | |
| 		if attempt > 0 {
 | |
| 			select {
 | |
| 			case <-ctx.Done():
 | |
| 				return nil, ctx.Err()
 | |
| 			case <-time.After(c.retryDelay):
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(eventData))
 | |
| 		if err != nil {
 | |
| 			lastErr = fmt.Errorf("failed to create request: %w", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		c.setHeaders(req)
 | |
| 		
 | |
| 		resp, err := c.httpClient.Do(req)
 | |
| 		if err != nil {
 | |
| 			lastErr = fmt.Errorf("failed to send request: %w", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		defer resp.Body.Close()
 | |
| 		
 | |
| 		if c.isRetryableStatus(resp.StatusCode) && attempt < c.retryCount {
 | |
| 			lastErr = fmt.Errorf("retryable error: HTTP %d", resp.StatusCode)
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		body, err := io.ReadAll(resp.Body)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to read response body: %w", err)
 | |
| 		}
 | |
| 		
 | |
| 		var eventResp EventResponse
 | |
| 		if err := json.Unmarshal(body, &eventResp); err != nil {
 | |
| 			return nil, fmt.Errorf("failed to unmarshal response: %w", err)
 | |
| 		}
 | |
| 		
 | |
| 		if resp.StatusCode >= 400 {
 | |
| 			return &eventResp, fmt.Errorf("SLURP API error (HTTP %d): %s", resp.StatusCode, eventResp.Error)
 | |
| 		}
 | |
| 		
 | |
| 		return &eventResp, nil
 | |
| 	}
 | |
| 	
 | |
| 	return nil, fmt.Errorf("failed after %d attempts: %w", c.retryCount+1, lastErr)
 | |
| }
 | |
| 
 | |
| // CreateEventsBatch sends multiple events to SLURP in a single request
 | |
| func (c *SlurpClient) CreateEventsBatch(ctx context.Context, events []SlurpEvent) (*BatchEventResponse, error) {
 | |
| 	url := fmt.Sprintf("%s/api/events/batch", c.baseURL)
 | |
| 	
 | |
| 	batchRequest := BatchEventRequest{
 | |
| 		Events: events,
 | |
| 		Source: "CHORUS-hmmm-integration",
 | |
| 	}
 | |
| 	
 | |
| 	batchData, err := json.Marshal(batchRequest)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to marshal batch request: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	var lastErr error
 | |
| 	for attempt := 0; attempt <= c.retryCount; attempt++ {
 | |
| 		if attempt > 0 {
 | |
| 			select {
 | |
| 			case <-ctx.Done():
 | |
| 				return nil, ctx.Err()
 | |
| 			case <-time.After(c.retryDelay):
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(batchData))
 | |
| 		if err != nil {
 | |
| 			lastErr = fmt.Errorf("failed to create batch request: %w", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		c.setHeaders(req)
 | |
| 		
 | |
| 		resp, err := c.httpClient.Do(req)
 | |
| 		if err != nil {
 | |
| 			lastErr = fmt.Errorf("failed to send batch request: %w", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		defer resp.Body.Close()
 | |
| 		
 | |
| 		if c.isRetryableStatus(resp.StatusCode) && attempt < c.retryCount {
 | |
| 			lastErr = fmt.Errorf("retryable error: HTTP %d", resp.StatusCode)
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		body, err := io.ReadAll(resp.Body)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to read batch response body: %w", err)
 | |
| 		}
 | |
| 		
 | |
| 		var batchResp BatchEventResponse
 | |
| 		if err := json.Unmarshal(body, &batchResp); err != nil {
 | |
| 			return nil, fmt.Errorf("failed to unmarshal batch response: %w", err)
 | |
| 		}
 | |
| 		
 | |
| 		if resp.StatusCode >= 400 {
 | |
| 			return &batchResp, fmt.Errorf("SLURP batch API error (HTTP %d): %s", resp.StatusCode, batchResp.Message)
 | |
| 		}
 | |
| 		
 | |
| 		return &batchResp, nil
 | |
| 	}
 | |
| 	
 | |
| 	return nil, fmt.Errorf("batch failed after %d attempts: %w", c.retryCount+1, lastErr)
 | |
| }
 | |
| 
 | |
| // GetHealth checks SLURP service health
 | |
| func (c *SlurpClient) GetHealth(ctx context.Context) (*HealthResponse, error) {
 | |
| 	url := fmt.Sprintf("%s/api/health", c.baseURL)
 | |
| 	
 | |
| 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create health request: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	c.setHeaders(req)
 | |
| 	
 | |
| 	resp, err := c.httpClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to send health request: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	body, err := io.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to read health response: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	var healthResp HealthResponse
 | |
| 	if err := json.Unmarshal(body, &healthResp); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to unmarshal health response: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	if resp.StatusCode >= 400 {
 | |
| 		return &healthResp, fmt.Errorf("SLURP health check failed (HTTP %d)", resp.StatusCode)
 | |
| 	}
 | |
| 	
 | |
| 	return &healthResp, nil
 | |
| }
 | |
| 
 | |
| // QueryEvents retrieves events from SLURP based on filters
 | |
| func (c *SlurpClient) QueryEvents(ctx context.Context, filters map[string]string) ([]SlurpEvent, error) {
 | |
| 	baseURL := fmt.Sprintf("%s/api/events", c.baseURL)
 | |
| 	
 | |
| 	// Build query parameters
 | |
| 	params := url.Values{}
 | |
| 	for key, value := range filters {
 | |
| 		params.Add(key, value)
 | |
| 	}
 | |
| 	
 | |
| 	queryURL := baseURL
 | |
| 	if len(params) > 0 {
 | |
| 		queryURL = fmt.Sprintf("%s?%s", baseURL, params.Encode())
 | |
| 	}
 | |
| 	
 | |
| 	req, err := http.NewRequestWithContext(ctx, "GET", queryURL, nil)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create query request: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	c.setHeaders(req)
 | |
| 	
 | |
| 	resp, err := c.httpClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to send query request: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	body, err := io.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to read query response: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	var events []SlurpEvent
 | |
| 	if err := json.Unmarshal(body, &events); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to unmarshal events: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	if resp.StatusCode >= 400 {
 | |
| 		return nil, fmt.Errorf("SLURP query failed (HTTP %d)", resp.StatusCode)
 | |
| 	}
 | |
| 	
 | |
| 	return events, nil
 | |
| }
 | |
| 
 | |
| // setHeaders sets common HTTP headers for SLURP API requests
 | |
| func (c *SlurpClient) setHeaders(req *http.Request) {
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	req.Header.Set("Accept", "application/json")
 | |
| 	req.Header.Set("User-Agent", "Bzzz-HMMM-Integration/1.0")
 | |
| 	
 | |
| 	if c.apiKey != "" {
 | |
| 		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // isRetryableStatus determines if an HTTP status code is retryable
 | |
| func (c *SlurpClient) isRetryableStatus(statusCode int) bool {
 | |
| 	switch statusCode {
 | |
| 	case http.StatusTooManyRequests,     // 429
 | |
| 		 http.StatusInternalServerError,  // 500
 | |
| 		 http.StatusBadGateway,          // 502
 | |
| 		 http.StatusServiceUnavailable,  // 503
 | |
| 		 http.StatusGatewayTimeout:      // 504
 | |
| 		return true
 | |
| 	default:
 | |
| 		return false
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Close cleans up the client resources
 | |
| func (c *SlurpClient) Close() error {
 | |
| 	// HTTP client doesn't need explicit cleanup, but we can implement
 | |
| 	// connection pooling cleanup if needed in the future
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ValidateConnection tests the connection to SLURP
 | |
| func (c *SlurpClient) ValidateConnection(ctx context.Context) error {
 | |
| 	_, err := c.GetHealth(ctx)
 | |
| 	return err
 | |
| } |