first commit
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Secrets (CRITICAL: Do not bake .env files into the image!)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*~
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# IDEs / Editor files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
26
.env.example
Normal file
26
.env.example
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# --- AI Configuration ---
|
||||||
|
OLLAMA_API_URL=http://localhost:11434/api/generate
|
||||||
|
OLLAMA_MODEL=llama3.2:1b
|
||||||
|
AI_SYSTEM_PROMPT="You are a Linux Sysadmin. Look at these server error logs from yesterday. Summarize the main issues, identify any potential attacks (like failed brute force logins), and ignore harmless background noise. Keep it brief."
|
||||||
|
|
||||||
|
# --- Email Reporting Configuration ---
|
||||||
|
REPORT_TO_EMAIL=admin@yourdomain.com
|
||||||
|
REPORT_FROM_EMAIL=system@yourdomain.com
|
||||||
|
SMTP_SERVER=mail.yourdomain.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASS=
|
||||||
|
|
||||||
|
# --- Log Analysis Configuration ---
|
||||||
|
JOURNAL_UNIT=mailserver
|
||||||
|
MAX_ERRORS_TO_ANALYZE=50
|
||||||
|
# Comma-separated list of words that trigger the AI
|
||||||
|
ERROR_KEYWORDS=error,failed,warning,fatal,timeout
|
||||||
|
|
||||||
|
# --- Execution Options ---
|
||||||
|
# Set to 'true' to see detailed background processes, 'false' for clean logs
|
||||||
|
DEBUG=true
|
||||||
|
# Set to 'true' to run also immediately when the container starts, 'false' to wait for the schedule
|
||||||
|
RUN_NOW=true
|
||||||
|
# Time format must be HH:MM in 24-hour format
|
||||||
|
REPORT_TIME=08:00
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Install systemd so the container has the 'journalctl' command available
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y systemd && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy our script
|
||||||
|
COPY daily_summary.py .
|
||||||
|
|
||||||
|
# Run Python in unbuffered mode (-u) so we can see logs in real-time via 'docker logs'
|
||||||
|
CMD ["python", "-u", "daily_summary.py"]
|
||||||
166
daily_summary.py
Normal file
166
daily_summary.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import smtplib
|
||||||
|
from email.message import EmailMessage
|
||||||
|
import time
|
||||||
|
import schedule
|
||||||
|
|
||||||
|
# --- CONFIGURATION (Loaded from environment variables) ---
|
||||||
|
OLLAMA_API = os.getenv("OLLAMA_API_URL", "http://localhost:11434/api/generate")
|
||||||
|
MODEL = os.getenv("OLLAMA_MODEL", "llama3.2:1b")
|
||||||
|
AI_SYSTEM_PROMPT = os.getenv("AI_SYSTEM_PROMPT", "You are a Linux Sysadmin. Summarize these logs briefly.")
|
||||||
|
|
||||||
|
MY_EMAIL = os.getenv("REPORT_TO_EMAIL")
|
||||||
|
FROM_EMAIL = os.getenv("REPORT_FROM_EMAIL")
|
||||||
|
SMTP_SERVER = os.getenv("SMTP_SERVER", "127.0.0.1")
|
||||||
|
SMTP_PORT = int(os.getenv("SMTP_PORT", 25))
|
||||||
|
SMTP_USER = os.getenv("SMTP_USER", "")
|
||||||
|
SMTP_PASS = os.getenv("SMTP_PASS", "")
|
||||||
|
|
||||||
|
JOURNAL_UNIT = os.getenv("JOURNAL_UNIT", "mailserver")
|
||||||
|
MAX_ERRORS = int(os.getenv("MAX_ERRORS_TO_ANALYZE", 50))
|
||||||
|
|
||||||
|
# Parse comma-separated keywords into a list
|
||||||
|
keyword_string = os.getenv("ERROR_KEYWORDS", "error,failed,warning,fatal,timeout")
|
||||||
|
KEYWORDS = [k.strip().lower() for k in keyword_string.split(',')]
|
||||||
|
|
||||||
|
# --- EXECUTION TOGGLES ---
|
||||||
|
DEBUG = os.getenv("DEBUG", "false").lower() in ("true", "1", "yes")
|
||||||
|
RUN_NOW = os.getenv("RUN_NOW", "false").lower() in ("true", "1", "yes")
|
||||||
|
REPORT_TIME = os.getenv("REPORT_TIME", "08:00")
|
||||||
|
|
||||||
|
def debug_print(message):
|
||||||
|
"""Prints only if DEBUG=true in .env"""
|
||||||
|
if DEBUG:
|
||||||
|
print(f"[DEBUG] {message}")
|
||||||
|
|
||||||
|
def get_yesterdays_logs():
|
||||||
|
print(f"Fetching logs for '{JOURNAL_UNIT}' from yesterday...")
|
||||||
|
debug_print(f"Running command: journalctl -u {JOURNAL_UNIT} --since yesterday --until today")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
['journalctl', '-u', JOURNAL_UNIT, '--since', 'yesterday', '--until', 'today'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
lines = result.stdout.splitlines()
|
||||||
|
debug_print(f"Fetched {len(lines)} total lines from journalctl.")
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def process_logs(logs):
|
||||||
|
total_lines = len(logs)
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
debug_print(f"Filtering logs using keywords: {KEYWORDS}")
|
||||||
|
for line in logs:
|
||||||
|
line_lower = line.lower()
|
||||||
|
if any(keyword in line_lower for keyword in KEYWORDS):
|
||||||
|
errors.append(line)
|
||||||
|
|
||||||
|
debug_print(f"Found {len(errors)} matching lines before truncation.")
|
||||||
|
|
||||||
|
# Truncate to maximum allowed errors to save CPU time
|
||||||
|
errors = errors[-MAX_ERRORS:]
|
||||||
|
debug_print(f"Truncated to {len(errors)} lines for AI processing (MAX_ERRORS={MAX_ERRORS}).")
|
||||||
|
return total_lines, errors
|
||||||
|
|
||||||
|
def get_ai_summary(errors):
|
||||||
|
if not errors:
|
||||||
|
debug_print("Skipping AI API call because error list is empty.")
|
||||||
|
return "No errors or warnings were found in the logs yesterday. Everything looks healthy!"
|
||||||
|
|
||||||
|
print(f"Asking AI to summarize {len(errors)} errors... (This will take a few minutes)")
|
||||||
|
|
||||||
|
error_text = "\n".join(errors)
|
||||||
|
|
||||||
|
# Combine the system prompt from .env with the logs
|
||||||
|
prompt = f"{AI_SYSTEM_PROMPT}\n\nLOGS:\n{error_text}"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": MODEL,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_print(f"Sending prompt to Ollama at {OLLAMA_API} using model '{MODEL}'")
|
||||||
|
debug_print(f"System Prompt used: {AI_SYSTEM_PROMPT}")
|
||||||
|
debug_print(f"Total payload length: {len(prompt)} characters.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(OLLAMA_API, json=payload)
|
||||||
|
debug_print(f"AI response status code: {response.status_code}")
|
||||||
|
return response.json().get('response', 'Error: AI returned empty response.')
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"AI Connection Exception: {str(e)}")
|
||||||
|
return f"Failed to connect to local AI: {str(e)}"
|
||||||
|
|
||||||
|
def send_email_report(total_traffic, ai_summary):
|
||||||
|
if not MY_EMAIL or not FROM_EMAIL:
|
||||||
|
print("Skipping email: REPORT_TO_EMAIL or REPORT_FROM_EMAIL not set.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Sending email report to {MY_EMAIL}...")
|
||||||
|
debug_print(f"SMTP Server: {SMTP_SERVER}:{SMTP_PORT}")
|
||||||
|
debug_print(f"SMTP Auth User: {'<configured>' if SMTP_USER else '<none>'}")
|
||||||
|
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg['Subject'] = f"Daily AI Summary: {JOURNAL_UNIT}"
|
||||||
|
msg['From'] = FROM_EMAIL
|
||||||
|
msg['To'] = MY_EMAIL
|
||||||
|
|
||||||
|
body = (
|
||||||
|
f"DAILY SERVER REPORT: {JOURNAL_UNIT}\n"
|
||||||
|
f"----------------------------------------\n"
|
||||||
|
f"Total Log Lines Processed: {total_traffic}\n\n"
|
||||||
|
f"AI Analysis of Errors/Warnings:\n"
|
||||||
|
f"{ai_summary}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
msg.set_content(body)
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
|
||||||
|
if SMTP_USER:
|
||||||
|
debug_print("Attempting STARTTLS and Login...")
|
||||||
|
server.starttls()
|
||||||
|
server.login(SMTP_USER, SMTP_PASS)
|
||||||
|
|
||||||
|
debug_print("Transmitting email payload...")
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
print("Email sent successfully!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to send email: {e}")
|
||||||
|
debug_print(f"Email Exception: {str(e)}")
|
||||||
|
|
||||||
|
def job():
|
||||||
|
print(f"\n--- Starting log analysis for {JOURNAL_UNIT} ---")
|
||||||
|
logs = get_yesterdays_logs()
|
||||||
|
total_traffic, errors = process_logs(logs)
|
||||||
|
summary = get_ai_summary(errors)
|
||||||
|
send_email_report(total_traffic, summary)
|
||||||
|
print("--- Analysis complete. ---")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Log Analyzer Container starting up...")
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print("[DEBUG] Debug mode is ENABLED.")
|
||||||
|
|
||||||
|
# 1. Register the permanent daily schedule first
|
||||||
|
schedule.every().day.at(REPORT_TIME).do(job)
|
||||||
|
print(f"Job permanently scheduled to run daily at {REPORT_TIME}.")
|
||||||
|
|
||||||
|
# 2. Handle the immediate run if requested
|
||||||
|
if RUN_NOW:
|
||||||
|
print("RUN_NOW is enabled. Executing immediate startup run...")
|
||||||
|
job()
|
||||||
|
print("--- Immediate run finished. Returning to standard schedule. ---")
|
||||||
|
else:
|
||||||
|
print("RUN_NOW is disabled. Skipping immediate run.")
|
||||||
|
|
||||||
|
# 3. Enter the permanent background loop
|
||||||
|
print(f"Entering background scheduler loop. Waiting for {REPORT_TIME}...")
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(60)
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
schedule
|
||||||
34
virt-daily-ai-summary.service
Normal file
34
virt-daily-ai-summary.service
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=daily-ai-summary (virt-daily-ai-summary)
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service
|
||||||
|
DefaultDependencies=no
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment="HOME=/root"
|
||||||
|
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-daily-ai-summary 2>/dev/null || true'
|
||||||
|
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-daily-ai-summary 2>/dev/null || true'
|
||||||
|
|
||||||
|
ExecStart=/usr/bin/env docker run \
|
||||||
|
--rm \
|
||||||
|
--name=virt-daily-ai-summary \
|
||||||
|
--log-driver=none \
|
||||||
|
--network=host \
|
||||||
|
--env-file=/virt/daily-ai-summary/env \
|
||||||
|
--mount type=bind,src=/var/log/journal,dst=/var/log/journal,ro \
|
||||||
|
--mount type=bind,src=/run/log/journal,dst=/run/log/journal,ro \
|
||||||
|
--mount type=bind,src=/etc/machine-id,dst=/etc/machine-id,ro \
|
||||||
|
--mount type=bind,src=/etc/localtime,dst=/etc/localtime,ro \
|
||||||
|
--mount type=bind,src=/etc/timezone,dst=/etc/timezone,ro \
|
||||||
|
virt-daily-ai-summary
|
||||||
|
|
||||||
|
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-daily-ai-summary 2>/dev/null || true'
|
||||||
|
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-daily-ai-summary 2>/dev/null || true'
|
||||||
|
|
||||||
|
Restart=always
|
||||||
|
RestartSec=30
|
||||||
|
SyslogIdentifier=virt-daily-ai-summary
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user