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:
2026-02-05 21:35:47 -03:00
parent 268ff690df
commit 5b662cf12f
8 changed files with 449 additions and 0 deletions

135
cmd/agent/main.go Normal file
View 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
View 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{}})
}

View 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"]

View 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
View 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
View 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
View 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",
})
},
})
}

View 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()
}
}