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