Initial commit: Project foundation

- Backend: Go API server with Gin framework
- Frontend: React setup (placeholder)
- ML Service: Python FastAPI skeleton
- Docker Compose: Full stack configuration
- Database: PostgreSQL schema with migrations
- Documentation: Implementation plan and README

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-02-05 00:44:11 +03:00
commit 7043429150
24 changed files with 2359 additions and 0 deletions

23
.env.example Normal file
View File

@@ -0,0 +1,23 @@
# Database Configuration
DB_HOST=postgres
DB_PORT=5432
DB_NAME=vlogs_manager
DB_USER=vlogs
DB_PASSWORD=your_secure_password_here
# JWT Configuration
JWT_SECRET=your_jwt_secret_key_at_least_32_characters_long
# VictoriaLogs Configuration
VLOGS_URL=http://victorialogs:9428
# vmalert Configuration
VMALERT_URL=http://vmalert:8880
VMALERT_RULES_DIR=/app/rules
# ML Service Configuration
ML_SERVICE_URL=http://ml-service:8000
# Application Configuration
ENV=development
LOG_LEVEL=info

20
.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Environment variables
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Temporary files
*.tmp
tmp/

1159
IMPLEMENTATION_PLAN.md Normal file

File diff suppressed because it is too large Load Diff

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# VictoriaLogs Manager
A comprehensive web application for managing VictoriaLogs with alerting, advanced reporting, and malicious pattern detection.
## Features
- **Log Exploration**: Query and search logs using LogsQL with visual query builder
- **Alert Management**: Create and manage vmalert rules with YAML editor
- **Pattern Detection**: Find malicious patterns using regex and ML-based anomaly detection
- **Reports & Dashboards**: Fancy visualizations and custom reports
- **Role-Based Access**: Admin, Editor, Analyst, and Viewer roles
## Tech Stack
- **Backend**: Go (Gin framework)
- **Frontend**: React + TypeScript + Vite
- **Database**: PostgreSQL
- **ML Service**: Python (FastAPI + scikit-learn)
- **Deployment**: Docker Compose
## Project Structure
```
victorialogs-manager/
├── backend/ # Go API server
├── frontend/ # React SPA
├── ml-service/ # Python ML service
└── docker-compose.yml
```
## Getting Started
### Prerequisites
- Go 1.21+
- Node.js 18+
- Python 3.11+
- Docker & Docker Compose
### Quick Start with Docker
```bash
# Copy environment file
cp .env.example .env
# Edit .env with your configuration
nano .env
# Start all services
docker-compose up -d
```
The application will be available at:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8080
- VictoriaLogs: http://localhost:9428
### Development Setup
#### Backend
```bash
cd backend
go mod download
go run cmd/server/main.go
```
#### Frontend
```bash
cd frontend
npm install
npm run dev
```
#### ML Service
```bash
cd ml-service
pip install -r requirements.txt
uvicorn app.main:app --reload
```
## Configuration
See `.env.example` for all available configuration options.
## Documentation
- [API Documentation](./docs/api.md) (coming soon)
- [User Guide](./docs/user-guide.md) (coming soon)
- [Deployment Guide](./docs/deployment.md) (coming soon)
## License
MIT

25
alertmanager.yml Normal file
View File

@@ -0,0 +1,25 @@
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'cluster', 'service']
group_wait: 10s
group_interval: 10s
repeat_interval: 12h
receiver: 'default'
receivers:
- name: 'default'
# Configure your notification channels here
# Examples: email, slack, webhook, etc.
# For now, this is a basic configuration
webhook_configs:
- url: 'http://backend:8080/api/v1/alerts/webhook'
send_resolved: true
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'cluster', 'service']

