1053 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1053 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package ucxi
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/ucxl"
 | |
| )
 | |
| 
 | |
| // Server represents a UCXI HTTP server for UCXL operations
 | |
| type Server struct {
 | |
| 	// HTTP server configuration
 | |
| 	server   *http.Server
 | |
| 	port     int
 | |
| 	basePath string
 | |
| 
 | |
| 	// Address resolution
 | |
| 	resolver AddressResolver
 | |
| 	
 | |
| 	// Content storage
 | |
| 	storage ContentStorage
 | |
| 	
 | |
| 	// Temporal navigation
 | |
| 	navigators map[string]*ucxl.TemporalNavigator
 | |
| 	navMutex   sync.RWMutex
 | |
| 
 | |
| 	// Server state
 | |
| 	running bool
 | |
| 	ctx     context.Context
 | |
| 	cancel  context.CancelFunc
 | |
| 
 | |
| 	// Middleware and logging
 | |
| 	logger Logger
 | |
| 
 | |
| 	// Response building
 | |
| 	responseBuilder *ucxl.ResponseBuilder
 | |
| }
 | |
| 
 | |
| // AddressResolver interface for resolving UCXL addresses to actual content
 | |
| type AddressResolver interface {
 | |
| 	Resolve(ctx context.Context, addr *ucxl.Address) (*ResolvedContent, error)
 | |
| 	Announce(ctx context.Context, addr *ucxl.Address, content *Content) error
 | |
| 	Discover(ctx context.Context, pattern *ucxl.Address) ([]*ResolvedContent, error)
 | |
| }
 | |
| 
 | |
| // ContentStorage interface for storing and retrieving content
 | |
| type ContentStorage interface {
 | |
| 	Store(ctx context.Context, key string, content *Content) error
 | |
| 	Retrieve(ctx context.Context, key string) (*Content, error)
 | |
| 	Delete(ctx context.Context, key string) error
 | |
| 	List(ctx context.Context, prefix string) ([]string, error)
 | |
| }
 | |
| 
 | |
| // Logger interface for server logging
 | |
| type Logger interface {
 | |
| 	Info(msg string, fields ...interface{})
 | |
| 	Warn(msg string, fields ...interface{})
 | |
| 	Error(msg string, fields ...interface{})
 | |
| 	Debug(msg string, fields ...interface{})
 | |
| }
 | |
| 
 | |
| // Content represents content stored at a UCXL address
 | |
| type Content struct {
 | |
| 	Data        []byte            `json:"data"`
 | |
| 	ContentType string            `json:"content_type"`
 | |
| 	Metadata    map[string]string `json:"metadata"`
 | |
| 	Version     int               `json:"version"`
 | |
| 	CreatedAt   time.Time         `json:"created_at"`
 | |
| 	UpdatedAt   time.Time         `json:"updated_at"`
 | |
| 	Author      string            `json:"author,omitempty"`
 | |
| 	Checksum    string            `json:"checksum,omitempty"`
 | |
| }
 | |
| 
 | |
| // ResolvedContent represents content resolved from a UCXL address
 | |
| type ResolvedContent struct {
 | |
| 	Address   *ucxl.Address `json:"address"`
 | |
| 	Content   *Content      `json:"content"`
 | |
| 	Source    string        `json:"source"`    // Source node/peer ID
 | |
| 	Resolved  time.Time     `json:"resolved"`  // Resolution timestamp
 | |
| 	TTL       time.Duration `json:"ttl"`       // Time to live for caching
 | |
| }
 | |
| 
 | |
| // Deprecated: Use ucxl.UCXLResponse and ucxl.UCXLError instead
 | |
| // Legacy Response type kept for backward compatibility
 | |
| type Response struct {
 | |
| 	Success   bool        `json:"success"`
 | |
| 	Data      interface{} `json:"data,omitempty"`
 | |
| 	Error     string      `json:"error,omitempty"`
 | |
| 	Timestamp time.Time   `json:"timestamp"`
 | |
| 	RequestID string      `json:"request_id,omitempty"`
 | |
| 	Version   string      `json:"version"`
 | |
| }
 | |
| 
 | |
| // Deprecated: Use ucxl.UCXLError instead
 | |
| // Legacy ErrorResponse type kept for backward compatibility
 | |
| type ErrorResponse struct {
 | |
| 	Code    int    `json:"code"`
 | |
| 	Message string `json:"message"`
 | |
| 	Details string `json:"details,omitempty"`
 | |
| }
 | |
| 
 | |
| // UCXLValidationError represents a structured UCXL validation error
 | |
| type UCXLValidationError struct {
 | |
| 	Code    string `json:"code"`
 | |
| 	Field   string `json:"field"`
 | |
| 	Message string `json:"message"`
 | |
| 	Address string `json:"address"`
 | |
| }
 | |
| 
 | |
| // ServerConfig holds server configuration
 | |
| type ServerConfig struct {
 | |
| 	Port     int             `json:"port"`
 | |
| 	BasePath string          `json:"base_path"`
 | |
| 	Resolver AddressResolver `json:"-"`
 | |
| 	Storage  ContentStorage  `json:"-"`
 | |
| 	Logger   Logger         `json:"-"`
 | |
| }
 | |
| 
 | |
| // NewServer creates a new UCXI server
 | |
