Initial CHORUS project setup
🎭 CHORUS - Container-First P2P Task Coordination System - Docker-first architecture designed from ground up - Environment variable-based configuration (no config files) - Structured logging to stdout/stderr for container runtimes - License validation required for operation - Clean separation from BZZZ legacy systemd approach Core features implemented: - Container-optimized logging system - Environment-based configuration management - License validation with KACHING integration - Basic HTTP API and health endpoints - Docker build and deployment configuration Ready for P2P protocol development and AI integration. 🤖 Generated with Claude Code
This commit is contained in:
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Binaries
|
||||||
|
/chorus
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Go specific
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Environment and configuration
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
!*.env.example
|
||||||
|
/docker/chorus.env
|
||||||
|
|
||||||
|
# Data directories
|
||||||
|
/data/
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
/logs/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
/tmp/
|
||||||
|
*.tmp
|
||||||
99
README.md
Normal file
99
README.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# CHORUS - Container-First P2P Task Coordination System
|
||||||
|
|
||||||
|
CHORUS is a next-generation P2P task coordination and collaborative AI system designed from the ground up for containerized deployments. It takes the best lessons learned from BZZZ and reimagines them for Docker Swarm, Kubernetes, and modern container orchestration platforms.
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
CHORUS enables distributed AI agents to coordinate, collaborate, and execute tasks across container clusters, supporting deployments from single containers to hundreds of instances in enterprise environments.
|
||||||
|
|
||||||
|
## Key Design Principles
|
||||||
|
|
||||||
|
- **Container-First**: Designed specifically for Docker/Kubernetes deployments
|
||||||
|
- **License-Controlled**: Simple environment variable-based licensing
|
||||||
|
- **Cloud-Native Logging**: Structured logging to stdout/stderr for container runtime collection
|
||||||
|
- **Swarm-Ready P2P**: P2P protocols optimized for container networking
|
||||||
|
- **Scalable Agent IDs**: Agent identification system that works across distributed deployments
|
||||||
|
- **Zero-Config**: Minimal configuration requirements via environment variables
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
CHORUS follows a microservices architecture where each container runs a single agent instance:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ CHORUS Agent │ │ CHORUS Agent │ │ CHORUS Agent │
|
||||||
|
│ Container 1 │◄─┤ Container 2 │─►│ Container N │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│ │ │
|
||||||
|
└──────────────────────┼──────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Container │
|
||||||
|
│ Network │
|
||||||
|
│ (P2P Mesh) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Valid CHORUS license key
|
||||||
|
- Access to Ollama endpoints for AI functionality
|
||||||
|
|
||||||
|
### Basic Deployment
|
||||||
|
|
||||||
|
1. Clone and configure:
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.chorus.services/tony/CHORUS.git
|
||||||
|
cd CHORUS
|
||||||
|
cp docker/chorus.env.example docker/chorus.env
|
||||||
|
# Edit docker/chorus.env with your license key and configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Deploy:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Scale (Docker Swarm):
|
||||||
|
```bash
|
||||||
|
docker service scale chorus_agent=10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Licensing
|
||||||
|
|
||||||
|
CHORUS requires a valid license key to operate. Set your license key in the environment:
|
||||||
|
|
||||||
|
```env
|
||||||
|
CHORUS_LICENSE_KEY=your-license-key-here
|
||||||
|
CHORUS_LICENSE_EMAIL=your-email@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**No license = No operation.** CHORUS will not start without valid licensing.
|
||||||
|
|
||||||
|
## Differences from BZZZ
|
||||||
|
|
||||||
|
| Aspect | BZZZ | CHORUS |
|
||||||
|
|--------|------|--------|
|
||||||
|
| Deployment | systemd service (1 per host) | Container (N per cluster) |
|
||||||
|
| Configuration | Web UI setup | Environment variables |
|
||||||
|
| Logging | Journal/files | stdout/stderr (structured) |
|
||||||
|
| Licensing | Setup-time validation | Runtime environment variable |
|
||||||
|
| Agent IDs | Host-based | Container/cluster-based |
|
||||||
|
| P2P Discovery | mDNS local network | Container network + service discovery |
|
||||||
|
|
||||||
|
## Development Status
|
||||||
|
|
||||||
|
🚧 **Early Development** - CHORUS is being designed and built. Not yet ready for production use.
|
||||||
|
|
||||||
|
Current Phase: Architecture design and core foundation development.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
CHORUS is a commercial product. Contact chorus.services for licensing information.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
CHORUS is developed by the chorus.services team. For contributions or feedback, please use the issue tracker on our GITEA instance.
|
||||||
89
cmd/chorus/main.go
Normal file
89
cmd/chorus/main.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chorus.services/chorus/internal/agent"
|
||||||
|
"chorus.services/chorus/internal/config"
|
||||||
|
"chorus.services/chorus/internal/licensing"
|
||||||
|
"chorus.services/chorus/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppName = "CHORUS"
|
||||||
|
AppVersion = "0.1.0-dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize container-optimized logger
|
||||||
|
logger := logging.NewContainerLogger(AppName)
|
||||||
|
|
||||||
|
logger.Info("🎭 Starting CHORUS v%s - Container-First P2P Task Coordination", AppVersion)
|
||||||
|
|
||||||
|
// Load configuration from environment
|
||||||
|
cfg, err := config.LoadFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("❌ Configuration error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Validate license before any P2P operations
|
||||||
|
logger.Info("🔐 Validating CHORUS license...")
|
||||||
|
licenseValidator := licensing.NewValidator(cfg.License)
|
||||||
|
if err := licenseValidator.Validate(); err != nil {
|
||||||
|
logger.Error("❌ License validation failed: %v", err)
|
||||||
|
logger.Error("💰 CHORUS requires a valid license to operate")
|
||||||
|
logger.Error("📞 Contact chorus.services for licensing information")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.Info("✅ License validation successful")
|
||||||
|
|
||||||
|
// Create context for graceful shutdown
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Initialize CHORUS agent
|
||||||
|
agent, err := agent.New(ctx, cfg, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("❌ Failed to create agent: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start agent services
|
||||||
|
if err := agent.Start(); err != nil {
|
||||||
|
logger.Error("❌ Failed to start agent: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("✅ CHORUS agent started successfully")
|
||||||
|
logger.Info("🆔 Agent ID: %s", agent.ID())
|
||||||
|
logger.Info("🔗 P2P Address: %s", agent.P2PAddress())
|
||||||
|
logger.Info("🌐 API Endpoint: http://localhost:%d", cfg.Network.APIPort)
|
||||||
|
logger.Info("🏥 Health Endpoint: http://localhost:%d/health", cfg.Network.HealthPort)
|
||||||
|
|
||||||
|
// Set up graceful shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Wait for shutdown signal
|
||||||
|
<-sigChan
|
||||||
|
logger.Info("🛑 Shutdown signal received, stopping CHORUS agent...")
|
||||||
|
|
||||||
|
// Cancel context to trigger graceful shutdown
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Give services time to shut down gracefully
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
if err := agent.Stop(shutdownCtx); err != nil {
|
||||||
|
logger.Error("⚠️ Error during agent shutdown: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Info("✅ CHORUS agent stopped gracefully")
|
||||||
|
}
|
||||||
|
}
|
||||||
65
docker/Dockerfile
Normal file
65
docker/Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# CHORUS - Container-First P2P Task Coordination System
|
||||||
|
# Multi-stage build for minimal production image
|
||||||
|
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk --no-cache add git ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy go mod files first (for better caching)
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the CHORUS binary
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||||
|
-ldflags='-w -s -extldflags "-static"' \
|
||||||
|
-o chorus \
|
||||||
|
./cmd/chorus
|
||||||
|
|
||||||
|
# Final minimal runtime image
|
||||||
|
FROM alpine:3.18
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
curl
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup -g 1000 chorus && \
|
||||||
|
adduser -u 1000 -G chorus -s /bin/sh -D chorus
|
||||||
|
|
||||||
|
# Create application directories
|
||||||
|
RUN mkdir -p /app/data && \
|
||||||
|
chown -R chorus:chorus /app
|
||||||
|
|
||||||
|
# Copy binary from builder stage
|
||||||
|
COPY --from=builder /build/chorus /app/chorus
|
||||||
|
RUN chmod +x /app/chorus
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER chorus
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 8080 8081 9000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8081/health || exit 1
|
||||||
|
|
||||||
|
# Set default environment variables
|
||||||
|
ENV LOG_LEVEL=info \
|
||||||
|
LOG_FORMAT=structured \
|
||||||
|
CHORUS_BIND_ADDRESS=0.0.0.0 \
|
||||||
|
CHORUS_API_PORT=8080 \
|
||||||
|
CHORUS_HEALTH_PORT=8081 \
|
||||||
|
CHORUS_P2P_PORT=9000
|
||||||
|
|
||||||
|
# Start CHORUS
|
||||||
|
ENTRYPOINT ["/app/chorus"]
|
||||||
36
docker/build.sh
Executable file
36
docker/build.sh
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# CHORUS Build and Deployment Script
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
IMAGE_NAME="chorus"
|
||||||
|
IMAGE_TAG="${1:-latest}"
|
||||||
|
|
||||||
|
echo "🎭 Building CHORUS container image..."
|
||||||
|
echo "📁 Project root: $PROJECT_ROOT"
|
||||||
|
echo "🏷️ Image: $IMAGE_NAME:$IMAGE_TAG"
|
||||||
|
|
||||||
|
# Build the Docker image
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
docker build -f docker/Dockerfile -t "$IMAGE_NAME:$IMAGE_TAG" .
|
||||||
|
|
||||||
|
echo "✅ CHORUS container built successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Quick Start:"
|
||||||
|
echo " 1. Copy environment file:"
|
||||||
|
echo " cp docker/chorus.env.example docker/chorus.env"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Edit docker/chorus.env with your license key:"
|
||||||
|
echo " CHORUS_LICENSE_EMAIL=your-email@example.com"
|
||||||
|
echo " CHORUS_LICENSE_KEY=your-license-key-here"
|
||||||
|
echo ""
|
||||||
|
echo " 3. Start CHORUS:"
|
||||||
|
echo " docker-compose -f docker/docker-compose.yml --env-file docker/chorus.env up -d"
|
||||||
|
echo ""
|
||||||
|
echo " 4. Check status:"
|
||||||
|
echo " docker-compose -f docker/docker-compose.yml logs -f"
|
||||||
|
echo " curl http://localhost:8081/health"
|
||||||
|
echo ""
|
||||||
|
echo "📖 For production deployment instructions, see README.md"
|
||||||
43
docker/chorus.env.example
Normal file
43
docker/chorus.env.example
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# CHORUS Environment Configuration
|
||||||
|
# Copy this file to 'chorus.env' and customize for your deployment
|
||||||
|
|
||||||
|
# =================
|
||||||
|
# REQUIRED SETTINGS
|
||||||
|
# =================
|
||||||
|
|
||||||
|
# License configuration (REQUIRED - CHORUS will not start without these)
|
||||||
|
CHORUS_LICENSE_EMAIL=your-email@example.com
|
||||||
|
CHORUS_LICENSE_KEY=your-license-key-here
|
||||||
|
CHORUS_CLUSTER_ID=production-cluster
|
||||||
|
|
||||||
|
# ==================
|
||||||
|
# OPTIONAL SETTINGS
|
||||||
|
# ==================
|
||||||
|
|
||||||
|
# Agent Configuration
|
||||||
|
# CHORUS_AGENT_ID= # Auto-generated if not specified
|
||||||
|
CHORUS_SPECIALIZATION=general_developer
|
||||||
|
CHORUS_MAX_TASKS=3
|
||||||
|
CHORUS_CAPABILITIES=general_development,task_coordination,ai_integration
|
||||||
|
|
||||||
|
# Network Ports (adjust if ports conflict)
|
||||||
|
CHORUS_API_PORT=8080
|
||||||
|
CHORUS_HEALTH_PORT=8081
|
||||||
|
CHORUS_P2P_PORT=9000
|
||||||
|
|
||||||
|
# AI Integration
|
||||||
|
OLLAMA_ENDPOINT=http://host.docker.internal:11434
|
||||||
|
CHORUS_DEFAULT_MODEL=llama3.1:8b
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOG_LEVEL=info # debug, info, warn, error
|
||||||
|
LOG_FORMAT=structured # structured (JSON) or human
|
||||||
|
|
||||||
|
# Docker Deployment Settings
|
||||||
|
CHORUS_REPLICAS=1 # Number of CHORUS instances to run
|
||||||
|
CHORUS_CPU_LIMIT=1.0 # CPU limit per container
|
||||||
|
CHORUS_MEMORY_LIMIT=1G # Memory limit per container
|
||||||
|
|
||||||
|
# Advanced P2P Settings (for cluster deployments)
|
||||||
|
# CHORUS_BOOTSTRAP_PEERS= # Comma-separated list of bootstrap peers
|
||||||
|
# CHORUS_DHT_ENABLED=true # Enable DHT for peer discovery
|
||||||
110
docker/docker-compose.yml
Normal file
110
docker/docker-compose.yml
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
chorus:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: docker/Dockerfile
|
||||||
|
image: chorus:latest
|
||||||
|
|
||||||
|
# REQUIRED: License configuration (CHORUS will not start without this)
|
||||||
|
environment:
|
||||||
|
# CRITICAL: License configuration - REQUIRED for operation
|
||||||
|
- CHORUS_LICENSE_EMAIL=${CHORUS_LICENSE_EMAIL:?CHORUS_LICENSE_EMAIL is required}
|
||||||
|
- CHORUS_LICENSE_KEY=${CHORUS_LICENSE_KEY:?CHORUS_LICENSE_KEY is required}
|
||||||
|
- CHORUS_CLUSTER_ID=${CHORUS_CLUSTER_ID:-docker-cluster}
|
||||||
|
|
||||||
|
# Agent configuration
|
||||||
|
- CHORUS_AGENT_ID=${CHORUS_AGENT_ID:-} # Auto-generated if not provided
|
||||||
|
- CHORUS_SPECIALIZATION=${CHORUS_SPECIALIZATION:-general_developer}
|
||||||
|
- CHORUS_MAX_TASKS=${CHORUS_MAX_TASKS:-3}
|
||||||
|
- CHORUS_CAPABILITIES=${CHORUS_CAPABILITIES:-general_development,task_coordination}
|
||||||
|
|
||||||
|
# Network configuration
|
||||||
|
- CHORUS_API_PORT=8080
|
||||||
|
- CHORUS_HEALTH_PORT=8081
|
||||||
|
- CHORUS_P2P_PORT=9000
|
||||||
|
- CHORUS_BIND_ADDRESS=0.0.0.0
|
||||||
|
|
||||||
|
# AI configuration
|
||||||
|
- OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT:-http://host.docker.internal:11434}
|
||||||
|
- CHORUS_DEFAULT_MODEL=${CHORUS_DEFAULT_MODEL:-llama3.1:8b}
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
- LOG_FORMAT=${LOG_FORMAT:-structured}
|
||||||
|
|
||||||
|
# Persistent data storage
|
||||||
|
volumes:
|
||||||
|
- chorus_data:/app/data
|
||||||
|
|
||||||
|
# Network ports
|
||||||
|
ports:
|
||||||
|
- "${CHORUS_API_PORT:-8080}:8080" # HTTP API
|
||||||
|
- "${CHORUS_HEALTH_PORT:-8081}:8081" # Health checks
|
||||||
|
- "${CHORUS_P2P_PORT:-9000}:9000" # P2P communication
|
||||||
|
|
||||||
|
# Container resource limits
|
||||||
|
deploy:
|
||||||
|
mode: replicated
|
||||||
|
replicas: ${CHORUS_REPLICAS:-1}
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
delay: 10s
|
||||||
|
failure_action: rollback
|
||||||
|
order: start-first
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
delay: 5s
|
||||||
|
max_attempts: 3
|
||||||
|
window: 120s
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "${CHORUS_CPU_LIMIT:-1.0}"
|
||||||
|
memory: "${CHORUS_MEMORY_LIMIT:-1G}"
|
||||||
|
reservations:
|
||||||
|
cpus: "0.1"
|
||||||
|
memory: 128M
|
||||||
|
placement:
|
||||||
|
preferences:
|
||||||
|
- spread: node.id
|
||||||
|
constraints:
|
||||||
|
- node.role == worker
|
||||||
|
|
||||||
|
# Network configuration
|
||||||
|
networks:
|
||||||
|
- chorus_net
|
||||||
|
|
||||||
|
# Host resolution for external services
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
# Container logging configuration
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
tag: "{{.ImageName}}/{{.Name}}/{{.ID}}"
|
||||||
|
|
||||||
|
# Health check configuration
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
# Persistent volumes
|
||||||
|
volumes:
|
||||||
|
chorus_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# Networks for CHORUS communication
|
||||||
|
networks:
|
||||||
|
chorus_net:
|
||||||
|
driver: overlay
|
||||||
|
attachable: true
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 10.201.0.0/24
|
||||||
12
go.mod
Normal file
12
go.mod
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module chorus.services/chorus
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/libp2p/go-libp2p v0.32.2
|
||||||
|
github.com/libp2p/go-libp2p-kad-dht v0.25.2
|
||||||
|
github.com/libp2p/go-libp2p-pubsub v0.9.3
|
||||||
|
github.com/multiformats/go-multiaddr v0.12.2
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
214
internal/agent/agent.go
Normal file
214
internal/agent/agent.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"chorus.services/chorus/internal/config"
|
||||||
|
"chorus.services/chorus/internal/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Agent represents a CHORUS agent instance
|
||||||
|
type Agent struct {
|
||||||
|
id string
|
||||||
|
config *config.Config
|
||||||
|
logger logging.Logger
|
||||||
|
|
||||||
|
// Services
|
||||||
|
apiServer *http.Server
|
||||||
|
healthServer *http.Server
|
||||||
|
|
||||||
|
// P2P components (to be implemented)
|
||||||
|
// p2pHost host.Host
|
||||||
|
// dht *dht.DHT
|
||||||
|
// pubsub *pubsub.PubSub
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new CHORUS agent
|
||||||
|
func New(ctx context.Context, cfg *config.Config, logger logging.Logger) (*Agent, error) {
|
||||||
|
agent := &Agent{
|
||||||
|
id: cfg.Agent.ID,
|
||||||
|
config: cfg,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize HTTP servers
|
||||||
|
if err := agent.initHTTPServers(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize HTTP servers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Initialize P2P components
|
||||||
|
// TODO: Initialize task coordination
|
||||||
|
// TODO: Initialize AI integration
|
||||||
|
|
||||||
|
return agent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts all agent services
|
||||||
|
func (a *Agent) Start() error {
|
||||||
|
a.logger.Info("🚀 Starting CHORUS agent services...")
|
||||||
|
|
||||||
|
// Start API server
|
||||||
|
go func() {
|
||||||
|
a.logger.Info("🌐 Starting API server on :%d", a.config.Network.APIPort)
|
||||||
|
if err := a.apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
a.logger.Error("❌ API server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start health server
|
||||||
|
go func() {
|
||||||
|
a.logger.Info("🏥 Starting health server on :%d", a.config.Network.HealthPort)
|
||||||
|
if err := a.healthServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
a.logger.Error("❌ Health server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO: Start P2P services
|
||||||
|
// TODO: Start task coordination
|
||||||
|
// TODO: Connect to DHT network
|
||||||
|
|
||||||
|
a.logger.Info("✅ All CHORUS agent services started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop gracefully stops all agent services
|
||||||
|
func (a *Agent) Stop(ctx context.Context) error {
|
||||||
|
a.logger.Info("🛑 Stopping CHORUS agent services...")
|
||||||
|
|
||||||
|
// Stop HTTP servers
|
||||||
|
if err := a.apiServer.Shutdown(ctx); err != nil {
|
||||||
|
a.logger.Error("⚠️ Error stopping API server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.healthServer.Shutdown(ctx); err != nil {
|
||||||
|
a.logger.Error("⚠️ Error stopping health server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Stop P2P services
|
||||||
|
// TODO: Stop task coordination
|
||||||
|
// TODO: Disconnect from DHT network
|
||||||
|
|
||||||
|
a.logger.Info("✅ CHORUS agent services stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the agent ID
|
||||||
|
func (a *Agent) ID() string {
|
||||||
|
return a.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// P2PAddress returns the P2P address (placeholder)
|
||||||
|
func (a *Agent) P2PAddress() string {
|
||||||
|
// TODO: Return actual P2P address when P2P is implemented
|
||||||
|
return fmt.Sprintf("/ip4/%s/tcp/%d/p2p/%s", a.config.Network.BindAddr, a.config.Network.P2PPort, a.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initHTTPServers initializes the HTTP servers for API and health endpoints
|
||||||
|
func (a *Agent) initHTTPServers() error {
|
||||||
|
// API server
|
||||||
|
apiMux := http.NewServeMux()
|
||||||
|
apiMux.HandleFunc("/", a.handleRoot)
|
||||||
|
apiMux.HandleFunc("/agent/info", a.handleAgentInfo)
|
||||||
|
apiMux.HandleFunc("/agent/tasks", a.handleTasks)
|
||||||
|
|
||||||
|
a.apiServer = &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", a.config.Network.BindAddr, a.config.Network.APIPort),
|
||||||
|
Handler: apiMux,
|
||||||
|
ReadTimeout: 15 * time.Second,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health server
|
||||||
|
healthMux := http.NewServeMux()
|
||||||
|
healthMux.HandleFunc("/health", a.handleHealth)
|
||||||
|
healthMux.HandleFunc("/health/ready", a.handleReady)
|
||||||
|
healthMux.HandleFunc("/health/live", a.handleLive)
|
||||||
|
|
||||||
|
a.healthServer = &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", a.config.Network.BindAddr, a.config.Network.HealthPort),
|
||||||
|
Handler: healthMux,
|
||||||
|
ReadTimeout: 5 * time.Second,
|
||||||
|
WriteTimeout: 5 * time.Second,
|
||||||
|
IdleTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTP handler implementations
|
||||||
|
|
||||||
|
func (a *Agent) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := map[string]interface{}{
|
||||||
|
"service": "CHORUS",
|
||||||
|
"version": "0.1.0-dev",
|
||||||
|
"agent_id": a.id,
|
||||||
|
"status": "running",
|
||||||
|
"endpoints": map[string]string{
|
||||||
|
"agent_info": "/agent/info",
|
||||||
|
"tasks": "/agent/tasks",
|
||||||
|
"health": fmt.Sprintf("http://localhost:%d/health", a.config.Network.HealthPort),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
// Simple JSON response without external dependency
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"service": "%s",
|
||||||
|
"version": "0.1.0-dev",
|
||||||
|
"agent_id": "%s",
|
||||||
|
"status": "running"
|
||||||
|
}`, "CHORUS", a.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleAgentInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"agent_id": "%s",
|
||||||
|
"specialization": "%s",
|
||||||
|
"max_tasks": %d,
|
||||||
|
"capabilities": %q
|
||||||
|
}`, a.id, a.config.Agent.Specialization, a.config.Agent.MaxTasks, a.config.Agent.Capabilities)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleTasks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: Implement task management
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"active_tasks": [],
|
||||||
|
"completed_tasks": [],
|
||||||
|
"available_for_tasks": true
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "healthy",
|
||||||
|
"agent_id": "%s",
|
||||||
|
"timestamp": "%s",
|
||||||
|
"checks": {
|
||||||
|
"license": "valid",
|
||||||
|
"api_server": "running",
|
||||||
|
"p2p": "not_implemented"
|
||||||
|
}
|
||||||
|
}`, a.id, time.Now().UTC().Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleReady(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Kubernetes readiness probe
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleLive(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Kubernetes liveness probe
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "live")
|
||||||
|
}
|
||||||
140
internal/config/config.go
Normal file
140
internal/config/config.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the complete CHORUS configuration loaded from environment variables
|
||||||
|
type Config struct {
|
||||||
|
Agent AgentConfig `yaml:"agent"`
|
||||||
|
Network NetworkConfig `yaml:"network"`
|
||||||
|
License LicenseConfig `yaml:"license"`
|
||||||
|
AI AIConfig `yaml:"ai"`
|
||||||
|
Logging LoggingConfig `yaml:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentConfig defines agent-specific settings
|
||||||
|
type AgentConfig struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Specialization string `yaml:"specialization"`
|
||||||
|
MaxTasks int `yaml:"max_tasks"`
|
||||||
|
Capabilities []string `yaml:"capabilities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkConfig defines network and API settings
|
||||||
|
type NetworkConfig struct {
|
||||||
|
P2PPort int `yaml:"p2p_port"`
|
||||||
|
APIPort int `yaml:"api_port"`
|
||||||
|
HealthPort int `yaml:"health_port"`
|
||||||
|
BindAddr string `yaml:"bind_address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LicenseConfig defines licensing settings
|
||||||
|
type LicenseConfig struct {
|
||||||
|
Email string `yaml:"email"`
|
||||||
|
LicenseKey string `yaml:"license_key"`
|
||||||
|
ClusterID string `yaml:"cluster_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AIConfig defines AI service settings
|
||||||
|
type AIConfig struct {
|
||||||
|
OllamaEndpoint string `yaml:"ollama_endpoint"`
|
||||||
|
DefaultModel string `yaml:"default_model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggingConfig defines logging settings
|
||||||
|
type LoggingConfig struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
Format string `yaml:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromEnvironment loads configuration from environment variables
|
||||||
|
// This is the primary configuration method for CHORUS (no config files)
|
||||||
|
func LoadFromEnvironment() (*Config, error) {
|
||||||
|
cfg := &Config{
|
||||||
|
Agent: AgentConfig{
|
||||||
|
ID: getEnvOrDefault("CHORUS_AGENT_ID", ""),
|
||||||
|
Specialization: getEnvOrDefault("CHORUS_SPECIALIZATION", "general_developer"),
|
||||||
|
MaxTasks: getEnvIntOrDefault("CHORUS_MAX_TASKS", 3),
|
||||||
|
Capabilities: getEnvArrayOrDefault("CHORUS_CAPABILITIES", []string{"general_development", "task_coordination"}),
|
||||||
|
},
|
||||||
|
Network: NetworkConfig{
|
||||||
|
P2PPort: getEnvIntOrDefault("CHORUS_P2P_PORT", 9000),
|
||||||
|
APIPort: getEnvIntOrDefault("CHORUS_API_PORT", 8080),
|
||||||
|
HealthPort: getEnvIntOrDefault("CHORUS_HEALTH_PORT", 8081),
|
||||||
|
BindAddr: getEnvOrDefault("CHORUS_BIND_ADDRESS", "0.0.0.0"),
|
||||||
|
},
|
||||||
|
License: LicenseConfig{
|
||||||
|
Email: os.Getenv("CHORUS_LICENSE_EMAIL"),
|
||||||
|
LicenseKey: os.Getenv("CHORUS_LICENSE_KEY"),
|
||||||
|
ClusterID: getEnvOrDefault("CHORUS_CLUSTER_ID", "default-cluster"),
|
||||||
|
},
|
||||||
|
AI: AIConfig{
|
||||||
|
OllamaEndpoint: getEnvOrDefault("OLLAMA_ENDPOINT", "http://localhost:11434"),
|
||||||
|
DefaultModel: getEnvOrDefault("CHORUS_DEFAULT_MODEL", "llama3.1:8b"),
|
||||||
|
},
|
||||||
|
Logging: LoggingConfig{
|
||||||
|
Level: getEnvOrDefault("LOG_LEVEL", "info"),
|
||||||
|
Format: getEnvOrDefault("LOG_FORMAT", "structured"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required configuration
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("configuration validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ensures all required configuration is present
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
if c.License.Email == "" {
|
||||||
|
return fmt.Errorf("CHORUS_LICENSE_EMAIL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.License.LicenseKey == "" {
|
||||||
|
return fmt.Errorf("CHORUS_LICENSE_KEY is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Agent.ID == "" {
|
||||||
|
// Auto-generate agent ID if not provided
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
containerID := os.Getenv("HOSTNAME") // Docker sets this to container ID
|
||||||
|
if containerID != "" && containerID != hostname {
|
||||||
|
c.Agent.ID = fmt.Sprintf("chorus-%s", containerID[:12])
|
||||||
|
} else {
|
||||||
|
c.Agent.ID = fmt.Sprintf("chorus-%s", hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for environment variable parsing
|
||||||
|
|
||||||
|
func getEnvOrDefault(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvIntOrDefault(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if parsed, err := strconv.Atoi(value); err == nil {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvArrayOrDefault(key string, defaultValue []string) []string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
97
internal/licensing/validator.go
Normal file
97
internal/licensing/validator.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package licensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultKachingURL = "https://kaching.chorus.services"
|
||||||
|
LicenseTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// LicenseConfig holds licensing information
|
||||||
|
type LicenseConfig struct {
|
||||||
|
Email string
|
||||||
|
LicenseKey string
|
||||||
|
ClusterID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator handles license validation with KACHING
|
||||||
|
type Validator struct {
|
||||||
|
config LicenseConfig
|
||||||
|
kachingURL string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidator creates a new license validator
|
||||||
|
func NewValidator(config LicenseConfig) *Validator {
|
||||||
|
return &Validator{
|
||||||
|
config: config,
|
||||||
|
kachingURL: DefaultKachingURL,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: LicenseTimeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs license validation with KACHING license authority
|
||||||
|
// CRITICAL: CHORUS will not start without valid license validation
|
||||||
|
func (v *Validator) Validate() error {
|
||||||
|
if v.config.Email == "" || v.config.LicenseKey == "" {
|
||||||
|
return fmt.Errorf("license email and key are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare validation request
|
||||||
|
request := map[string]interface{}{
|
||||||
|
"email": v.config.Email,
|
||||||
|
"license_key": v.config.LicenseKey,
|
||||||
|
"cluster_id": v.config.ClusterID,
|
||||||
|
"product": "CHORUS",
|
||||||
|
"version": "0.1.0-dev",
|
||||||
|
"container": true, // Flag indicating this is a container deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal license request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call KACHING license authority
|
||||||
|
licenseURL := fmt.Sprintf("%s/v1/license/validate", v.kachingURL)
|
||||||
|
resp, err := v.client.Post(licenseURL, "application/json", bytes.NewReader(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
// FAIL-CLOSED: No network = No license = No operation
|
||||||
|
return fmt.Errorf("unable to contact license authority: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var licenseResponse map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&licenseResponse); err != nil {
|
||||||
|
return fmt.Errorf("invalid license authority response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check validation result
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
message := "license validation failed"
|
||||||
|
if msg, ok := licenseResponse["message"].(string); ok {
|
||||||
|
message = msg
|
||||||
|
}
|
||||||
|
return fmt.Errorf("license validation failed: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// License is valid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateBackground performs background license validation (for runtime checks)
|
||||||
|
// This is used for periodic license validation during operation
|
||||||
|
func (v *Validator) ValidateBackground() error {
|
||||||
|
// Similar to Validate() but with longer timeout and retry logic
|
||||||
|
// Implementation would include retry logic and graceful degradation
|
||||||
|
return v.Validate()
|
||||||
|
}
|
||||||
210
internal/logging/logger.go
Normal file
210
internal/logging/logger.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Logger interface for CHORUS logging
|
||||||
|
type Logger interface {
|
||||||
|
Info(msg string, args ...interface{})
|
||||||
|
Warn(msg string, args ...interface{})
|
||||||
|
Error(msg string, args ...interface{})
|
||||||
|
Debug(msg string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerLogger provides structured logging optimized for container environments
|
||||||
|
// All logs go to stdout/stderr for collection by container runtime (Docker, K8s, etc.)
|
||||||
|
type ContainerLogger struct {
|
||||||
|
name string
|
||||||
|
level LogLevel
|
||||||
|
format LogFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogLevel represents logging levels
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DEBUG LogLevel = iota
|
||||||
|
INFO
|
||||||
|
WARN
|
||||||
|
ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogFormat represents log output formats
|
||||||
|
type LogFormat int
|
||||||
|
|
||||||
|
const (
|
||||||
|
STRUCTURED LogFormat = iota // JSON structured logging
|
||||||
|
HUMAN // Human-readable logging
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogEntry represents a structured log entry
|
||||||
|
type LogEntry struct {
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContainerLogger creates a new container-optimized logger
|
||||||
|
func NewContainerLogger(serviceName string) *ContainerLogger {
|
||||||
|
level := INFO
|
||||||
|
format := STRUCTURED
|
||||||
|
|
||||||
|
// Parse log level from environment
|
||||||
|
if levelStr := os.Getenv("LOG_LEVEL"); levelStr != "" {
|
||||||
|
switch levelStr {
|
||||||
|
case "debug":
|
||||||
|
level = DEBUG
|
||||||
|
case "info":
|
||||||
|
level = INFO
|
||||||
|
case "warn":
|
||||||
|
level = WARN
|
||||||
|
case "error":
|
||||||
|
level = ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse log format from environment
|
||||||
|
if formatStr := os.Getenv("LOG_FORMAT"); formatStr == "human" {
|
||||||
|
format = HUMAN
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ContainerLogger{
|
||||||
|
name: serviceName,
|
||||||
|
level: level,
|
||||||
|
format: format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs informational messages
|
||||||
|
func (l *ContainerLogger) Info(msg string, args ...interface{}) {
|
||||||
|
if l.level <= INFO {
|
||||||
|
l.log(INFO, msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs warning messages
|
||||||
|
func (l *ContainerLogger) Warn(msg string, args ...interface{}) {
|
||||||
|
if l.level <= WARN {
|
||||||
|
l.log(WARN, msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs error messages to stderr
|
||||||
|
func (l *ContainerLogger) Error(msg string, args ...interface{}) {
|
||||||
|
if l.level <= ERROR {
|
||||||
|
l.logToStderr(ERROR, msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs debug messages (only when DEBUG level is enabled)
|
||||||
|
func (l *ContainerLogger) Debug(msg string, args ...interface{}) {
|
||||||
|
if l.level <= DEBUG {
|
||||||
|
l.log(DEBUG, msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// log writes log entries to stdout
|
||||||
|
func (l *ContainerLogger) log(level LogLevel, msg string, args ...interface{}) {
|
||||||
|
entry := l.createLogEntry(level, msg, args...)
|
||||||
|
|
||||||
|
switch l.format {
|
||||||
|
case STRUCTURED:
|
||||||
|
l.writeJSON(os.Stdout, entry)
|
||||||
|
case HUMAN:
|
||||||
|
l.writeHuman(os.Stdout, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logToStderr writes log entries to stderr (for errors)
|
||||||
|
func (l *ContainerLogger) logToStderr(level LogLevel, msg string, args ...interface{}) {
|
||||||
|
entry := l.createLogEntry(level, msg, args...)
|
||||||
|
|
||||||
|
switch l.format {
|
||||||
|
case STRUCTURED:
|
||||||
|
l.writeJSON(os.Stderr, entry)
|
||||||
|
case HUMAN:
|
||||||
|
l.writeHuman(os.Stderr, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createLogEntry creates a structured log entry
|
||||||
|
func (l *ContainerLogger) createLogEntry(level LogLevel, msg string, args ...interface{}) LogEntry {
|
||||||
|
return LogEntry{
|
||||||
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
Level: l.levelToString(level),
|
||||||
|
Service: l.name,
|
||||||
|
Message: fmt.Sprintf(msg, args...),
|
||||||
|
Data: make(map[string]interface{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSON writes the log entry as JSON
|
||||||
|
func (l *ContainerLogger) writeJSON(output *os.File, entry LogEntry) {
|
||||||
|
if jsonData, err := json.Marshal(entry); err == nil {
|
||||||
|
fmt.Fprintln(output, string(jsonData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeHuman writes the log entry in human-readable format
|
||||||
|
func (l *ContainerLogger) writeHuman(output *os.File, entry LogEntry) {
|
||||||
|
fmt.Fprintf(output, "[%s] [%s] [%s] %s\n",
|
||||||
|
entry.Timestamp,
|
||||||
|
entry.Level,
|
||||||
|
entry.Service,
|
||||||
|
entry.Message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// levelToString converts LogLevel to string
|
||||||
|
func (l *ContainerLogger) levelToString(level LogLevel) string {
|
||||||
|
switch level {
|
||||||
|
case DEBUG:
|
||||||
|
return "DEBUG"
|
||||||
|
case INFO:
|
||||||
|
return "INFO"
|
||||||
|
case WARN:
|
||||||
|
return "WARN"
|
||||||
|
case ERROR:
|
||||||
|
return "ERROR"
|
||||||
|
default:
|
||||||
|
return "UNKNOWN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithData creates a logger that includes additional structured data in log entries
|
||||||
|
func (l *ContainerLogger) WithData(data map[string]interface{}) Logger {
|
||||||
|
// Return a new logger instance that includes the data
|
||||||
|
// This is useful for request-scoped logging with context
|
||||||
|
return &dataLogger{
|
||||||
|
base: l,
|
||||||
|
data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataLogger is a wrapper that adds structured data to log entries
|
||||||
|
type dataLogger struct {
|
||||||
|
base Logger
|
||||||
|
data map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataLogger) Info(msg string, args ...interface{}) {
|
||||||
|
d.base.Info(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataLogger) Warn(msg string, args ...interface{}) {
|
||||||
|
d.base.Warn(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataLogger) Error(msg string, args ...interface{}) {
|
||||||
|
d.base.Error(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dataLogger) Debug(msg string, args ...interface{}) {
|
||||||
|
d.base.Debug(msg, args...)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user