diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..707a310 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + groups: + python-dependencies: + patterns: ["*"] + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml deleted file mode 100644 index 5f4ae36..0000000 --- a/.github/workflows/benchmarks.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Performance Benchmarking - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - benchmark: - name: Core Engine Latency Tracking - runs-on: ubuntu-latest - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - - - name: 🐍 Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: 'pip' - - # FIXED STEP: Safely provisions your core package dependencies so imports pass smoothly - - name: πŸ“¦ Install Engine & Profiling Tools - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install pytest pytest-benchmark - - - name: ⏱️ Profile Code Execution Speeds - run: | - # Runs benchmark assertions without breaking over standard warning definitions - pytest --benchmark-only --benchmark-skip || echo "Performance profiles logged successfully." diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index 954819f..0000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Continuous Integration & Telemetry - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - engine-test: - name: Core Engine Analysis Matrix - runs-on: ubuntu-latest - - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - - - name: 🐍 Set up Python Environment - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: 'pip' - - - name: πŸ“¦ Install Telemetry Core Dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install pytest pytest-cov - - - name: πŸ§ͺ Execute Log Parsing Analytics Suite - run: | - # Runs test matrix with tracking to identify untested tracking components - pytest --cov=src --cov-report=term-missing tests/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24e1944..7959776 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,89 @@ -name: Code Quality Assurance +name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - style-compliance: - name: Code Hygiene Enforcement + quality: runs-on: ubuntu-latest - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + - run: python -m pip install --upgrade pip "setuptools>=83" && pip install -e ".[dev]" + - run: ruff format --check . + - run: ruff check . + - run: mypy + - run: pytest + - uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage + path: coverage.xml - - name: 🐍 Set up Python - uses: actions/setup-python@v5 + package-and-container: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.11' - cache: 'pip' + python-version: "3.11" + - run: pip install build + - run: python -m build + - run: docker build --tag logsight-ai:${{ github.sha }} . + - run: docker run --rm logsight-ai:${{ github.sha }} health - - name: πŸ“¦ Provision Linting Binaries - run: | - python -m pip install --upgrade pip - pip install flake8 + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: python -m pip install --upgrade "setuptools>=83" && pip install -e ".[dev]" + - run: bandit -c pyproject.toml -r logsight + - run: pip-audit + - run: pip-licenses --format=json --output-file=licenses.json + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: anchore/sbom-action@v0 + with: + path: . + format: spdx-json + output-file: sbom.spdx.json + - uses: actions/upload-artifact@v4 + with: + name: security-inventory + path: | + sbom.spdx.json + licenses.json - # UPDATED STEP: Explicitly ignores E261 inline comment spacing to clear the final roadblock - - name: πŸš€ Verify PEP 8 Alignment - run: flake8 . --ignore=W293,W291,F401,E302,E305,E701,E203,E501,E261 --exclude=venv,.git,__pycache__,build,dist + benchmark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + - run: pip install -e ".[dev]" + - run: mkdir -p benchmarks && pytest tests/test_benchmarks.py --benchmark-only --benchmark-json=benchmarks/latest.json --no-cov + - uses: actions/upload-artifact@v4 + with: + name: benchmark-${{ github.sha }} + path: benchmarks/latest.json diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..608f7db --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,23 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "17 6 * * 1" + +permissions: + contents: read + security-events: write + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: python + - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/data-validation.yml b/.github/workflows/data-validation.yml deleted file mode 100644 index f4f7097..0000000 --- a/.github/workflows/data-validation.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Ingestion Schema Validation - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - schedule: - - cron: '0 12 * * *' - -jobs: - schema-check: - name: Verify Log Data Contracts - runs-on: ubuntu-latest - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - - - name: 🐍 Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: 'pip' - - - name: πŸ“¦ Install Data Integrity Frameworks - run: | - python -m pip install --upgrade pip - pip install pydantic great_expectations pytest - - # UPDATED STEP: Runs the suite safely and logs the data contract status gracefully - - name: 🧬 Audit Ingestion Model Integrity - run: | - pytest tests/test_schema_contracts.py || pytest tests/ || echo "⚠️ Ingestion Schema: Data contract test file not found. Skipping validation pass." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index c35fb3f..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Automated Release Engineering - -on: - push: - branches: - - main - -jobs: - tagging-engine: - name: Construct Semantic Version Tags - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: 🏷️ Calculate Release Version Alpha - uses: anothrNick/github-tag-action@1.64.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - WITH_V: true - DEFAULT_BUMP: patch diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml deleted file mode 100644 index 9d32529..0000000 --- a/.github/workflows/sast.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: SAST Code Scan - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - bandit-scan: - name: Custom Code Flaw Analysis - runs-on: ubuntu-latest - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - - - name: 🐍 Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: πŸ“¦ Install Bandit Security Analyzer - run: | - pip install bandit - - - name: πŸ” Scan Source Code for Vulnerabilities - # Scans your python directories recursively, ignoring known non-production script paths - run: bandit -r . -ll -ii --exclude ./venv,./tests diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index 6776c9c..0000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Security Analysis - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - security-audit: - name: Dependency & Secret Shield - runs-on: ubuntu-latest - - steps: - - name: ⬇️ Checkout Repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: πŸ›‘οΈ Scan for Exposed System Secrets - uses: trufflesecurity/trufflehog@main - with: - extra_args: --only-verified - - - name: 🐍 Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: πŸ“¦ Evaluate Package Insecurities - run: | - pip install safety - safety check || echo "⚠️ Security Alert: Review reported package vulnerabilities." diff --git a/Dockerfile b/Dockerfile index fc8f80e..287b9e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# ── Stage 1: build ────────────────────────────────────────────────────────── +# Ò”€Ò”€ Stage 1: build Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ FROM python:3.11-slim AS builder WORKDIR /app @@ -13,7 +13,7 @@ COPY logsight/ logsight/ # Build the wheel RUN python -m build --wheel --outdir /dist -# ── Stage 2: runtime ───────────────────────────────────────────────────────── +# Ò”€Ò”€ Stage 2: runtime Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€Ò”€ FROM python:3.11-slim AS runtime LABEL maintainer="Trojan3877" \ @@ -32,5 +32,7 @@ RUN pip install --no-cache-dir /tmp/*.whl && rm /tmp/*.whl # Drop privileges USER logsight +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["logsight", "health"] + ENTRYPOINT ["logsight"] CMD ["--help"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9492925 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Corey Leath + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6faf854..3f7cb3b 100644 --- a/README.md +++ b/README.md @@ -1,142 +1,81 @@ -

- - Build Status - - Python Version - Code Coverage - Code Style - Architecture - State Management - Fault Tolerance - LLM Engine - UI Layer - Type Checking - Security - JSON Parse SLA - Latency -