| func NewServer(config ServerConfig) *Server {
 | |
| 	ctx, cancel := context.WithCancel(context.Background())
 | |
| 
 | |
| 	s := &Server{
 | |
| 		port:       config.Port,
 | |
| 		basePath:   strings.TrimSuffix(config.BasePath, "/"),
 | |
| 		resolver:   config.Resolver,
 | |
| 		storage:    config.Storage,
 | |
| 		logger:     config.Logger,
 | |
| 		navigators: make(map[string]*ucxl.TemporalNavigator),
 | |
| 		ctx:        ctx,
 | |
| 		cancel:     cancel,
 | |
| 	}
 | |
| 
 | |
| 	// Initialize response builder with server source
 | |
| 	s.responseBuilder = ucxl.NewResponseBuilder("", "ucxi-server")
 | |
| 
 | |
| 	return s
 | |
| }
 | |
| 
 | |
| // Start starts the UCXI HTTP server
 | |
| func (s *Server) Start() error {
 | |
| 	if s.running {
 | |
| 		return fmt.Errorf("server is already running")
 | |
| 	}
 | |
| 
 | |
| 	mux := http.NewServeMux()
 | |
| 
 | |
| 	// Register routes
 | |
| 	s.registerRoutes(mux)
 | |
| 
 | |
| 	s.server = &http.Server{
 | |
| 		Addr:         fmt.Sprintf(":%d", s.port),
 | |
| 		Handler:      s.withMiddleware(mux),
 | |
| 		ReadTimeout:  30 * time.Second,
 | |
| 		WriteTimeout: 30 * time.Second,
 | |
| 		IdleTimeout:  60 * time.Second,
 | |
| 	}
 | |
| 
 | |
| 	s.running = true
 | |
| 	s.logger.Info("Starting UCXI server", "port", s.port, "base_path", s.basePath)
 | |
| 
 | |
| 	return s.server.ListenAndServe()
 | |
| }
 | |
| 
 | |
| // Stop stops the UCXI HTTP server
 | |