22
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# Go
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work
# Binary
/bin/
/build/
server
# Vendor
vendor/
# vmalert rules (generated)
configs/vmalert-rules/*.yml
configs/vmalert-rules/*.yaml
!configs/vmalert-rules/.gitkeep

40
backend/Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
# Build stage
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server
# Final stage
FROM alpine:latest
# Install ca-certificates for HTTPS
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Copy binary from builder
COPY --from=builder /app/server .
# Copy configuration
COPY --from=builder /app/configs ./configs
# Expose port
EXPOSE 8080
# Run the application
CMD ["./server"]

View File

@@ -0,0 +1,39 @@
server:
port: 8080
read_timeout: 30s
write_timeout: 30s
database:
host: ${DB_HOST:postgres}
port: ${DB_PORT:5432}
name: ${DB_NAME:vlogs_manager}
user: ${DB_USER:vlogs}
password: ${DB_PASSWORD}
max_connections: 25
victorialogs:
url: ${VLOGS_URL:http://victorialogs:9428}
timeout: 30s
max_query_size: 10000
vmalert:
url: ${VMALERT_URL:http://vmalert:8880}
rules_dir: ${VMALERT_RULES_DIR:/app/rules}
reload_endpoint: "/-/reload"
ml_service:
url: ${ML_SERVICE_URL:http://ml-service:8000}
timeout: 60s
auth:
jwt_secret: ${JWT_SECRET}
access_token_duration: 15m
refresh_token_duration: 168h
rate_limiting:
enabled: true
requests_per_minute:
admin: 1000
editor: 500
analyst: 300
viewer: 200

View File

12
backend/go.mod Normal file
View File

@@ -0,0 +1,12 @@
module github.com/yourusername/victorialogs-manager
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.18.0
github.com/joho/godotenv v1.5.1
gopkg.in/yaml.v3 v3.0.1
)

View File

@@ -0,0 +1,186 @@
package api
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yourusername/victorialogs-manager/internal/config"
)
// NewRouter creates and configures the main router
func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
// Set Gin mode
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// CORS middleware
router.Use(corsMiddleware())
// Health check endpoint
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"service": "victorialogs-manager",
})
})
// API v1 routes
v1 := router.Group("/api/v1")
{
// Authentication routes (public)
auth := v1.Group("/auth")
{
auth.POST("/login", func(c *gin.Context) {
// TODO: Implement login handler
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
auth.POST("/refresh", func(c *gin.Context) {
// TODO: Implement refresh handler
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
auth.GET("/me", func(c *gin.Context) {
// TODO: Implement me handler (requires auth)
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
}
// Protected routes (require authentication)
// TODO: Add auth middleware here
// User management routes
users := v1.Group("/users")
{
users.GET("", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
users.POST("", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
users.GET("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
users.PUT("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
users.DELETE("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
}
// Log querying routes
logs := v1.Group("/logs")
{
logs.POST("/query", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
logs.POST("/tail", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
logs.GET("/facets", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
logs.POST("/stats", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
logs.POST("/export", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
}
// Alert management routes
alerts := v1.Group("/alerts")
{
alerts.GET("/rules", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
alerts.POST("/rules", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
alerts.GET("/rules/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
alerts.PUT("/rules/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
alerts.DELETE("/rules/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
alerts.POST("/reload", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
alerts.GET("/active", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
}
// Pattern detection routes
patterns := v1.Group("/patterns")
{
patterns.GET("", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
patterns.POST("", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
patterns.PUT("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
patterns.DELETE("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
patterns.POST("/detect", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
patterns.POST("/ml/train", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
patterns.POST("/ml/detect", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
}
// Report routes
reports := v1.Group("/reports")
{
reports.GET("", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
reports.POST("", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
reports.GET("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
reports.PUT("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
reports.DELETE("/:id", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
reports.POST("/:id/execute", func(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
})
}
}
return router
}
// corsMiddleware handles CORS headers
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

View File

@@ -0,0 +1,106 @@
package config
import (
"fmt"
"os"
"time"
"gopkg.in/yaml.v3"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
VictoriaLogs VictoriaLogsConfig `yaml:"victorialogs"`
VMAlert VMAlertConfig `yaml:"vmalert"`
MLService MLServiceConfig `yaml:"ml_service"`
Auth AuthConfig `yaml:"auth"`
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
}
type ServerConfig struct {
Port int `yaml:"port"`
ReadTimeout time.Duration `yaml:"read_timeout"`
WriteTimeout time.Duration `yaml:"write_timeout"`
}
type DatabaseConfig struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Name string `yaml:"name"`
User string `yaml:"user"`
Password string `yaml:"password"`
MaxConnections int `yaml:"max_connections"`
}
type VictoriaLogsConfig struct {
URL string `yaml:"url"`
Timeout time.Duration `yaml:"timeout"`
MaxQuerySize int `yaml:"max_query_size"`
}
type VMAlertConfig struct {
URL string `yaml:"url"`
RulesDir string `yaml:"rules_dir"`
ReloadEndpoint string `yaml:"reload_endpoint"`
}
type MLServiceConfig struct {
URL string `yaml:"url"`
Timeout time.Duration `yaml:"timeout"`
}
type AuthConfig struct {
JWTSecret string `yaml:"jwt_secret"`
AccessTokenDuration time.Duration `yaml:"access_token_duration"`
RefreshTokenDuration time.Duration `yaml:"refresh_token_duration"`
}
type RateLimitingConfig struct {
Enabled bool `yaml:"enabled"`
RequestsPerMinute map[string]int `yaml:"requests_per_minute"`
}
// Load loads configuration from YAML file and environment variables
func Load(configPath string) (*Config, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Expand environment variables in config
expandedData := []byte(os.ExpandEnv(string(data)))
var cfg Config
if err := yaml.Unmarshal(expandedData, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
// Validate required fields
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("config validation failed: %w", err)
}
return &cfg, nil
}
func (c *Config) validate() error {
if c.Database.Password == "" {
return fmt.Errorf("database password is required")
}
if c.Auth.JWTSecret == "" {
return fmt.Errorf("JWT secret is required")
}
if len(c.Auth.JWTSecret) < 32 {
return fmt.Errorf("JWT secret must be at least 32 characters")
}
return nil
}
// GetDSN returns the PostgreSQL connection string
func (c *DatabaseConfig) GetDSN() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
c.Host, c.Port, c.User, c.Password, c.Name,
)
}

View File

@@ -0,0 +1,78 @@
package models
import "time"
// Role represents user roles
type Role string
const (
RoleAdmin Role = "admin"
RoleEditor Role = "editor"
RoleAnalyst Role = "analyst"
RoleViewer Role = "viewer"
)
// Permission represents a specific permission
type Permission string
const (
PermViewLogs Permission = "logs:view"
PermExportLogs Permission = "logs:export"
PermManageAlerts Permission = "alerts:manage"
PermViewAlerts Permission = "alerts:view"
PermManagePatterns Permission = "patterns:manage"
PermRunML Permission = "patterns:ml"
PermManageReports Permission = "reports:manage"
PermManageUsers Permission = "users:manage"
)
// RolePermissions maps roles to their permissions
var RolePermissions = map[Role][]Permission{
RoleAdmin: {
PermViewLogs, PermExportLogs, PermManageAlerts,
PermViewAlerts, PermManagePatterns, PermRunML,
PermManageReports, PermManageUsers,
},
RoleEditor: {
PermViewLogs, PermExportLogs, PermManageAlerts,
PermViewAlerts, PermManagePatterns, PermManageReports,
},
RoleAnalyst: {
PermViewLogs, PermViewAlerts, PermManagePatterns, PermRunML,
},
RoleViewer: {
PermViewLogs, PermViewAlerts,
},
}
// User represents a user in the system
type User struct {
ID string `json:"id" db:"id"`
Username string `json:"username" db:"username"`
Email string `json:"email" db:"email"`
PasswordHash string `json:"-" db:"password_hash"`
Role Role `json:"role" db:"role"`
IsActive bool `json:"is_active" db:"is_active"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// HasPermission checks if the user has a specific permission
func (u *User) HasPermission(perm Permission) bool {
permissions, ok := RolePermissions[u.Role]
if !ok {
return false
}
for _, p := range permissions {
if p == perm {
return true
}
}
return false
}
// GetPermissions returns all permissions for the user's role
func (u *User) GetPermissions() []Permission {
return RolePermissions[u.Role]
}

View File

@@ -0,0 +1,132 @@
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL CHECK (role IN ('admin', 'editor', 'analyst', 'viewer')),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create index on username and email for faster lookups
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
-- Create alert_rules table
CREATE TABLE IF NOT EXISTS alert_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
group_name VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL CHECK (type IN ('alert', 'record')),
expression TEXT NOT NULL,
duration VARCHAR(50),
labels JSONB DEFAULT '{}',
annotations JSONB DEFAULT '{}',
severity VARCHAR(50),
enabled BOOLEAN DEFAULT true,
yaml_content TEXT NOT NULL,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for alert_rules
CREATE INDEX idx_alert_rules_group_name ON alert_rules(group_name);
CREATE INDEX idx_alert_rules_enabled ON alert_rules(enabled);
CREATE INDEX idx_alert_rules_created_by ON alert_rules(created_by);
-- Create patterns table
CREATE TABLE IF NOT EXISTS patterns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL CHECK (type IN ('regex', 'ml')),
description TEXT,
config JSONB NOT NULL,
severity VARCHAR(50),
tags TEXT[],
enabled BOOLEAN DEFAULT true,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for patterns
CREATE INDEX idx_patterns_type ON patterns(type);
CREATE INDEX idx_patterns_enabled ON patterns(enabled);
CREATE INDEX idx_patterns_created_by ON patterns(created_by);
-- Create reports table
CREATE TABLE IF NOT EXISTS reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
config JSONB NOT NULL,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create index for reports
CREATE INDEX idx_reports_created_by ON reports(created_by);
-- Create audit_logs table for tracking sensitive operations
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(255) NOT NULL,
resource_type VARCHAR(100),
resource_id UUID,
details JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes for audit_logs
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
-- Create function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create triggers for updated_at
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_alert_rules_updated_at BEFORE UPDATE ON alert_rules
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_patterns_updated_at BEFORE UPDATE ON patterns
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_reports_updated_at BEFORE UPDATE ON reports
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Insert default admin user (password: admin123 - CHANGE IN PRODUCTION!)
-- Password hash is bcrypt hash of "admin123"
INSERT INTO users (username, email, password_hash, role)
VALUES (
'admin',
'admin@example.com',
'$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5lW.HKCFcVpSK',
'admin'
) ON CONFLICT (username) DO NOTHING;
-- Insert sample viewer user (password: viewer123)
INSERT INTO users (username, email, password_hash, role)
VALUES (
'viewer',
'viewer@example.com',
'$2a$12$3pYKzLw9z1FqN8QdKxI.J.nVZY5TqJxM7Y.qQHkZKZhzJ8qQnPZ8W',
'viewer'
) ON CONFLICT (username) DO NOTHING;

152
docker-compose.yml Normal file
View File

@@ -0,0 +1,152 @@
version: '3.8'
services:
# VictoriaLogs - Log database
victorialogs:
image: victoriametrics/victoria-logs:latest
container_name: victorialogs
ports:
- "9428:9428"
volumes:
- vlogs-data:/victoria-logs-data
command:
- "-storageDataPath=/victoria-logs-data"
- "-retentionPeriod=30d"
networks:
- vlogs-network
restart: unless-stopped
# vmalert - Alerting engine
vmalert:
image: victoriametrics/vmalert:latest
container_name: vmalert
ports:
- "8880:8880"
volumes:
- vmalert-rules:/etc/vmalert/rules
command:
- "-datasource.url=http://victorialogs:9428"
- "-rule=/etc/vmalert/rules/*.yml"
- "-notifier.url=http://alertmanager:9093"
depends_on:
- victorialogs
- alertmanager
networks:
- vlogs-network
restart: unless-stopped
# Alertmanager - Alert notifications
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
- alertmanager-data:/alertmanager
command:
- "--config.file=/etc/alertmanager/alertmanager.yml"
- "--storage.path=/alertmanager"
networks:
- vlogs-network
restart: unless-stopped
# PostgreSQL - Application database
postgres:
image: postgres:15-alpine
container_name: postgres
environment:
POSTGRES_DB: ${DB_NAME:-vlogs_manager}
POSTGRES_USER: ${DB_USER:-vlogs}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./backend/migrations:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
networks:
- vlogs-network
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-vlogs}"]
interval: 10s
timeout: 5s
retries: 5
# Go Backend API
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: backend
ports:
- "8080:8080"
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: ${DB_NAME:-vlogs_manager}
DB_USER: ${DB_USER:-vlogs}
DB_PASSWORD: ${DB_PASSWORD}
VLOGS_URL: http://victorialogs:9428
VMALERT_URL: http://vmalert:8880
ML_SERVICE_URL: http://ml-service:8000
JWT_SECRET: ${JWT_SECRET}
VMALERT_RULES_DIR: /app/rules
volumes:
- vmalert-rules:/app/rules
depends_on:
postgres:
condition: service_healthy
victorialogs:
condition: service_started
vmalert:
condition: service_started
ml-service:
condition: service_started
networks:
- vlogs-network
restart: unless-stopped
# Python ML Service
ml-service:
build:
context: ./ml-service
dockerfile: Dockerfile
container_name: ml-service
ports:
- "8000:8000"
environment:
VLOGS_URL: http://victorialogs:9428
MODEL_DIR: /app/models
volumes:
- ml-models:/app/models
networks:
- vlogs-network
restart: unless-stopped
# React Frontend
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
VITE_API_BASE_URL: http://localhost:8080/api/v1
container_name: frontend
ports:
- "3000:80"
depends_on:
- backend
networks:
- vlogs-network
restart: unless-stopped
volumes:
vlogs-data:
postgres-data:
vmalert-rules:
ml-models:
alertmanager-data:
networks:
vlogs-network:
driver: bridge

25
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Build
dist/
build/
.vite/
# Cache
.cache/
*.tsbuildinfo
# Environment
.env.local
.env.development.local
.env.test.local
.env.production.local
# Debug logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

34
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build argument for API URL
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
# Build the application
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built assets from builder
COPY --from=builder /app/dist /usr/share/nginx/html
# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

28
frontend/nginx.conf Normal file
View File

@@ -0,0 +1,28 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# SPA routing - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

26
ml-service/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
# Virtual environment
venv/
env/
ENV/
# ML models
models/*.pkl
models/*.joblib
!models/.gitkeep
# Jupyter
.ipynb_checkpoints/
# Cache
.pytest_cache/
.mypy_cache/

27
ml-service/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements file
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app/ ./app/
# Create models directory
RUN mkdir -p models
# Expose port
EXPOSE 8000
# Run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1 @@
# ML Service Package

122
ml-service/app/main.py Normal file
View File

@@ -0,0 +1,122 @@
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="VictoriaLogs ML Service",
description="Machine Learning service for anomaly detection in logs",
version="1.0.0"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Request/Response models
class TrainRequest(BaseModel):
model_name: str
query: str
start_time: str
end_time: str
contamination: float = 0.1
class DetectRequest(BaseModel):
model_name: str
logs: List[Dict[str, Any]]
class TrainResponse(BaseModel):
status: str
model_name: str
model_path: str
message: str
class DetectResponse(BaseModel):
status: str
anomalies: List[Dict[str, Any]]
anomaly_count: int
@app.get("/")
async def root():
return {
"service": "VictoriaLogs ML Service",
"status": "running",
"version": "1.0.0"
}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@app.post("/api/ml/train", response_model=TrainResponse)
async def train_model(request: TrainRequest):
"""
Train an anomaly detection model on historical logs
"""
logger.info(f"Training model: {request.model_name}")
# TODO: Implement model training
# 1. Fetch logs from VictoriaLogs
# 2. Extract features
# 3. Train Isolation Forest model
# 4. Save model to disk
return TrainResponse(
status="success",
model_name=request.model_name,
model_path=f"/app/models/{request.model_name}.pkl",
message="Model training not yet implemented"
)
@app.post("/api/ml/detect", response_model=DetectResponse)
async def detect_anomalies(request: DetectRequest):
"""
Detect anomalies in a batch of logs using a trained model
"""
logger.info(f"Detecting anomalies with model: {request.model_name}")
# TODO: Implement anomaly detection
# 1. Load trained model
# 2. Extract features from logs
# 3. Predict anomalies
# 4. Return flagged logs with scores
return DetectResponse(
status="success",
anomalies=[],
anomaly_count=0
)
@app.get("/api/ml/models")
async def list_models():
"""
List all trained models
"""
# TODO: List model files from disk
return {
"models": [],
"count": 0
}
@app.delete("/api/ml/models/{model_name}")
async def delete_model(model_name: str):
"""
Delete a trained model
"""
logger.info(f"Deleting model: {model_name}")
# TODO: Delete model file
return {
"status": "success",
"message": f"Model {model_name} deleted"
}

View File

View File

@@ -0,0 +1,9 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
scikit-learn==1.4.0
pandas==2.2.0
numpy==1.26.3
httpx==0.26.0
python-multipart==0.0.6
joblib==1.3.2