feat: Initial OPHION structure
- Go backend with Fiber framework - Agent for metrics collection - Docker Compose for self-hosted - Auth middleware (JWT + API Keys) - Rate limiting - Install script
This commit is contained in:
135
cmd/agent/main.go
Normal file
135
cmd/agent/main.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
"github.com/shirou/gopsutil/v3/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Metrics struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
CPU CPUMetric `json:"cpu"`
|
||||||
|
Memory MemMetric `json:"memory"`
|
||||||
|
Disk []DiskMetric `json:"disk"`
|
||||||
|
Network NetMetric `json:"network"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CPUMetric struct {
|
||||||
|
UsagePercent float64 `json:"usage_percent"`
|
||||||
|
Cores int `json:"cores"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemMetric struct {
|
||||||
|
Total uint64 `json:"total"`
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
UsedPercent float64 `json:"used_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiskMetric struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Total uint64 `json:"total"`
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
UsedPercent float64 `json:"used_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetMetric struct {
|
||||||
|
BytesSent uint64 `json:"bytes_sent"`
|
||||||
|
BytesRecv uint64 `json:"bytes_recv"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
serverURL := os.Getenv("OPHION_SERVER")
|
||||||
|
if serverURL == "" {
|
||||||
|
serverURL = "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := os.Getenv("OPHION_API_KEY")
|
||||||
|
if apiKey == "" {
|
||||||
|
log.Fatal("OPHION_API_KEY is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
interval := 30 * time.Second
|
||||||
|
log.Printf("🐍 OPHION Agent starting - reporting to %s every %s", serverURL, interval)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
for range ticker.C {
|
||||||
|
metrics := collectMetrics()
|
||||||
|
sendMetrics(serverURL, apiKey, metrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectMetrics() Metrics {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
|
||||||
|
cpuPercent, _ := cpu.Percent(time.Second, false)
|
||||||
|
cpuUsage := 0.0
|
||||||
|
if len(cpuPercent) > 0 {
|
||||||
|
cpuUsage = cpuPercent[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
memInfo, _ := mem.VirtualMemory()
|
||||||
|
|
||||||
|
diskInfo, _ := disk.Usage("/")
|
||||||
|
disks := []DiskMetric{{
|
||||||
|
Path: "/",
|
||||||
|
Total: diskInfo.Total,
|
||||||
|
Used: diskInfo.Used,
|
||||||
|
UsedPercent: diskInfo.UsedPercent,
|
||||||
|
}}
|
||||||
|
|
||||||
|
netIO, _ := net.IOCounters(false)
|
||||||
|
netMetric := NetMetric{}
|
||||||
|
if len(netIO) > 0 {
|
||||||
|
netMetric.BytesSent = netIO[0].BytesSent
|
||||||
|
netMetric.BytesRecv = netIO[0].BytesRecv
|
||||||
|
}
|
||||||
|
|
||||||
|
return Metrics{
|
||||||
|
Hostname: hostname,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
CPU: CPUMetric{
|
||||||
|
UsagePercent: cpuUsage,
|
||||||
|
Cores: runtime.NumCPU(),
|
||||||
|
},
|
||||||
|
Memory: MemMetric{
|
||||||
|
Total: memInfo.Total,
|
||||||
|
Used: memInfo.Used,
|
||||||
|
UsedPercent: memInfo.UsedPercent,
|
||||||
|
},
|
||||||
|
Disk: disks,
|
||||||
|
Network: netMetric,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendMetrics(serverURL, apiKey string, metrics Metrics) {
|
||||||
|
data, _ := json.Marshal(metrics)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", serverURL+"/api/v1/metrics", bytes.NewBuffer(data))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error sending metrics: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
log.Printf("✓ Metrics sent: CPU=%.1f%% MEM=%.1f%%",
|
||||||
|
metrics.CPU.UsagePercent, metrics.Memory.UsedPercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
cmd/server/main.go
Normal file
65
cmd/server/main.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
AppName: "OPHION Observability Platform",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.Use(logger.New())
|
||||||
|
app.Use(cors.New())
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.Get("/health", func(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "ophion",
|
||||||
|
"version": "0.1.0",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// API routes
|
||||||
|
api := app.Group("/api/v1")
|
||||||
|
api.Get("/metrics", getMetrics)
|
||||||
|
api.Post("/metrics", ingestMetrics)
|
||||||
|
api.Get("/logs", getLogs)
|
||||||
|
api.Post("/logs", ingestLogs)
|
||||||
|
api.Get("/alerts", getAlerts)
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("🐍 OPHION starting on port %s", port)
|
||||||
|
log.Fatal(app.Listen(":" + port))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMetrics(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{"metrics": []string{}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingestMetrics(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{"status": "received"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogs(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{"logs": []string{}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingestLogs(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{"status": "received"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAlerts(c *fiber.Ctx) error {
|
||||||
|
return c.JSON(fiber.Map{"alerts": []string{}})
|
||||||
|
}
|
||||||
13
deploy/docker/Dockerfile.server
Normal file
13
deploy/docker/Dockerfile.server
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM golang:1.22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o ophion-server ./cmd/server
|
||||||
|
|
||||||
|
FROM alpine:3.19
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/ophion-server .
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["./ophion-server"]
|
||||||
58
deploy/docker/docker-compose.yml
Normal file
58
deploy/docker/docker-compose.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
ophion-server:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/Dockerfile.server
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgres://ophion:ophion@postgres:5432/ophion
|
||||||
|
- CLICKHOUSE_URL=clickhouse://clickhouse:9000/ophion
|
||||||
|
- REDIS_URL=redis://redis:6379
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-change-me-in-production}
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- clickhouse
|
||||||
|
- redis
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ophion-web:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/Dockerfile.web
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- API_URL=http://ophion-server:8080
|
||||||
|
depends_on:
|
||||||
|
- ophion-server
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=ophion
|
||||||
|
- POSTGRES_PASSWORD=ophion
|
||||||
|
- POSTGRES_DB=ophion
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
image: clickhouse/clickhouse-server:24.1
|
||||||
|
volumes:
|
||||||
|
- clickhouse_data:/var/lib/clickhouse
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
clickhouse_data:
|
||||||
|
redis_data:
|
||||||
9
go.mod
Normal file
9
go.mod
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module github.com/bigtux/ophion
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gofiber/fiber/v2 v2.52.0
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.1
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
|
)
|
||||||
49
install.sh
Executable file
49
install.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🐍 OPHION - Observability Platform Installer"
|
||||||
|
echo "============================================="
|
||||||
|
|
||||||
|
# Check Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo "❌ Docker not found. Installing..."
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Docker Compose
|
||||||
|
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||||
|
echo "❌ Docker Compose not found. Please install it."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create directory
|
||||||
|
INSTALL_DIR="${OPHION_DIR:-/opt/ophion}"
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
cd "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# Download docker-compose
|
||||||
|
echo "📥 Downloading OPHION..."
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/bigtux/ophion/main/deploy/docker/docker-compose.yml -o docker-compose.yml
|
||||||
|
|
||||||
|
# Generate secrets
|
||||||
|
JWT_SECRET=$(openssl rand -hex 32)
|
||||||
|
echo "JWT_SECRET=$JWT_SECRET" > .env
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
echo "🚀 Starting OPHION..."
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ OPHION installed successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "📊 Dashboard: http://localhost:3000"
|
||||||
|
echo "🔌 API: http://localhost:8080"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Open http://localhost:3000 in your browser"
|
||||||
|
echo "2. Create your admin account"
|
||||||
|
echo "3. Add your first server with the agent"
|
||||||
|
echo ""
|
||||||
|
echo "To install the agent on a server:"
|
||||||
|
echo " curl -fsSL https://get.ophion.io/agent | bash"
|
||||||
|
echo ""
|
||||||
27
internal/api/ratelimit.go
Normal file
27
internal/api/ratelimit.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RateLimitMiddleware() fiber.Handler {
|
||||||
|
return limiter.New(limiter.Config{
|
||||||
|
Max: 100, // 100 requests
|
||||||
|
Expiration: 1 * time.Minute, // per minute
|
||||||
|
KeyGenerator: func(c *fiber.Ctx) string {
|
||||||
|
// Use API key or IP for rate limiting
|
||||||
|
if key := c.Locals("api_key"); key != nil {
|
||||||
|
return key.(string)
|
||||||
|
}
|
||||||
|
return c.IP()
|
||||||
|
},
|
||||||
|
LimitReached: func(c *fiber.Ctx) error {
|
||||||
|
return c.Status(429).JSON(fiber.Map{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
93
internal/auth/middleware.go
Normal file
93
internal/auth/middleware.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jwtSecret []byte
|
||||||
|
|
||||||
|
func Init(secret string) {
|
||||||
|
jwtSecret = []byte(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAPIKey creates a new API key for agents
|
||||||
|
func GenerateAPIKey() string {
|
||||||
|
bytes := make([]byte, 32)
|
||||||
|
rand.Read(bytes)
|
||||||
|
return "ophion_" + hex.EncodeToString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateJWT creates a JWT token for users
|
||||||
|
func GenerateJWT(userID string, email string) (string, error) {
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"sub": userID,
|
||||||
|
"email": email,
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString(jwtSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateJWT validates a JWT token
|
||||||
|
func ValidateJWT(tokenString string) (*jwt.MapClaims, error) {
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return jwtSecret, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil || !token.Valid {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return nil, jwt.ErrInvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return &claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMiddleware protects routes
|
||||||
|
func AuthMiddleware() fiber.Handler {
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
authHeader := c.Get("Authorization")
|
||||||
|
|
||||||
|
if authHeader == "" {
|
||||||
|
return c.Status(401).JSON(fiber.Map{
|
||||||
|
"error": "Missing authorization header",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support both "Bearer <token>" and API keys
|
||||||
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
|
||||||
|
// Check if it's an API key
|
||||||
|
if strings.HasPrefix(token, "ophion_") {
|
||||||
|
// TODO: Validate API key against database
|
||||||
|
c.Locals("auth_type", "api_key")
|
||||||
|
c.Locals("api_key", token)
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate JWT
|
||||||
|
claims, err := ValidateJWT(token)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(401).JSON(fiber.Map{
|
||||||
|
"error": "Invalid token",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals("auth_type", "jwt")
|
||||||
|
c.Locals("user_id", (*claims)["sub"])
|
||||||
|
c.Locals("email", (*claims)["email"])
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user