This comprehensive refactoring addresses critical architectural issues: IMPORT CYCLE RESOLUTION: • pkg/crypto ↔ pkg/slurp/roles: Created pkg/security/access_levels.go • pkg/ucxl → pkg/dht: Created pkg/storage/interfaces.go • pkg/slurp/leader → pkg/election → pkg/slurp/storage: Moved types to pkg/election/interfaces.go MODULE PATH MIGRATION: • Changed from github.com/anthonyrawlins/bzzz to chorus.services/bzzz • Updated all import statements across 115+ files • Maintains compatibility while removing personal GitHub account dependency TYPE SYSTEM IMPROVEMENTS: • Resolved duplicate type declarations in crypto package • Added missing type definitions (RoleStatus, TimeRestrictions, KeyStatus, KeyRotationResult) • Proper interface segregation to prevent future cycles ARCHITECTURAL BENEFITS: • Build now progresses past structural issues to normal dependency resolution • Cleaner separation of concerns between packages • Eliminates circular dependencies that prevented compilation • Establishes foundation for scalable codebase growth 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
327 lines
9.0 KiB
Go
327 lines
9.0 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"chorus.services/bzzz/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: "bzzz-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
|
|
} |