| func (s *Server) Stop() error {
 | |
| 	if !s.running {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	s.logger.Info("Stopping UCXI server")
 | |
| 	s.cancel()
 | |
| 	s.running = false
 | |
| 
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 | |
| 	defer cancel()
 | |
| 
 | |
| 	return s.server.Shutdown(ctx)
 | |
| }
 | |
| 
 | |
| // registerRoutes registers all UCXI HTTP routes
 | |
| func (s *Server) registerRoutes(mux *http.ServeMux) {
 | |
| 	prefix := s.basePath + "/ucxi/v1"
 | |
| 
 | |
| 	// Content operations
 | |
| 	mux.HandleFunc(prefix+"/get", s.handleGet)
 | |
| 	mux.HandleFunc(prefix+"/put", s.handlePut)
 | |
| 	mux.HandleFunc(prefix+"/post", s.handlePost)
 | |
| 	mux.HandleFunc(prefix+"/delete", s.handleDelete)
 | |
| 
 | |
| 	// Discovery and announcement
 | |
| 	mux.HandleFunc(prefix+"/announce", s.handleAnnounce)
 | |
| 	mux.HandleFunc(prefix+"/discover", s.handleDiscover)
 | |
| 
 | |
| 	// Temporal navigation
 | |
| 	mux.HandleFunc(prefix+"/navigate", s.handleNavigate)
 | |
| 
 | |
| 	// Server status and health
 | |
| 	mux.HandleFunc(prefix+"/health", s.handleHealth)
 | |
| 	mux.HandleFunc(prefix+"/status", s.handleStatus)
 | |
| 
 | |
| 	// Role-based collaboration endpoints
 | |
| 	mux.HandleFunc(prefix+"/collaboration", s.handleCollaboration)
 | |
| }
 | |
| 
 | |
| // handleGet handles GET requests for retrieving content
 | |
| func (s *Server) handleGet(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodGet {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addressStr := r.URL.Query().Get("address")
 | |
| 	if addressStr == "" {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Missing address parameter", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addr, err := ucxl.Parse(addressStr)
 | |
| 	if err != nil {
 | |
| 		if validationErr, ok := err.(*ucxl.ValidationError); ok {
 | |
| 			s.writeUCXLValidationError(w, validationErr)
 | |
| 		} else {
 | |
| 			s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Resolve the address
 | |
| 	resolved, err := s.resolver.Resolve(r.Context(), addr)
 | |
| 	if err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusNotFound, "Failed to resolve address", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	s.writeSuccessResponse(w, resolved)
 | |
| }
 | |
| 
 | |
| // handlePut handles PUT requests for storing content
 | |
| func (s *Server) handlePut(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodPut {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addressStr := r.URL.Query().Get("address")
 | |
| 	if addressStr == "" {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Missing address parameter", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addr, err := ucxl.Parse(addressStr)
 | |
| 	if err != nil {
 | |
| 		if validationErr, ok := err.(*ucxl.ValidationError); ok {
 | |
| 			s.writeUCXLValidationError(w, validationErr)
 | |
| 		} else {
 | |
| 			s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Read content from request body
 | |
| 	body, err := io.ReadAll(r.Body)
 | |
| 	if err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Failed to read request body", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	content := &Content{
 | |
| 		Data:        body,
 | |
| 		ContentType: r.Header.Get("Content-Type"),
 | |
| 		Metadata:    make(map[string]string),
 | |
| 		CreatedAt:   time.Now(),
 | |
| 		UpdatedAt:   time.Now(),
 | |
| 		Author:      r.Header.Get("X-Author"),
 | |
| 	}
 | |
| 
 | |
| 	// Copy custom metadata from headers
 | |
| 	for key, values := range r.Header {
 | |
| 		if strings.HasPrefix(key, "X-Meta-") {
 | |
| 			metaKey := strings.TrimPrefix(key, "X-Meta-")
 | |
| 			if len(values) > 0 {
 | |
| 				content.Metadata[metaKey] = values[0]
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Store the content
 | |
| 	key := s.generateStorageKey(addr)
 | |
| 	if err := s.storage.Store(r.Context(), key, content); err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusInternalServerError, "Failed to store content", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Announce the content
 | |
| 	if err := s.resolver.Announce(r.Context(), addr, content); err != nil {
 | |
| 		s.logger.Warn("Failed to announce content", "error", err.Error(), "address", addr.String())
 | |
| 		// Don't fail the request if announcement fails
 | |
| 	}
 | |
| 
 | |
| 	response := map[string]interface{}{
 | |
| 		"address": addr.String(),
 | |
| 		"key":     key,
 | |
| 		"stored":  true,
 | |
| 	}
 | |
| 
 | |
| 	s.writeSuccessResponse(w, response)
 | |
| }
 | |
| 
 | |
| // handlePost handles POST requests for updating content
 | |
| func (s *Server) handlePost(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodPost {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// POST is similar to PUT but may have different semantics
 | |
| 	// For now, delegate to PUT handler
 | |
| 	s.handlePut(w, r)
 | |
| }
 | |
| 
 | |
| // handleDelete handles DELETE requests for removing content
 | |
| func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodDelete {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addressStr := r.URL.Query().Get("address")
 | |
| 	if addressStr == "" {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Missing address parameter", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addr, err := ucxl.Parse(addressStr)
 | |
| 	if err != nil {
 | |
| 		if validationErr, ok := err.(*ucxl.ValidationError); ok {
 | |
| 			s.writeUCXLValidationError(w, validationErr)
 | |
| 		} else {
 | |
| 			s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	key := s.generateStorageKey(addr)
 | |
| 	if err := s.storage.Delete(r.Context(), key); err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusInternalServerError, "Failed to delete content", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	response := map[string]interface{}{
 | |
| 		"address": addr.String(),
 | |
| 		"key":     key,
 | |
| 		"deleted": true,
 | |
| 	}
 | |
| 
 | |
| 	s.writeSuccessResponse(w, response)
 | |
| }
 | |
| 
 | |
| // handleAnnounce handles content announcement requests
 | |
| func (s *Server) handleAnnounce(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodPost {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var request struct {
 | |
| 		Address string  `json:"address"`
 | |
| 		Content Content `json:"content"`
 | |
| 	}
 | |
| 
 | |
| 	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Invalid JSON request", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addr, err := ucxl.Parse(request.Address)
 | |
| 	if err != nil {
 | |
| 		if validationErr, ok := err.(*ucxl.ValidationError); ok {
 | |
| 			s.writeUCXLValidationError(w, validationErr)
 | |
| 		} else {
 | |
| 			s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := s.resolver.Announce(r.Context(), addr, &request.Content); err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusInternalServerError, "Failed to announce content", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	response := map[string]interface{}{
 | |
| 		"address":   addr.String(),
 | |
| 		"announced": true,
 | |
| 	}
 | |
| 
 | |
| 	s.writeSuccessResponse(w, response)
 | |
| }
 | |
| 
 | |
| // handleDiscover handles content discovery requests
 | |
| func (s *Server) handleDiscover(w http.ResponseWriter, r *http.Request) {
 | |
| 	requestID := s.getRequestID(r)
 | |
| 	builder := ucxl.NewResponseBuilder(requestID, "ucxi-server")
 | |
| 	path := r.URL.Path
 | |
| 
 | |
| 	if r.Method != http.MethodGet {
 | |
| 		err := builder.MethodNotAllowed([]string{"GET"}, path)
 | |
| 		s.writeUCXLError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	pattern := r.URL.Query().Get("pattern")
 | |
| 	if pattern == "" {
 | |
| 		err := builder.BadRequest("Missing pattern parameter", path)
 | |
| 		s.writeUCXLError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addr, err := ucxl.Parse(pattern)
 | |
| 	if err != nil {
 | |
| 		ucxlErr := builder.InvalidAddress("Invalid UCXL pattern format", path, map[string]interface{}{
 | |
| 			"provided_pattern": pattern,
 | |
| 			"parse_error":      err.Error(),
 | |
| 		})
 | |
| 		s.writeUCXLError(w, ucxlErr)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	results, err := s.resolver.Discover(r.Context(), addr)
 | |
| 	if err != nil {
 | |
| 		ucxlErr := builder.ErrorWithDetails(ucxl.CodeInternalError, "Discovery operation failed", path, map[string]interface{}{
 | |
| 			"pattern":         addr.String(),
 | |
| 			"discovery_error": err.Error(),
 | |
| 		})
 | |
| 		s.writeUCXLError(w, ucxlErr)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	responseData := map[string]interface{}{
 | |
| 		"pattern":       addr.String(),
 | |
| 		"results":       results,
 | |
| 		"results_count": len(results),
 | |
| 	}
 | |
| 
 | |
| 	response := builder.OK(responseData)
 | |
| 	s.writeUCXLResponse(w, response)
 | |
| }
 | |
| 
 | |
| // handleNavigate handles temporal navigation requests
 | |
| func (s *Server) handleNavigate(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodPost {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var request struct {
 | |
| 		Address         string `json:"address"`
 | |
| 		TemporalSegment string `json:"temporal_segment"`
 | |
| 	}
 | |
| 
 | |
| 	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Invalid JSON request", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	addr, err := ucxl.Parse(request.Address)
 | |
| 	if err != nil {
 | |
| 		if validationErr, ok := err.(*ucxl.ValidationError); ok {
 | |
| 			s.writeUCXLValidationError(w, validationErr)
 | |
| 		} else {
 | |
| 			s.writeErrorResponse(w, http.StatusBadRequest, "Invalid UCXL address", err.Error())
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Get or create navigator for this address context
 | |
| 	navKey := s.generateNavigatorKey(addr)
 | |
| 	navigator := s.getOrCreateNavigator(navKey, 10) // Default to 10 versions
 | |
| 
 | |
| 	// Parse the new temporal segment
 | |
| 	tempAddr := fmt.Sprintf("ucxl://temp:temp@temp:temp/%s", request.TemporalSegment)
 | |
| 	tempParsed, err := ucxl.Parse(tempAddr)
 | |
| 	if err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Invalid temporal segment", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Perform navigation
 | |
| 	result, err := navigator.Navigate(tempParsed.TemporalSegment)
 | |
| 	if err != nil {
 | |
| 		s.writeErrorResponse(w, http.StatusBadRequest, "Navigation failed", err.Error())
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	s.writeSuccessResponse(w, result)
 | |
| }
 | |
| 
 | |
| // handleHealth handles health check requests
 | |
| func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
 | |
| 	if r.Method != http.MethodGet {
 | |
| 		s.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed", "")
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	health := map[string]interface{}{
 | |
| 		"status":  "healthy",
 | |
| 		"running": s.running,
 | |
| 		"uptime":  time.Now().UTC(),
 | |
| 	}
 | |
| 
 | |
| 	s.writeSuccessResponse(w, health)
 | |
| }
 | |
| 
 | |
| // handleStatus handles server status requests
 | |
| // Implements requirements from Issue 010 - Status Endpoints and Config Surface
 | |
| // Extended to include role-based collaboration and HMMM integration status
 | |
| func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
 | |
| 	requestID := s.getRequestID(r)
 | |
| 	builder := ucxl.NewResponseBuilder(requestID, "ucxi-server")
 | |
| 	path := r.URL.Path
 | |
| 
 | |
| 	if r.Method != http.MethodGet {
 | |
| 		err := builder.MethodNotAllowed([]string{"GET"}, path)
 | |
| 		s.writeUCXLError(w, err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	s.navMutex.RLock()
 | |
| 	navigatorCount := len(s.navigators)
 | |
| 	navigatorKeys := make([]string, 0, len(s.navigators))
 | |
| 	for key := range s.navigators {
 | |
| 		navigatorKeys = append(navigatorKeys, key)
 | |
| 	}
 | |
| 	s.navMutex.RUnlock()
 | |
| 
 | |
| 	// Get resolver and storage metrics if available
 | |
| 	resolverStats := s.getResolverStats()
 | |
| 	storageMetrics := s.getStorageMetrics()
 | |
| 	collaborationStatus := s.getCollaborationStatus()
 | |
| 	hmmmIntegrationStatus := s.getHmmmIntegrationStatus()
 | |
| 
 | |
| 	status := map[string]interface{}{
 | |
| 		"server": map[string]interface{}{
 | |
| 			"port":      s.port,
 | |
| 			"base_path": s.basePath,
 | |
| 			"running":   s.running,
 | |
| 			"version":   "2.1.0", // Incremented for role-based collaboration support
 | |
| 			"started_at": time.Now().Add(-time.Hour).UTC(), // Placeholder - would track actual start time
 | |
| 		},
 | |
| 		"ucxi": map[string]interface{}{
 | |
| 			"enabled": s.running,
 | |
| 			"endpoints": []string{
 | |
| 				"/get", "/put", "/post", "/delete",
 | |
| 				"/announce", "/discover", "/navigate",
 | |
| 				"/health", "/status", "/collaboration",
 | |
| 			},
 | |
| 		},
 | |
| 		"resolver": resolverStats,
 | |
| 		"storage":  storageMetrics,
 | |
| 		"navigators": map[string]interface{}{
 | |
| 			"active_count": navigatorCount,
 | |
| 			"keys":        navigatorKeys,
 | |
| 		},
 | |
| 		"p2p": map[string]interface{}{
 | |
| 			"enabled": s.resolver != nil,
 | |
| 			"announce_enabled": s.resolver != nil,
 | |
| 			"discover_enabled": s.resolver != nil,
 | |
| 		},
 | |
| 		"collaboration": collaborationStatus,
 | |
| 		"hmmm_integration": hmmmIntegrationStatus,
 | |
| 		"metrics": map[string]interface{}{
 | |
| 			"timestamp": time.Now().UTC(),
 | |
| 			"uptime_seconds": int64(time.Hour.Seconds()), // Placeholder
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	response := builder.OK(status)
 | |
| 	s.writeUCXLResponse(w, response)
 | |
| }
 | |
| 
 | |
| // handleCollaboration handles role-based collaboration endpoint requests
 | |
| func (s *Server) handleCollaboration(w http.ResponseWriter, r *http.Request) {
 | |
| 	requestID := s.getRequestID(r)
 | |
| 	builder := ucxl.NewResponseBuilder(requestID, "ucxi-server")
 | |
| 	path := r.URL.Path
 | |
| 
 | |
| 	switch r.Method {
 | |
| 	case http.MethodGet:
 | |
| 		s.handleGetCollaboration(w, r, builder, path)
 | |
| 	case http.MethodPost:
 | |
| 		s.handlePostCollaboration(w, r, builder, path)
 | |
| 	default:
 | |
| 		err := builder.MethodNotAllowed([]string{"GET", "POST"}, path)
 | |
| 		s.writeUCXLError(w, err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // handleGetCollaboration handles GET requests for collaboration status
 | |
| func (s *Server) handleGetCollaboration(w http.ResponseWriter, r *http.Request, builder *ucxl.ResponseBuilder, path string) {
 | |
| 	// Get query parameters for filtering
 | |
| 	roleFilter := r.URL.Query().Get("role")
 | |
| 	projectFilter := r.URL.Query().Get("project")
 | |
| 	expertiseFilter := r.URL.Query().Get("expertise")
 | |
| 
 | |
| 	collaborationData := map[string]interface{}{
 | |
| 		"system": s.getCollaborationStatus(),
 | |
| 		"filters_applied": map[string]interface{}{
 | |
| 			"role": roleFilter,
 | |
| 			"project": projectFilter,
 | |
| 			"expertise": expertiseFilter,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// If specific filters are requested, provide more detailed information
 | |
| 	if roleFilter != "" || projectFilter != "" || expertiseFilter != "" {
 | |
| 		collaborationData["filtered_results"] = s.getFilteredCollaborationResults(roleFilter, projectFilter, expertiseFilter)
 | |
| 	}
 | |
| 
 | |
| 	// Add active collaboration sessions (would be populated from actual pubsub system)
 | |
| 	collaborationData["active_sessions"] = []map[string]interface{}{
 | |
| 		{
 | |
| 			"type": "expertise_request",
 | |
| 			"from_role": "junior_developer",
 | |
| 			"required_expertise": []string{"api_design", "error_handling"},
 | |
| 			"project_id": "CHORUS",
 | |
| 			"thread_id": "thread-123",
 | |
| 			"participants": []string{"claude", "alice"},
 | |
| 			"status": "active",
 | |
| 			"created_at": time.Now().Add(-10 * time.Minute).UTC(),
 | |
| 		},
 | |
| 		{
 | |
| 			"type": "project_update",
 | |
| 			"from_role": "tech_lead",
 | |
| 			"project_id": "CHORUS",
 | |
| 			"thread_id": "thread-456",
 | |
| 			"deliverable": "api_standardization",
 | |
| 			"status": "in_progress",
 | |
| 			"progress": 75,
 | |
| 			"created_at": time.Now().Add(-5 * time.Minute).UTC(),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	response := builder.OK(collaborationData)
 | |
| 	s.writeUCXLResponse(w, response)
 | |
| }
 | |
| 
 | |
| // handlePostCollaboration handles POST requests for initiating collaboration
 | |
| func (s *Server) handlePostCollaboration(w http.ResponseWriter, r *http.Request, builder *ucxl.ResponseBuilder, path string) {
 | |
| 	var request struct {
 | |
| 		Type              string                 `json:"type"`
 | |
| 		FromRole          string                 `json:"from_role"`
 | |
| 		ToRoles           []string               `json:"to_roles,omitempty"`
 | |
| 		RequiredExpertise []string               `json:"required_expertise,omitempty"`
 | |
| 		ProjectID         string                 `json:"project_id,omitempty"`
 | |
| 		Priority          string                 `json:"priority,omitempty"`
 | |
| 		Data              map[string]interface{} `json:"data"`
 | |
| 	}
 | |
| 
 | |
| 	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
 | |
| 		ucxlErr := builder.BadRequest("Invalid JSON request body", path)
 | |
| 		s.writeUCXLError(w, ucxlErr)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Validate collaboration request
 | |
| 	if request.Type == "" {
 | |
| 		ucxlErr := builder.ErrorWithDetails(ucxl.CodeInvalidPayload, "Missing collaboration type", path, map[string]interface{}{
 | |
| 			"field": "type",
 | |
| 			"valid_types": []string{
 | |
| 				"expertise_request", "mentorship_request", "project_update",
 | |
| 				"status_update", "work_allocation", "deliverable_ready",
 | |
| 			},
 | |
| 		})
 | |
| 		s.writeUCXLError(w, ucxlErr)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if request.FromRole == "" {
 | |
| 		ucxlErr := builder.ErrorWithDetails(ucxl.CodeInvalidPayload, "Missing from_role", path, map[string]interface{}{
 | |
| 			"field": "from_role",
 | |
| 			"message": "Collaboration requests must specify the initiating role",
 | |
| 		})
 | |
| 		s.writeUCXLError(w, ucxlErr)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Generate collaboration session ID
 | |
| 	threadID := fmt.Sprintf("thread-%s-%d", request.Type, time.Now().Unix())
 | |
| 
 | |
| 	// In a real implementation, this would trigger pubsub messages
 | |
| 	// For now, we simulate the response
 | |
| 	collaborationResult := map[string]interface{}{
 | |
| 		"collaboration_initiated": true,
 | |
| 		"thread_id": threadID,
 | |
| 		"type": request.Type,
 | |
| 		"from_role": request.FromRole,
 | |
| 		"to_roles": request.ToRoles,
 | |
| 		"required_expertise": request.RequiredExpertise,
 | |
| 		"project_id": request.ProjectID,
 | |
| 		"priority": request.Priority,
 | |
| 		"status": "initiated",
 | |
| 		"created_at": time.Now().UTC(),
 | |
| 	}
 | |
| 
 | |
| 	// Add type-specific response data
 | |
| 	switch request.Type {
 | |
| 	case "expertise_request":
 | |
| 		collaborationResult["expected_response_time"] = "15m"
 | |
| 		collaborationResult["routing"] = "expertise_based"
 | |
| 	case "mentorship_request":
 | |
| 		collaborationResult["mentorship_type"] = "code_review"
 | |
| 		collaborationResult["routing"] = "seniority_based"
 | |
| 	case "project_update":
 | |
| 		collaborationResult["broadcast_scope"] = "project_wide"
 | |
| 		collaborationResult["routing"] = "project_based"
 | |
| 	}
 | |
| 
 | |
| 	response := builder.Created(collaborationResult)
 | |
| 	s.writeUCXLResponse(w, response)
 | |
| }
 | |
| 
 | |
| // getFilteredCollaborationResults returns filtered collaboration data
 | |
| func (s *Server) getFilteredCollaborationResults(role, project, expertise string) map[string]interface{} {
 | |
| 	// In a real implementation, this would query the actual pubsub system
 | |
| 	// For now, return simulated filtered results
 | |
| 	results := map[string]interface{}{
 | |
| 		"matching_agents": []map[string]interface{}{},
 | |
| 		"active_topics": []string{},
 | |
| 		"recent_activity": []map[string]interface{}{},
 | |
| 	}
 | |
| 
 | |
| 	if role != "" {
 | |
| 		results["matching_agents"] = []map[string]interface{}{
 | |
| 			{
 | |
| 				"agent_id": "claude",
 | |
| 				"role": role,
 | |
| 				"expertise": []string{"api_design", "error_handling", "documentation"},
 | |
| 				"availability": "available",
 | |
| 				"last_seen": time.Now().Add(-2 * time.Minute).UTC(),
 | |
| 			},
 | |
| 		}
 | |
| 		results["active_topics"] = []string{
 | |
| 			fmt.Sprintf("CHORUS/roles/%s/v1", strings.ToLower(strings.ReplaceAll(role, " ", "_"))),
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if project != "" {
 | |
| 		results["project_topics"] = []string{
 | |
| 			fmt.Sprintf("CHORUS/projects/%s/coordination/v1", project),
 | |
| 		}
 | |
| 		results["project_status"] = map[string]interface{}{
 | |
| 			"project_id": project,
 | |
| 			"active_collaborations": 2,
 | |
| 			"recent_deliverables": []string{"api_standardization"},
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if expertise != "" {
 | |
| 		results["expertise_topics"] = []string{
 | |
| 			fmt.Sprintf("CHORUS/expertise/%s/v1", strings.ToLower(strings.ReplaceAll(expertise, " ", "_"))),
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return results
 | |
| }
 | |
| 
 | |
| // getResolverStats returns resolver registry statistics
 | |
| func (s *Server) getResolverStats() map[string]interface{} {
 | |
| 	if s.resolver == nil {
 | |
| 		return map[string]interface{}{
 | |
| 			"enabled": false,
 | |
| 			"error": "resolver not configured",
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Basic resolver statistics
 | |
| 	// In a real implementation, these would come from the resolver interface
 | |
| 	return map[string]interface{}{
 | |
| 		"enabled": true,
 | |
| 		"operations": map[string]interface{}{
 | |
| 			"resolve_count": 0,  // Would track actual metrics
 | |
| 			"announce_count": 0, // Would track actual metrics
 | |
| 			"discover_count": 0, // Would track actual metrics
 | |
| 		},
 | |
| 		"performance": map[string]interface{}{
 | |
| 			"avg_resolve_time_ms": 0,
 | |
| 			"success_rate": 1.0,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // getStorageMetrics returns storage performance metrics
 | |
| func (s *Server) getStorageMetrics() map[string]interface{} {
 | |
| 	if s.storage == nil {
 | |
| 		return map[string]interface{}{
 | |
| 			"enabled": false,
 | |
| 			"error": "storage not configured",
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Basic storage metrics
 | |
| 	// In a real implementation, these would come from the storage interface
 | |
| 	return map[string]interface{}{
 | |
| 		"enabled": true,
 | |
| 		"operations": map[string]interface{}{
 | |
| 			"store_count": 0,     // Would track actual metrics
 | |
| 			"retrieve_count": 0,  // Would track actual metrics
 | |
| 			"delete_count": 0,    // Would track actual metrics
 | |
| 		},
 | |
| 		"cache": map[string]interface{}{
 | |
| 			"size": 0,           // Would track cache size
 | |
| 			"hit_rate": 0.0,     // Would track cache hit rate
 | |
| 			"miss_rate": 0.0,    // Would track cache miss rate
 | |
| 		},
 | |
| 		"performance": map[string]interface{}{
 | |
| 			"avg_store_time_ms": 0,
 | |
| 			"avg_retrieve_time_ms": 0,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // getCollaborationStatus returns role-based collaboration system status
 | |
| func (s *Server) getCollaborationStatus() map[string]interface{} {
 | |
| 	return map[string]interface{}{
 | |
| 		"enabled": true,
 | |
| 		"features": map[string]interface{}{
 | |
| 			"role_based_messaging": true,
 | |
| 			"expertise_routing": true,
 | |
| 			"mentorship_support": true,
 | |
| 			"project_coordination": true,
 | |
| 			"status_updates": true,
 | |
| 		},
 | |
| 		"pubsub": map[string]interface{}{
 | |
| 			"topics": map[string]interface{}{
 | |
| 				"chorus_coordination": "CHORUS/coordination/v1",
 | |
| 				"hmmm_meta_discussion": "hmmm/meta-discussion/v1",
 | |
| 				"context_feedback": "CHORUS/context-feedback/v1",
 | |
| 			},
 | |
| 			"dynamic_topics": map[string]interface{}{
 | |
| 				"role_based_enabled": true,
 | |
| 				"project_topics_enabled": true,
 | |
| 				"expertise_routing_enabled": true,
 | |
| 			},
 | |
| 		},
 | |
| 		"message_types": []string{
 | |
| 			"role_announcement", "expertise_request", "expertise_response",
 | |
| 			"status_update", "work_allocation", "role_collaboration",
 | |
| 			"mentorship_request", "mentorship_response", "project_update",
 | |
| 			"deliverable_ready",
 | |
| 		},
 | |
| 		"metrics": map[string]interface{}{
 | |
| 			"active_roles": 0,        // Would track from actual pubsub system
 | |
| 			"active_projects": 0,     // Would track from actual pubsub system
 | |
| 			"collaboration_events": 0, // Would track collaboration message counts
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // getHmmmIntegrationStatus returns HMMM adapter integration status
 | |
| func (s *Server) getHmmmIntegrationStatus() map[string]interface{} {
 | |
| 	return map[string]interface{}{
 | |
| 		"enabled": true,
 | |
| 		"adapter": map[string]interface{}{
 | |
| 			"version": "1.0.0",
 | |
| 			"raw_publish_enabled": true,
 | |
| 			"topic_auto_join": true,
 | |
| 		},
 | |
| 		"features": map[string]interface{}{
 | |
| 			"slurp_event_integration": true,
 | |
| 			"per_issue_rooms": true,
 | |
| 			"consensus_driven_events": true,
 | |
| 			"context_updates": true,
 | |
| 		},
 | |
| 		"topics": map[string]interface{}{
 | |
| 			"slurp_events": "hmmm/slurp-events/v1",
 | |
| 			"context_updates": "hmmm/context-updates/v1",
 | |
| 			"issue_discussions": "hmmm/issues/{issue_id}/v1",
 | |
| 		},
 | |
| 		"message_types": []string{
 | |
| 			"slurp_event_generated", "slurp_event_ack", "slurp_context_update",
 | |
| 			"meta_discussion", "coordination_request", "dependency_alert",
 | |
| 			"escalation_trigger",
 | |
| 		},
 | |
| 		"metrics": map[string]interface{}{
 | |
| 			"slurp_events_generated": 0,  // Would track actual metrics
 | |
| 			"slurp_events_acknowledged": 0, // Would track actual metrics
 | |
| 			"active_discussions": 0,      // Would track active HMMM discussions
 | |
| 			"consensus_sessions": 0,      // Would track consensus sessions
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Utility methods
 | |
| 
 | |
| // generateStorageKey generates a storage key from a UCXL address
 | |
| func (s *Server) generateStorageKey(addr *ucxl.Address) string {
 | |
| 	return fmt.Sprintf("%s:%s@%s:%s/%s",
 | |
| 		addr.Agent, addr.Role, addr.Project, addr.Task, addr.TemporalSegment.String())
 | |
| }
 | |
| 
 | |
| // generateNavigatorKey generates a navigator key from a UCXL address
 | |
| func (s *Server) generateNavigatorKey(addr *ucxl.Address) string {
 | |
| 	return fmt.Sprintf("%s:%s@%s:%s", addr.Agent, addr.Role, addr.Project, addr.Task)
 | |
| }
 | |
| 
 | |
| // getOrCreateNavigator gets or creates a temporal navigator
 | |
| func (s *Server) getOrCreateNavigator(key string, maxVersion int) *ucxl.TemporalNavigator {
 | |
| 	s.navMutex.Lock()
 | |
| 	defer s.navMutex.Unlock()
 | |
| 
 | |
| 	if navigator, exists := s.navigators[key]; exists {
 | |
| 		return navigator
 | |
| 	}
 | |
| 
 | |
| 	navigator := ucxl.NewTemporalNavigator(maxVersion)
 | |
| 	s.navigators[key] = navigator
 | |
| 	return navigator
 | |
| }
 | |
| 
 | |
| // withMiddleware wraps the handler with common middleware
 | |
| func (s *Server) withMiddleware(handler http.Handler) http.Handler {
 | |
| 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 		// Add CORS headers
 | |
| 		w.Header().Set("Access-Control-Allow-Origin", "*")
 | |
| 		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
 | |
| 		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Author, X-Meta-*")
 | |
| 
 | |
| 		// Handle preflight requests
 | |
| 		if r.Method == http.MethodOptions {
 | |
| 			w.WriteHeader(http.StatusOK)
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		// Set content type to JSON by default
 | |
| 		w.Header().Set("Content-Type", "application/json")
 | |
| 
 | |
| 		// Log request
 | |
| 		start := time.Now()
 | |
| 		s.logger.Debug("Request", "method", r.Method, "url", r.URL.String(), "remote", r.RemoteAddr)
 | |
| 
 | |
| 		// Call the handler
 | |
| 		handler.ServeHTTP(w, r)
 | |
| 
 | |
| 		// Log response
 | |
| 		duration := time.Since(start)
 | |
| 		s.logger.Debug("Response", "duration", duration.String())
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // writeSuccessResponse writes a successful JSON response
 | |
| func (s *Server) writeSuccessResponse(w http.ResponseWriter, data interface{}) {
 | |
| 	response := Response{
 | |
| 		Success:   true,
 | |
| 		Data:      data,
 | |
| 		Timestamp: time.Now().UTC(),
 | |
| 		Version:   "1.0.0",
 | |
| 	}
 | |
| 
 | |
| 	w.WriteHeader(http.StatusOK)
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| }
 | |
| 
 | |
| // writeErrorResponse writes an error JSON response
 | |
| func (s *Server) writeErrorResponse(w http.ResponseWriter, statusCode int, message, details string) {
 | |
| 	response := Response{
 | |
| 		Success:   false,
 | |
| 		Error:     message,
 | |
| 		Timestamp: time.Now().UTC(),
 | |
| 		Version:   "1.0.0",
 | |
| 	}
 | |
| 
 | |
| 	if details != "" {
 | |
| 		response.Data = map[string]string{"details": details}
 | |
| 	}
 | |
| 
 | |
| 	w.WriteHeader(statusCode)
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| }
 | |
| 
 | |
| // writeUCXLValidationError writes a structured UCXL validation error response
 | |
| func (s *Server) writeUCXLValidationError(w http.ResponseWriter, validationErr *ucxl.ValidationError) {
 | |
| 	ucxlError := UCXLValidationError{
 | |
| 		Code:    "UCXL-400-INVALID_ADDRESS",
 | |
| 		Field:   validationErr.Field,
 | |
| 		Message: validationErr.Message,
 | |
| 		Address: validationErr.Raw,
 | |
| 	}
 | |
| 
 | |
| 	response := Response{
 | |
| 		Success:   false,
 | |
| 		Error:     "Invalid UCXL address",
 | |
| 		Data:      ucxlError,
 | |
| 		Timestamp: time.Now().UTC(),
 | |
| 		Version:   "1.0.0",
 | |
| 	}
 | |
| 
 | |
| 	w.WriteHeader(http.StatusBadRequest)
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| }
 | |
| 
 | |
| // writeUCXLResponse writes a standardized UCXL success response
 | |
| func (s *Server) writeUCXLResponse(w http.ResponseWriter, response *ucxl.UCXLResponse) {
 | |
| 	httpStatus := ucxl.GetHTTPStatus(response.Response.Code)
 | |
| 	w.WriteHeader(httpStatus)
 | |
| 	json.NewEncoder(w).Encode(response)
 | |
| }
 | |
| 
 | |
| // writeUCXLError writes a standardized UCXL error response
 | |
| func (s *Server) writeUCXLError(w http.ResponseWriter, error *ucxl.UCXLError) {
 | |
| 	httpStatus := ucxl.GetHTTPStatus(error.Error.Code)
 | |
| 	w.WriteHeader(httpStatus)
 | |
| 	json.NewEncoder(w).Encode(error)
 | |
| }
 | |
| 
 | |
| 
 | |
| // getRequestID extracts or generates a request ID
 | |
| func (s *Server) getRequestID(r *http.Request) string {
 | |
| 	if r != nil {
 | |
| 		if requestID := r.Header.Get("X-Request-ID"); requestID != "" {
 | |
| 			return requestID
 | |
| 		}
 | |
| 		if requestID := r.Header.Get("Request-ID"); requestID != "" {
 | |
| 			return requestID
 | |
| 		}
 | |
| 	}
 | |
| 	// Generate a new request ID
 | |
| 	return time.Now().Format("20060102-150405") + "-" + s.randomString(8)
 | |
| }
 | |
| 
 | |
| // randomString generates a random string for request IDs
 | |
| func (s *Server) randomString(length int) string {
 | |
| 	const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
 | |
| 	result := make([]byte, length)
 | |
| 	for i := range result {
 | |
| 		result[i] = charset[time.Now().UnixNano()%(int64(len(charset)))]
 | |
| 	}
 | |
| 	return string(result)
 | |
| }
 | |
| 
 | |
| // Simple logger implementation
 | |
| type SimpleLogger struct{}
 | |
| 
 | |
| func (l SimpleLogger) Info(msg string, fields ...interface{})  { log.Printf("INFO: %s %v", msg, fields) }
 | |
| func (l SimpleLogger) Warn(msg string, fields ...interface{})  { log.Printf("WARN: %s %v", msg, fields) }
 | |
| func (l SimpleLogger) Error(msg string, fields ...interface{}) { log.Printf("ERROR: %s %v", msg, fields) }
 | |
| func (l SimpleLogger) Debug(msg string, fields ...interface{}) { log.Printf("DEBUG: %s %v", msg, fields) } | 