- -[![Continuous Integration](https://github.com/Trojan3877/LogSight-AI/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/ci-cd.yml) -[![Code Quality Assurance](https://github.com/Trojan3877/LogSight-AI/actions/workflows/ci.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/ci.yml) -[![Security Analysis](https://github.com/Trojan3877/LogSight-AI/actions/workflows/security.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/security.yml) -[![Automated Release](https://github.com/Trojan3877/LogSight-AI/actions/workflows/release.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/release.yml) -[![Continuous Integration](https://github.com/Trojan3877/LogSight-AI/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/ci-cd.yml) -[![Performance Benchmarking](https://github.com/Trojan3877/LogSight-AI/actions/workflows/benchmarks.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/benchmarks.yml) -[![SAST Code Security](https://github.com/Trojan3877/LogSight-AI/actions/workflows/sast.yml/badge.svg)](https://github.com/Trojan3877/LogSight-AI/actions/workflows/sast.yml) - -LogSight-AI: Enterprise Multi-Agent Observability & Telemetry System -LogSight-AI is a fault-tolerant, production-grade AI observability pipeline that ingests chaotic, unstructured system logs, serializes them into predictable data schemas, and conducts structural root-cause analysis. -Unlike basic single-prompt wrapper scripts that suffer from token-burn loops and unpredictable outputs, LogSight-AI implements **Sandipan Bhaumik’s production agent orchestration patterns**: separating execution layers into specialized sub-agents managed via **Immutable State Management** and isolated by an active **Execution Circuit Breaker**. - System Architecture & Data Flow -LogSight-AI decouples specialized skills into dedicated worker layers, enforcing deterministic bounds across the processing lifecycle. -### Deterministic System Execution Flow -The processing lifecycle follows a unidirectional, structured pipeline to ensure predictable inputs and outputs at every layer: +# LogSight-AI + +[![CI](https://github.com/CoreyLeath-code/LogSight-AI/actions/workflows/ci.yml/badge.svg)](https://github.com/CoreyLeath-code/LogSight-AI/actions/workflows/ci.yml) +[![CodeQL](https://github.com/CoreyLeath-code/LogSight-AI/actions/workflows/codeql.yml/badge.svg)](https://github.com/CoreyLeath-code/LogSight-AI/actions/workflows/codeql.yml) +[![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +LogSight-AI is a local-first Python CLI for parsing common log formats, summarizing error patterns, detecting message-length outliers, and locating error-rate spikes. The production package does not transmit logs or require credentials. + +## Architecture + +```mermaid +flowchart LR + A["File or stdin"] --> B["Format parser"] + B --> C["Typed LogEntry records"] + C --> D["Statistics and anomaly analysis"] + D --> E["Rich CLI report"] +``` + +Supported formats include ISO-8601 application logs, syslog, nginx access logs, and generic level-prefixed lines. Detection is an explainable statistical heuristic; it is not a trained model and no accuracy claim is made without a labeled evaluation corpus. + +## Quick start + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -e . +logsight health +logsight analyze application.log +cat application.log | logsight stdin +``` + +Useful controls: + +```bash +logsight analyze application.log --threshold 3.0 --window 200 --spike-threshold 0.20 ``` -[Raw Log Ingestion] - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Log Parser Worker Agent β”‚ ──► Enforces valid JSON Serialization - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Circuit Breaker Guardrail β”‚ ──► Active Evaluation (Validates bounds / loop caps) - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Root-Cause Analysis Worker β”‚ ──► Performs deep infrastructure triage - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Remediation Synthesis Worker β”‚ ──► Generates machine-actionable Runbook SOPs - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό -[Immutable State Output Object] +## Verified metrics + +Measured locally on 2026-07-17; CI artifacts are the canonical per-commit record. + +| Metric | Value | +|---|---:| +| Automated tests | 50 passing | +| Core package coverage | 95.26% | +| Benchmark input | 1,000 lines | +| Median pipeline latency | 10.315 ms | +| Mean throughput | 96.21 runs/sec | +| Approximate line throughput | 96,213 lines/sec | +| Security findings | Pending CI security job | +| Docker image size | Pending CI build | + +Results vary by hardware and Python version. See [Benchmark Guide](docs/BENCHMARKING.md) and [Benchmark Report](benchmarks/benchmark_report.md). + +## Engineering controls + +Every pull request runs formatting, linting, strict type checking, unit/integration/CLI tests, a 90% coverage gate, package and container validation, Bandit, dependency audit, SBOM generation, CodeQL, and a reproducible microbenchmark. Checks fail closed. + +## Documentation + +- [Production audit](docs/AUDIT.md) +- [Architecture](docs/architecture.md) +- [Deployment and rollback checklist](docs/DEPLOYMENT.md) +- [Benchmark methodology](docs/BENCHMARKING.md) +- [Runtime metrics](docs/metrics.md) +- [Security policy](SECURITY.md) + +The Streamlit and external-LLM files are retained as demonstrations and are not part of the supported package or deployment contract; see the audit for the work required to promote them. + +## Development + +```bash +pip install -e ".[dev]" +ruff format . +ruff check . +mypy +pytest ``` -## πŸ“Š Repository Metrics - -Measured from the repository on 2026-07-12. - -| Area | Metric | Current Value | Source | -|---|---:|---:|---| -| Codebase | Tracked files | 33 | `git ls-files` | -| Codebase | Python files | 14 | `*.py` files across app, agents, package, and tests | -| Codebase | Python NCLOC | 846 | Non-empty, non-comment Python lines | -| Package | Core package modules | 4 | `logsight/*.py` | -| Package | Agent modules | 3 | `agents/*.py` | -| Tests | Pytest test cases | 34 passing | `pytest --cov=logsight` | -| Tests | Package coverage | 80% | `pytest-cov` over `logsight` | -| CI/CD | GitHub Actions workflows | 7 | `.github/workflows/*.yml` | -| Dependencies | Runtime dependencies | 5 | `requirements.txt` | -| Dependencies | Development dependencies | 3 | `requirements-dev.txt` | -| Delivery | Runtime assets | 3 | `Dockerfile`, `docker-compose.yml`, `Procfile` | -| Docs | Documentation pages | 3 | `README.md`, `docs/*.md` | - -## ⚑ Operational Metrics - -| Metric | Default / Current Value | Produced By | -|---|---:|---| -| Parsed log entries | Runtime total | `compute_stats()` | -| Error count | `ERROR` + `CRITICAL` entries | `compute_stats()` | -| Warning count | `WARNING` entries | `compute_stats()` | -| Error rate | `error_count / total` | `WindowStats.error_rate` | -| Top messages | Top 10 repeated messages | `compute_stats()` | -| Anomaly threshold | `zscore_threshold = 2.5` | `detect_anomalies()` | -| Spike window size | `window_size = 100` entries | `error_rate_spike()` | -| Spike threshold | `spike_threshold = 0.25` | `error_rate_spike()` | - - - 1. **The Core Four Model:** Every worker is explicitly configured with decoupled system instructions, isolated parameter inputs, structural schema requirements, and a dedicated role context. - 2. **State Immutability:** State changes are achieved by generating explicit copies of the runtime history object, providing a clean trace for deep system observability. - 3. **Fault Isolation:** Malformed strings, JSON parser validation faults, or unhandled exceptions trip defensive thresholds, safely degrading performance to preserve upstream uptime. -System Performance & Operational Benchmarks -The multi-agent design delivers measurable performance improvements over typical single-stage LLM chains when parsing complex telemetry payloads: -| Operational Dimension | Legacy Single-Chain LLM Wrapper | Upgraded Multi-Agent Pipeline | Impact Metric | -|---|---|---|---| -| **JSON Parse Reliability** | 76.4% on malformed logs | 99.8% via isolated worker schemas | **+23.4% Stability** | -| **Average Mitigation Latency** | 8.2 seconds | 3.4 seconds (decoupled processing) | **58.5% Latency Reduction** | -| **Worst-Case Cost Profile** | Infinite loop token burn (Uncapped) | Hard-stopped by active Circuit Breaker | **Predictable Cost Ceiling** | -| **Schema Uniformity** | Deviates under stress conditions | Strict Pydantic type compilation | **Zero Schema Drift** | -## πŸš€ Quick Start Instructions -### Prerequisites - * Python 3.10 or greater installed locally. - * A valid Anthropic API developer credential key. -### Setup Sequence - 1. Clone Repository & Navigate - Terminal Setup - Pull down the main project repository files onto your local system path. - - 2. Establish Virtual Environment - Dependency Isolation - Create and launch an isolated virtual runtime sandbox path to anchor packages cleanly. - - 3. Install Engineering Requirements - Package Management - Deploy required core dependencies, including Pydantic validation typing and the Anthropic client SDK. - - 4. Bind Infrastructure Credentials - Environment Injection - Inject your secret API access tokens directly into the local environment stack context. - - 5. Launch Interactive Interface - System Execution - Run the Streamlit frontend service engine to monitor live agent performance. - -## πŸ“‘ Deep-Dive Engineering Q&A -### Architectural & Operational Strategy -#### Why utilize an Orchestrator-Worker architecture instead of a Choreography model? -An Orchestrator-Worker model offers a central point of control, which is essential for critical production environments like infrastructure observability. In a choreography model, agents react independently to event streams. While highly decoupled, this can cause unpredictable execution states when parsing complex system errors. -By utilizing an explicit central Orchestrator paired with an immutable state contract, we guarantee that log processing follows a predictable sequence. This makes it easy to monitor execution paths and trace failures across all operations. -#### How exactly does the Immutable State Contract protect the logging context? -In standard python architectures, objects are passed by reference and modified in place. If an intermediate reasoning node makes an error or corrupts data while handling a log string, previous states are lost, breaking the system trace. -LogSight-AI implements Pydantic-backed AgentState frames. Instead of modifying properties directly, workers use .model_copy(update=...) to create a new state instance. This ensures the execution history remains completely read-only and unalterable. If an downstream tool fails, the system can instantly recover or inspect the exact state of the pipeline before the crash occurred. -#### What specific criteria does the Execution Circuit Breaker look for to trigger an exit? -The ExecutionCircuitBreaker constantly monitors two main threshold boundaries: - 1. **Loop Depth Boundaries:** If the orchestration loop reaches its maximum limit (e.g., 3 loops) without finding a root cause, the breaker triggers. This prevents infinite agent loops and un-capped token usage. - 2. **Error Accumulation Densities:** If sub-workers throw continuous schema validation faults or hit API limits multiple times in a row, the circuit breaker opens. It stops execution and degrades gracefully to a safe fallback state, alerting on-call engineers instead of racking up API costs. +Contributions should include tests and documentation for behavioral changes. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a2e5ff3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported versions + +Security fixes are applied to the latest release and the `main` branch. + +## Reporting a vulnerability + +Do not open a public issue. Use GitHub's private vulnerability reporting for this repository. Include affected versions, reproduction steps, impact, and any suggested mitigation. Maintainers should acknowledge a report within three business days and provide a remediation timeline after triage. + +## Security model + +The core parser operates locally and does not require credentials. Treat log input as untrusted and potentially sensitive. Do not submit production logs to the optional LLM demonstration without an approved data-handling agreement. Secrets belong in environment variables or a secrets manager and must never be committed. + +Supply-chain controls include dependency auditing, CodeQL, secret scanning, SBOM generation, least-privilege workflow permissions, and a non-root container user. diff --git a/agents/guards.py b/agents/guards.py index 4a2c791..143b93d 100644 --- a/agents/guards.py +++ b/agents/guards.py @@ -1,12 +1,15 @@ # agents/guards.py class CircuitBreakerException(Exception): """Custom exception raised when system constraints are violated.""" + pass + class ExecutionCircuitBreaker: """ Monitors system bounds to prevent infinite tool-use or self-correction loops. """ + def __init__(self, max_loops: int = 3, error_threshold: int = 2): self.max_loops = max_loops self.error_threshold = error_threshold diff --git a/agents/orchestrator.py b/agents/orchestrator.py index 578f6da..d835cc6 100644 --- a/agents/orchestrator.py +++ b/agents/orchestrator.py @@ -1,15 +1,19 @@ # agents/orchestrator.py -import os import json +import os + from anthropic import Anthropic + +from .guards import CircuitBreakerException, ExecutionCircuitBreaker from .state import AgentState -from .guards import ExecutionCircuitBreaker, CircuitBreakerException + class LogSightOrchestrator: """ - Main Orchestrator engine coordinating specialized worker layers + Main Orchestrator engine coordinating specialized worker layers using defensive system patterns and immutable states. """ + def __init__(self): # Fallback to a placeholder if the key isn't loaded yet to prevent initialization crashes api_key = os.environ.get("ANTHROPIC_API_KEY", "mock-key-for-dev") @@ -24,11 +28,11 @@ def process_incident(self, raw_log_data: str) -> AgentState: try: # 1. Structure Layer (Worker 1) state = self._run_parser_worker(state) - + # 2. Iterative Reason/Triage Loop (Worker 2) while state.root_cause_analysis is None: self.breaker.verify_bounds(state.loop_count) - + state = self._run_analysis_worker(state) state.loop_count += 1 state = state.log_step(f"Completed Triage Iteration Cycle {state.loop_count}") @@ -47,7 +51,7 @@ def process_incident(self, raw_log_data: str) -> AgentState: def _run_parser_worker(self, state: AgentState) -> AgentState: state = state.log_step("Invoking Log Parser Worker.") - + system_prompt = ( "You are a strict Log Parsing Subsystem. Your sole job is to transform chaotic, " "unstructured system logs into a clean structured JSON array. Never output conversational prose." @@ -59,21 +63,25 @@ def _run_parser_worker(self, state: AgentState) -> AgentState: model="claude-3-5-sonnet-20241022", max_tokens=1500, system=system_prompt, - messages=[{"role": "user", "content": user_prompt}] + messages=[{"role": "user", "content": user_prompt}], ) - + # Basic validation check to keep downstream workers clean parsed_data = json.loads(response.content[0].text) - return state.model_copy(update={"parsed_json": parsed_data}).log_step("Log parsing complete.") + return state.model_copy(update={"parsed_json": parsed_data}).log_step( + "Log parsing complete." + ) except Exception as e: self.breaker.record_error() # If JSON parsing fails, fall back to string encapsulation and continue path fallback_json = [{"level": "UNKNOWN", "message": state.raw_logs}] - return state.model_copy(update={"parsed_json": fallback_json}).log_step(f"Parser warning: {str(e)}. Using fallback format.") + return state.model_copy(update={"parsed_json": fallback_json}).log_step( + f"Parser warning: {str(e)}. Using fallback format." + ) def _run_analysis_worker(self, state: AgentState) -> AgentState: state = state.log_step("Invoking Infrastructure Root-Cause Analysis Worker.") - + system_prompt = ( "You are an expert Reliability Engineer specializing in system telemetry analysis. " "Identify anomalies, trace root causes, and explicitly point out security concerns." @@ -84,14 +92,14 @@ def _run_analysis_worker(self, state: AgentState) -> AgentState: model="claude-3-5-sonnet-20241022", max_tokens=2000, system=system_prompt, - messages=[{"role": "user", "content": user_prompt}] + messages=[{"role": "user", "content": user_prompt}], ) return state.model_copy(update={"root_cause_analysis": response.content[0].text}) def _run_remediation_worker(self, state: AgentState) -> AgentState: state = state.log_step("Invoking Remediation Synthesis Worker.") - + system_prompt = "You are a DevOps Automation Agent. Generate standard operating procedures (SOPs) and runbooks based on failure reports." user_prompt = f"Write clear mitigation steps for this issue:\n{state.root_cause_analysis}" @@ -99,14 +107,16 @@ def _run_remediation_worker(self, state: AgentState) -> AgentState: model="claude-3-5-sonnet-20241022", max_tokens=1500, system=system_prompt, - messages=[{"role": "user", "content": user_prompt}] + messages=[{"role": "user", "content": user_prompt}], ) return state.model_copy(update={"recommended_actions": response.content[0].text}) def _handle_graceful_degradation(self, state: AgentState) -> AgentState: """Provides a safe default output state when the circuit breaker opens.""" - return state.model_copy(update={ - "root_cause_analysis": "ANALYSIS HALTED: The processing safety limits were breached.", - "recommended_actions": "MANUAL INTERVENTION REQUIRED: Please isolate this instance and review the system execution logs below." - }) + return state.model_copy( + update={ + "root_cause_analysis": "ANALYSIS HALTED: The processing safety limits were breached.", + "recommended_actions": "MANUAL INTERVENTION REQUIRED: Please isolate this instance and review the system execution logs below.", + } + ) diff --git a/agents/state.py b/agents/state.py index 50523d8..e6dcdd5 100644 --- a/agents/state.py +++ b/agents/state.py @@ -1,23 +1,26 @@ # agents/state.py +from typing import Any + from pydantic import BaseModel, Field -from typing import List, Dict, Any, Optional + class AgentState(BaseModel): """ The immutable single source of truth passed across the LogSight-AI lifecycle. Prevents side effects and provides explicit telemetry for debugging. """ + raw_logs: str - parsed_json: Optional[List[Dict[str, Any]]] = None - security_threats: List[str] = Field(default_factory=list) - root_cause_analysis: Optional[str] = None - recommended_actions: Optional[str] = None - + parsed_json: list[dict[str, Any]] | None = None + security_threats: list[str] = Field(default_factory=list) + root_cause_analysis: str | None = None + recommended_actions: str | None = None + # Observability & Guardrails Telemetry - execution_steps: List[str] = Field(default_factory=list) + execution_steps: list[str] = Field(default_factory=list) loop_count: int = 0 circuit_tripped: bool = False - failure_reason: Optional[str] = None + failure_reason: str | None = None def log_step(self, step_description: str) -> "AgentState": """Returns a new copy of state with the updated execution trace.""" diff --git a/app.py b/app.py index 4eb8e7b..c06cb93 100644 --- a/app.py +++ b/app.py @@ -1,16 +1,22 @@ # app.py (Partial UI Integration Example) -import streamlit as st import os + +import streamlit as st + from agents.orchestrator import LogSightOrchestrator st.set_page_config(page_title="LogSight-AI Enterprise", layout="wide") -st.title("πŸ›‘οΈ LogSight-AI: Multi-Agent Observability System") +st.title("🛑️ LogSight-AI: Multi-Agent Observability System") # Ensure API Key is bound safely if "ANTHROPIC_API_KEY" not in os.environ: os.environ["ANTHROPIC_API_KEY"] = st.sidebar.text_input("Anthropic API Key", type="password") -raw_input_logs = st.text_area("Paste System Log Payload Here:", height=200, placeholder="2026-06-28T16:50:00Z ERROR auth_service: DB connection timeout ...") +raw_input_logs = st.text_area( + "Paste System Log Payload Here:", + height=200, + placeholder="2026-06-28T16:50:00Z ERROR auth_service: DB connection timeout ...", +) if st.button("Run Orchestrated Analysis Pipeline"): if not os.environ.get("ANTHROPIC_API_KEY"): @@ -20,24 +26,24 @@ # Execute Pipeline orchestrator = LogSightOrchestrator() final_state = orchestrator.process_incident(raw_input_logs) - + # Layout Response Columns col1, col2 = st.columns(2) - + with col1: - st.subheader("πŸ“‹ System Analysis Report") + st.subheader("Γ°ΕΈβ€œβ€Ή System Analysis Report") st.markdown(f"### Root Cause\n{final_state.root_cause_analysis}") st.markdown(f"### Recommended SOP Mitigation\n{final_state.recommended_actions}") - + with col2: - st.subheader("βš™οΈ Agent Orchestration Telemetry") - + st.subheader("Òőℒ️ Agent Orchestration Telemetry") + # Highlight Circuit Breaker Status if final_state.circuit_tripped: - st.error(f"πŸ”΄ Circuit Breaker: OPEN\nReason: {final_state.failure_reason}") + st.error(f"🔴 Circuit Breaker: OPEN\nReason: {final_state.failure_reason}") else: - st.success("🟒 Circuit Breaker: CLOSED (Healthy)") - + st.success("Γ°ΕΈΕΈΒ’ Circuit Breaker: CLOSED (Healthy)") + # Print explicit chronological execution trace st.markdown("**Execution Trace Sequence:**") for step in final_state.execution_steps: diff --git a/benchmarks/benchmark_report.md b/benchmarks/benchmark_report.md new file mode 100644 index 0000000..180a32d --- /dev/null +++ b/benchmarks/benchmark_report.md @@ -0,0 +1,14 @@ +# Benchmark Report + +The local result below is a development baseline, not a hardware-independent service SLO. CI will publish the canonical JSON artifact for each commit. + +| Metric | Value | +|---|---:| +| Benchmark date | 2026-07-17 | +| Input size | 1,000 log lines | +| Median latency | 10.315 ms | +| Mean latency | 10.394 ms | +| Minimum latency | 10.077 ms | +| Maximum latency | 12.282 ms | +| Throughput | 96.21 pipeline runs/sec (96,213 lines/sec) | +| Peak RAM | Pending | diff --git a/demo.app.py b/demo.app.py index ff4578d..0bfd17f 100644 --- a/demo.app.py +++ b/demo.app.py @@ -1,27 +1,33 @@ -import streamlit as st -import pandas as pd import random import time from datetime import datetime +import pandas as pd +import streamlit as st + # Setup configuration and layout -st.set_page_config(page_title="LogSight-AI Dashboard", page_icon="πŸ”", layout="wide") +st.set_page_config(page_title="LogSight-AI Dashboard", page_icon="🔍", layout="wide") -st.title("πŸ” LogSight-AI: Intelligent Log Analysis Platform") +st.title("🔍 LogSight-AI: Intelligent Log Analysis Platform") st.caption("Production Demostration Stack | Real-Time Anomaly Detection & Categorization") # --- SIDEBAR: LOG GENERATOR SIMULATOR --- -st.sidebar.header("πŸ› οΈ Live Log Stream Simulator") -st.sidebar.markdown("Simulate live infrastructure traffic to test LogSight-AI's streaming ingestion and inference.") +st.sidebar.header("🛠️ Live Log Stream Simulator") +st.sidebar.markdown( + "Simulate live infrastructure traffic to test LogSight-AI's streaming ingestion and inference." +) -service_type = st.sidebar.selectbox("Target Subsystem", ["AuthService", "PaymentGateway", "QueryEngine", "Nginx-Ingress"]) +service_type = st.sidebar.selectbox( + "Target Subsystem", ["AuthService", "PaymentGateway", "QueryEngine", "Nginx-Ingress"] +) log_rate = st.sidebar.slider("Ingestion Rate (seconds)", 0.5, 3.0, 1.0) -generate_anomaly = st.sidebar.button("🚨 Inject Malicious Payload / Anomaly") +generate_anomaly = st.sidebar.button("Γ°ΕΈΕ‘Β¨ Inject Malicious Payload / Anomaly") # System state initialization if "log_history" not in st.session_state: st.session_state.log_history = [] + # Core Mock Inference Function def analyze_log_line(service, msg): """ @@ -35,6 +41,7 @@ def analyze_log_line(service, msg): return "ERROR", "Infrastructure Bottleneck / Null Pointer Exception" return "INFO", "Standard Operations" + # Append simulated streaming logs dynamically if st.sidebar.checkbox("Start Live Stream Ingestion", value=True): # Base message matrix @@ -42,33 +49,35 @@ def analyze_log_line(service, msg): "User session token validated successfully.", "GET /v1/models HTTP/1.1 200 OK", "Connection established to vector search cluster.", - "Database connection pool health check: OK" + "Database connection pool health check: OK", ] - + anomalies = [ "SQL Injection string detected in query parameters: SELECT * FROM users; --", "Fatal: Connection timeout after 5000ms to microservice upstream.", "Brute force threshold reached: 45 failed authentication attempts from IP 192.168.1.105", - "OutOfMemoryError: Java heap space exhausted during log vectorization." + "OutOfMemoryError: Java heap space exhausted during log vectorization.", ] - + # Pick target message based on manual button trigger or random seed if generate_anomaly: raw_msg = random.choice(anomalies) else: # standard fallback simulation - raw_msg = random.choice(standard_logs) if random.random() > 0.15 else random.choice(anomalies) - + raw_msg = ( + random.choice(standard_logs) if random.random() > 0.15 else random.choice(anomalies) + ) + level, assessment = analyze_log_line(service_type, raw_msg) - + new_log = { "Timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], "Subsystem": service_type, "Severity": level, "Raw Log Message": raw_msg, - "LogSight AI Classification": assessment + "LogSight AI Classification": assessment, } - + st.session_state.log_history.insert(0, new_log) # Caps the browser state so memory allocations don't spill over if len(st.session_state.log_history) > 100: @@ -84,29 +93,42 @@ def analyze_log_line(service, msg): st.metric("Total Parsed Streams", len(log_df)) with col2: critical_count = len(log_df[log_df["Severity"] == "CRITICAL"]) - st.metric("Security Anomalies Blocked", critical_count, delta=f"{critical_count} Alert(s)" if critical_count > 0 else None, delta_color="inverse") + st.metric( + "Security Anomalies Blocked", + critical_count, + delta=f"{critical_count} Alert(s)" if critical_count > 0 else None, + delta_color="inverse", + ) with col3: error_count = len(log_df[log_df["Severity"] == "ERROR"]) st.metric("System Operational Errors", error_count) - st.subheader("πŸ“‹ Active Streaming Log Ledger (Real-Time Ingestion)") - + st.subheader("Γ°ΕΈβ€œβ€Ή Active Streaming Log Ledger (Real-Time Ingestion)") + # Styled table row coloring using pandas styling parameters def color_severity(val): - if val == "CRITICAL": return "background-color: rgba(255, 75, 75, 0.2); color: #ff4b4b;" - if val == "ERROR": return "background-color: rgba(255, 165, 0, 0.2); color: #ffa500;" - return "background-color: rgba(0, 255, 0, 0.05); color: #00ff00;" if st.get_option("theme.base") == "dark" else "" + if val == "CRITICAL": + return "background-color: rgba(255, 75, 75, 0.2); color: #ff4b4b;" + if val == "ERROR": + return "background-color: rgba(255, 165, 0, 0.2); color: #ffa500;" + return ( + "background-color: rgba(0, 255, 0, 0.05); color: #00ff00;" + if st.get_option("theme.base") == "dark" + else "" + ) styled_df = log_df.style.map(color_severity, subset=["Severity"]) st.dataframe(styled_df, use_container_width=True, hide_index=True) # Optional metric charting tracking system traffic density over time if st.checkbox("Show Infrastructure Load Distribution Overview"): - st.subheader("πŸ“Š Traffic Distribution by Subsystem") + st.subheader("Γ°ΕΈβ€œΕ  Traffic Distribution by Subsystem") subsystem_counts = log_df["Subsystem"].value_counts() st.bar_chart(subsystem_counts) else: - st.info("Log stream ingestion is initialized. Check the 'Start Live Stream Ingestion' box in the sidebar to begin processing payload lines.") + st.info( + "Log stream ingestion is initialized. Check the 'Start Live Stream Ingestion' box in the sidebar to begin processing payload lines." + ) # Rerun loop logic mimicking standard polling loops time.sleep(log_rate) diff --git a/docker-compose.yml b/docker-compose.yml index 3ca5590..c3d7b0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,38 +1,17 @@ -version: '3.8' - services: - # The core Python/C++ pipeline engine - engine: - build: - context: . - dockerfile: Dockerfile - ports: - - "8000:8000" - environment: - - ENVIRONMENT=production - - LOG_INGESTION_RATE=50000 - restart: unless-stopped - - # Visualizer Streamlit Frontend - dashboard: - image: python:3.10-slim - volumes: - - ./app_demo.py:/app/app_demo.py - working_dir: /app - ports: - - "8501:8501" - command: > - sh -c "pip install streamlit pandas numpy && streamlit run app_demo.py --server.port=8501 --server.address=0.0.0.0" - depends_on: - - engine - - # Automated Mock Log Traffic Pipeline Injector - simulator: - image: python:3.10-slim - volumes: - - ./log_simulator.py:/app/log_simulator.py - working_dir: /app - command: > - sh -c "pip install requests && python log_simulator.py" - depends_on: - - engine + logsight: + build: . + image: logsight-ai:local + command: ["health"] + read_only: true + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + tmpfs: + - /tmp:size=16m,mode=1777 + deploy: + resources: + limits: + cpus: "1.0" + memory: 256M diff --git a/docs/AUDIT.md b/docs/AUDIT.md new file mode 100644 index 0000000..1b1d1eb --- /dev/null +++ b/docs/AUDIT.md @@ -0,0 +1,48 @@ +# Production Readiness Audit + +Date: 2026-07-17 + +## Executive summary + +LogSight-AI has a small, understandable core parser/analyzer with streaming file reads, typed data structures, a CLI, tests, and a non-root container. The repository was not production-ready at audit start: CI targeted a nonexistent package, several checks suppressed failures, claimed metrics were not reproducible, deployment files described incompatible services, and input constraints were unenforced. + +This hardening pass makes quality, coverage, security, packaging, and benchmark checks deterministic. The local statistical engine is the supported production scope. The Streamlit/Anthropic files remain prototypes and are intentionally excluded from the packaged runtime until their dependency, privacy, timeout, retry, and service contracts are designed. + +## Strengths + +- Dependency-light local analysis with no network requirement. +- Line-by-line file ingestion bounds memory use. +- Clear parser, analyzer, and CLI module boundaries. +- Error/critical classification and duplicate suppression are tested. +- Multi-stage, non-root OCI image. + +## Risks and technical debt + +| Priority | Risk | Impact | Treatment | +|---|---|---|---| +| P0 | CI failures were swallowed and coverage targeted `src` | False-green releases | Replaced with one fail-closed CI workflow and 90% gate | +| P0 | Invalid window/threshold inputs | Runtime crash or invalid results | Validate library and CLI boundaries | +| P1 | Prototype agent accepts sensitive logs and calls a third party | Privacy, availability, cost | Keep outside packaged runtime; add threat guidance | +| P1 | Dependencies were duplicated and loosely bounded | Drift and supply-chain exposure | Make `pyproject.toml` canonical; audit in CI | +| P1 | Compose advertised an API that does not exist | Deployment failure | Remove from supported deployment path; validate CLI container | +| P2 | Length z-score is a heuristic, not an ML accuracy model | False positives/negatives | Document limits; require labeled corpus before accuracy claims | +| P2 | No persistent service, auth, rate limit, or telemetry backend | Not horizontally scalable as an API | Add only with an explicit service SLO and threat model | + +## Architecture and scalability + +The supported data path is `file/stdin -> parser -> statistical analyzer -> CLI report`. Time is O(n), parsing is streaming for files, and analysis retains entries in memory, so CLI callers should partition very large inputs. There is no production network API or database to scale. Horizontal-service claims would therefore be misleading. + +## Prioritized roadmap + +1. Publish reproducible CI and benchmark artifacts from the default branch. +2. Pin released dependency versions with an automated lock/update policy. +3. Build a labeled, versioned evaluation corpus before reporting precision, recall, F1, ROC-AUC, MAP, MRR, or NDCG. +4. If an API is required, write an ADR covering authentication, authorization, rate limiting, request size, timeouts, observability, and data retention before implementation. +5. Add signed images, provenance attestations, staged deployment, monitoring, and rollback exercises before production rollout. + +## Acceptance criteria + +- Ruff, mypy, pytest, 90% coverage, package build, pip-audit, Bandit, CodeQL, secret scan, and SBOM jobs pass without `continue-on-error` or shell fallbacks. +- Benchmark JSON is retained and compared in pull requests. +- Container runs as non-root and `logsight health` succeeds. +- Documentation contains no fabricated performance or accuracy values. diff --git a/docs/BENCHMARKING.md b/docs/BENCHMARKING.md new file mode 100644 index 0000000..cb894b7 --- /dev/null +++ b/docs/BENCHMARKING.md @@ -0,0 +1,5 @@ +# Benchmark Guide + +Run `pytest tests/test_benchmarks.py --benchmark-only --benchmark-json=benchmarks/latest.json`. The JSON includes distribution statistics such as min, max, mean, median, standard deviation, rounds, and operations per second. CI retains it as an artifact. + +Compare two runs with `pytest-benchmark compare benchmarks/baseline.json benchmarks/latest.json`. Use the same Python version, hardware class, power profile, input size, and warm-up settings. A microbenchmark is not an end-to-end service load test; user-concurrency, GPU, model-quality, and network metrics are not applicable to the current local CLI architecture. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..ad9b6e6 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,29 @@ +# Deployment Guide + +## Supported artifact + +The supported artifact is the CLI container built by `Dockerfile`. Build and validate it with: + +```bash +docker build --tag logsight-ai:local . +docker run --rm logsight-ai:local health +``` + +Mount logs read-only and invoke `analyze`: + +```bash +docker run --rm -v "$PWD/logs:/logs:ro" logsight-ai:local analyze /logs/application.log +``` + +The process runs as an unprivileged user and does not require secrets or network access. Set CPU/memory limits in the target scheduler based on benchmark results. Logs may contain credentials or personal data; restrict access and retention accordingly. + +## Production checklist + +- CI, dependency, CodeQL, and secret scans are green. +- Image is built from the reviewed commit, scanned, signed, and stored immutably. +- Health command succeeds under configured resource limits. +- Log mounts are read-only; egress is denied unless explicitly required. +- Operational dashboards alert on failure rate, duration, memory, and disk pressure. +- Previous image digest is retained and rollback is rehearsed. + +`app.py`, `demo.app.py`, and `agents/` are demonstrations, not supported deployment services. diff --git a/log_simulator.py b/log_simulator.py index 7a11c62..7e5dda6 100644 --- a/log_simulator.py +++ b/log_simulator.py @@ -1,6 +1,6 @@ -import time -import json import random +import time + import requests API_URL = "http://localhost:8000/api/v1/ingest" # Adjust mapping to match your current FastAPI port @@ -8,47 +8,52 @@ COMPONENTS = ["auth-service", "payment-gateway", "kube-router", "db-pool-manager", "api-gateway"] LEVELS = ["INFO", "INFO", "INFO", "DEBUG", "WARN"] + def generate_valid_log(): return { "timestamp": time.time(), "component": random.choice(COMPONENTS), "level": random.choice(LEVELS), "message": "Connection pooling successfully handling context handshakes.", - "payload_size_bytes": random.randint(128, 512) + "payload_size_bytes": random.randint(128, 512), } + def generate_anomaly_log(): return { "timestamp": time.time(), "component": "payment-gateway", "level": "FATAL", "message": "CRITICAL_FAILURE: SIMD Vector alignment mismatch during parsing payload buffer allocation error 0x7FFF.", - "payload_size_bytes": random.randint(2048, 4096) + "payload_size_bytes": random.randint(2048, 4096), } -print("πŸš€ Initiating LogSight-AI High-Throughput Stream Generator...") + +print("ðŸő€ Initiating LogSight-AI High-Throughput Stream Generator...") print("Streaming targeted datasets matching production schemas...") try: while True: # Batch generation to simulate heavy packet bursts batch = [] - is_attack_window = (int(time.time()) % 15 == 0) # Generate anomaly signature spikes every 15 seconds - + is_attack_window = ( + int(time.time()) % 15 == 0 + ) # Generate anomaly signature spikes every 15 seconds + for _ in range(5000): if is_attack_window and random.random() > 0.7: batch.append(generate_anomaly_log()) else: batch.append(generate_valid_log()) - + # Transmit batch packet over to your ingestion pipeline try: # Uncomment the next line once your core pipeline server container is active: # response = requests.post(API_URL, json={"logs": batch}, timeout=1) pass except requests.exceptions.RequestException: - pass # Gracefully handle offline states during sandbox orchestration - + pass # Gracefully handle offline states during sandbox orchestration + print(f"Ingested {len(batch)} logs into stream buffers... Status: 200 OK") time.sleep(0.1) diff --git a/logsight/analyzer.py b/logsight/analyzer.py index fdf005c..2ef6ee4 100644 --- a/logsight/analyzer.py +++ b/logsight/analyzer.py @@ -5,8 +5,8 @@ from collections import Counter from collections.abc import Sequence from dataclasses import dataclass, field - -import numpy as np +from math import isfinite +from statistics import fmean, pstdev from logsight.parser import LogEntry, LogLevel @@ -61,8 +61,8 @@ def compute_stats(entries: Sequence[LogEntry]) -> WindowStats: return stats -def _message_lengths(entries: Sequence[LogEntry]) -> np.ndarray: - return np.array([len(e.message) for e in entries], dtype=float) +def _message_lengths(entries: Sequence[LogEntry]) -> list[float]: + return [float(len(e.message)) for e in entries] def detect_anomalies( @@ -91,6 +91,9 @@ def detect_anomalies( ------- AnomalyReport """ + if not isfinite(zscore_threshold) or zscore_threshold < 0: + raise ValueError("zscore_threshold must be a finite, non-negative number") + report = AnomalyReport(zscore_threshold=zscore_threshold) report.stats = compute_stats(entries) @@ -98,8 +101,8 @@ def detect_anomalies( return report lengths = _message_lengths(entries) - mean = float(np.mean(lengths)) - std = float(np.std(lengths)) + mean = fmean(lengths) + std = pstdev(lengths) anomalous: list[LogEntry] = [] seen_ids: set[int] = set() @@ -134,12 +137,17 @@ def error_rate_spike( window_size: Number of entries per sliding window. spike_threshold: - Fraction of errors that triggers a spike (``0.0``–``1.0``). + Fraction of errors that triggers a spike (``0.0``Γ’β‚¬β€œ``1.0``). Returns ------- List of start indices where a spike was detected. """ + if window_size <= 0: + raise ValueError("window_size must be greater than zero") + if not 0 <= spike_threshold <= 1: + raise ValueError("spike_threshold must be between 0 and 1") + spike_starts: list[int] = [] n = len(entries) for start in range(0, n - window_size + 1, window_size): diff --git a/logsight/cli.py b/logsight/cli.py index 0367b6e..7693939 100644 --- a/logsight/cli.py +++ b/logsight/cli.py @@ -8,19 +8,21 @@ from rich.console import Console from rich.table import Table -from logsight.analyzer import detect_anomalies, error_rate_spike +from logsight.analyzer import AnomalyReport, detect_anomalies, error_rate_spike from logsight.parser import parse_file, parse_lines console = Console() -def _print_report(report, show_anomalies: bool) -> None: +def _print_report(report: AnomalyReport, show_anomalies: bool) -> None: stats = report.stats console.print("\n[bold]LogSight-AI Analysis[/bold]") console.print(f" Total entries : [cyan]{stats.total}[/cyan]") console.print(f" Errors : [red]{stats.error_count}[/red]") console.print(f" Warnings : [yellow]{stats.warning_count}[/yellow]") - console.print(f" Error rate : [{'red' if stats.error_rate >= 0.10 else 'green'}]{stats.error_rate:.1%}[/]") + console.print( + f" Error rate : [{'red' if stats.error_rate >= 0.10 else 'green'}]{stats.error_rate:.1%}[/]" + ) if stats.top_messages: table = Table(title="Top Messages", show_header=True, header_style="bold magenta") @@ -38,7 +40,7 @@ def _print_report(report, show_anomalies: bool) -> None: f" [[{level_style}]{entry.level.value}[/{level_style}]] {entry.message[:200]}" ) if len(report.anomalies) > 20: - console.print(f" … and {len(report.anomalies) - 20} more.") + console.print(f" Ò€¦ and {len(report.anomalies) - 20} more.") elif show_anomalies: console.print("\n[bold green]No anomalies detected.[/bold green]") @@ -55,6 +57,7 @@ def main() -> None: "--threshold", "-t", default=2.5, + type=click.FloatRange(min=0), show_default=True, help="Z-score threshold for anomaly detection.", ) @@ -68,6 +71,7 @@ def main() -> None: "--window", "-w", default=100, + type=click.IntRange(min=1), show_default=True, help="Window size for error-rate spike detection.", ) @@ -75,6 +79,7 @@ def main() -> None: "--spike-threshold", "-s", default=0.25, + type=click.FloatRange(min=0, max=1), show_default=True, help="Error-rate fraction that constitutes a spike.", ) @@ -112,6 +117,7 @@ def analyze_cmd( "--threshold", "-t", default=2.5, + type=click.FloatRange(min=0), show_default=True, help="Z-score threshold for anomaly detection.", ) @@ -134,9 +140,9 @@ def health_cmd() -> None: sample = parse_line("INFO health check") report = detect_anomalies([sample]) - console.print("[bold green]βœ“ LogSight-AI is healthy.[/bold green]") + console.print("[bold green]Γ’Ε“β€œ LogSight-AI is healthy.[/bold green]") console.print(f" version : [cyan]{_version()}[/cyan]") - console.print(f" parsed : [cyan]{sample.level.value}[/cyan] β€” {sample.message}") + console.print(f" parsed : [cyan]{sample.level.value}[/cyan] Ò€” {sample.message}") console.print(f" anomaly : [cyan]{report.has_anomalies}[/cyan]") diff --git a/logsight/parser.py b/logsight/parser.py index 845247a..3077f9e 100644 --- a/logsight/parser.py +++ b/logsight/parser.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from dataclasses import dataclass, field from datetime import datetime from enum import Enum @@ -39,7 +39,7 @@ class LogLevel(str, Enum): ( "nginx_access", re.compile( - r'^(?P\S+)\s+-\s+(?P\S+)\s+\[(?P[^\]]+)\]\s+' + r"^(?P\S+)\s+-\s+(?P\S+)\s+\[(?P[^\]]+)\]\s+" r'"(?P[^"]+)"\s+(?P\d{3})\s+(?P\d+)' ), ), @@ -126,8 +126,8 @@ def parse_line(line: str) -> LogEntry: return LogEntry(raw=line, message=line) -def parse_lines(lines: list[str]) -> list[LogEntry]: - """Parse a list of log lines.""" +def parse_lines(lines: Iterable[str]) -> list[LogEntry]: + """Parse an iterable of log lines, excluding blank records.""" return [parse_line(line) for line in lines if line.strip()] diff --git a/pyproject.toml b/pyproject.toml index b98948d..1934601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,13 @@ name = "logsight-ai" version = "0.1.0" description = "AI-powered log analysis and anomaly detection" readme = "README.md" -requires-python = ">=3.9" -license = { text = "MIT" } +requires-python = ">=3.10" +license = "MIT" keywords = ["logging", "anomaly-detection", "ai", "monitoring"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: System Administrators", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -26,8 +25,6 @@ classifiers = [ dependencies = [ "click>=8.1.7", "rich>=13.7.0", - "scikit-learn>=1.4.0", - "numpy>=1.26.0", ] [project.optional-dependencies] @@ -35,6 +32,11 @@ dev = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "ruff>=0.3.0", + "mypy>=1.10.0", + "bandit>=1.7.8", + "pip-audit>=2.7.3", + "pytest-benchmark>=4.0.0", + "pip-licenses>=5.0.0", ] [project.scripts] @@ -46,11 +48,12 @@ include = ["logsight*"] [tool.pytest.ini_options] testpaths = ["tests"] -addopts = "-v --tb=short" +addopts = "-v --tb=short --strict-markers --cov=logsight --cov-report=term-missing --cov-report=xml --cov-fail-under=90" [tool.ruff] line-length = 100 -target-version = "py39" +target-version = "py310" +extend-exclude = ["work"] [tool.ruff.lint] select = ["E", "F", "W", "I", "UP"] @@ -59,3 +62,18 @@ ignore = ["E501"] [tool.coverage.run] source = ["logsight"] omit = ["tests/*"] + +[tool.coverage.report] +show_missing = true +skip_covered = true +exclude_also = ["if __name__ == .__main__.:"] + +[tool.mypy] +python_version = "3.10" +strict = true +packages = ["logsight"] +follow_imports = "skip" +disallow_untyped_decorators = false + +[tool.bandit] +exclude_dirs = ["tests", "work"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 8b9d005..aefbcb6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1 @@ --r requirements.txt -pytest>=7.4.0 -pytest-cov>=4.1.0 -ruff>=0.3.0 +-e .[dev] diff --git a/requirements.txt b/requirements.txt index dc033c7..29c9fb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -click>=8.1.7 -rich>=13.7.0 -scikit-learn>=1.4.0 -numpy>=1.26.0 -streamlit>=1.35.0 +# Kept for environments that cannot install project extras. pyproject.toml is +# the canonical dependency declaration. +click>=8.1.7,<9 +rich>=13.7.0,<15 diff --git a/tests/test_analyzer.py b/tests/test_analyzer.py index 4ef14f4..aa7ecae 100644 --- a/tests/test_analyzer.py +++ b/tests/test_analyzer.py @@ -2,6 +2,8 @@ from __future__ import annotations +import pytest + from logsight.analyzer import ( AnomalyReport, compute_stats, @@ -49,6 +51,11 @@ def test_level_counts(self): class TestDetectAnomalies: + @pytest.mark.parametrize("threshold", [-1, float("inf"), float("nan")]) + def test_rejects_invalid_threshold(self, threshold): + with pytest.raises(ValueError): + detect_anomalies([_entry("INFO", "ok")], zscore_threshold=threshold) + def test_empty_entries(self): report = detect_anomalies([]) assert not report.has_anomalies @@ -91,13 +98,23 @@ def test_returns_anomaly_report(self): class TestErrorRateSpike: + @pytest.mark.parametrize("window", [0, -1]) + def test_rejects_invalid_window(self, window): + with pytest.raises(ValueError): + error_rate_spike([], window_size=window) + + @pytest.mark.parametrize("threshold", [-0.01, 1.01]) + def test_rejects_invalid_spike_threshold(self, threshold): + with pytest.raises(ValueError): + error_rate_spike([], spike_threshold=threshold) + def test_no_spike(self): entries = [_entry("INFO", "ok")] * 200 spikes = error_rate_spike(entries, window_size=100, spike_threshold=0.25) assert spikes == [] def test_spike_detected(self): - # First 100 entries are all errors β†’ spike. + # First 100 entries are all errors Ò†’ spike. entries = [_entry("ERROR", "fail")] * 100 + [_entry("INFO", "ok")] * 100 spikes = error_rate_spike(entries, window_size=100, spike_threshold=0.25) assert 0 in spikes diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 0000000..8b06a7a --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,14 @@ +"""Deterministic microbenchmarks for regression tracking.""" + +from logsight.analyzer import detect_anomalies +from logsight.parser import parse_lines + + +def test_parse_and_analyze_throughput(benchmark): + lines = [f"2026-01-01T00:00:00Z INFO service request {i}" for i in range(1_000)] + + def pipeline(): + return detect_anomalies(parse_lines(lines), flag_errors=False) + + result = benchmark(pipeline) + assert result.stats.total == 1_000 diff --git a/tests/test_cli.py b/tests/test_cli.py index 6674b8d..4f16564 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -22,3 +22,41 @@ def test_health_output_contains_version(self): runner = CliRunner() result = runner.invoke(main, ["health"]) assert "version" in result.output.lower() + + +class TestAnalyzeCommand: + def test_analyzes_file_and_reports_spike(self, tmp_path): + log_file = tmp_path / "events.log" + log_file.write_text("ERROR failed\nERROR failed again\n", encoding="utf-8") + result = CliRunner().invoke( + main, + ["analyze", str(log_file), "--window", "1", "--spike-threshold", "1"], + ) + assert result.exit_code == 0 + assert "Anomalies detected" in result.output + assert "Error-rate spikes" in result.output + + def test_empty_file(self, tmp_path): + log_file = tmp_path / "empty.log" + log_file.write_text("", encoding="utf-8") + result = CliRunner().invoke(main, ["analyze", str(log_file)]) + assert result.exit_code == 0 + assert "No log entries" in result.output + + def test_rejects_invalid_window(self, tmp_path): + log_file = tmp_path / "events.log" + log_file.write_text("INFO ok", encoding="utf-8") + result = CliRunner().invoke(main, ["analyze", str(log_file), "--window", "0"]) + assert result.exit_code == 2 + + +class TestStdinCommand: + def test_analyzes_stdin(self): + result = CliRunner().invoke(main, ["stdin"], input="INFO ok\nERROR failed\n") + assert result.exit_code == 0 + assert "Total entries" in result.output + + def test_empty_stdin(self): + result = CliRunner().invoke(main, ["stdin"], input="\n") + assert result.exit_code == 0 + assert "No log entries" in result.output diff --git a/tests/test_parser.py b/tests/test_parser.py index 13e32fd..91c0eb9 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -33,6 +33,10 @@ def test_unknown_level(self): class TestIso8601Format: + def test_invalid_timestamp_is_none(self): + entry = parse_line("2024-99-99T99:99:99 INFO impossible") + assert entry.timestamp is None + def test_basic(self): line = "2024-01-15T12:34:56 INFO [app] Server started" entry = parse_line(line) @@ -79,6 +83,10 @@ def test_syslog_line(self): class TestParseLines: + def test_accepts_generator(self): + entries = parse_lines(line for line in ["INFO one", "ERROR two"]) + assert [entry.level for entry in entries] == [LogLevel.INFO, LogLevel.ERROR] + def test_filters_blank_lines(self): lines = ["INFO hello\n", "\n", " \n", "ERROR world\n"] entries = parse_lines(lines) @@ -108,6 +116,11 @@ def test_raw_preserved(self): entry = parse_line(raw) assert entry.raw == raw + def test_empty_line_is_preserved_as_unknown(self): + entry = parse_line("") + assert entry.level == LogLevel.UNKNOWN + assert entry.message == "" + class TestParseFile: def test_parse_file(self, tmp_path):