mirror of
https://github.com/b3nw/nginx-proxy-manager-mcp.git
synced 2026-05-22 16:35:48 -05:00
Reads nginx access/error logs directly from a mounted NPM log directory, enabling agents to debug proxy issues without SSH access. Requires mounting NPM's /data/logs volume and setting NPM_LOG_DIR. Also includes a feature request PRD for proposing a native log API upstream.
147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
"""Tests for the log reader module."""
|
|
|
|
import pytest
|
|
|
|
from npm_mcp.exceptions import NpmLogError
|
|
from npm_mcp.logs import (
|
|
is_log_dir_configured,
|
|
list_available_logs,
|
|
read_log_lines,
|
|
)
|
|
|
|
SAMPLE_ACCESS_LOG = (
|
|
'[22/May/2025:10:00:01 +0000] - 200 200 - GET https app.example.com'
|
|
' "/" [Client 10.0.0.1] [Length 1542] "Mozilla/5.0" "-"\n'
|
|
'[22/May/2025:10:00:02 +0000] - 301 301 - GET http app.example.com'
|
|
' "/old-path" [Client 10.0.0.2] [Length 0] "curl/7.88" "-"\n'
|
|
'[22/May/2025:10:00:03 +0000] - 404 404 - GET https app.example.com'
|
|
' "/missing" [Client 10.0.0.1] [Length 548] "Mozilla/5.0" "-"\n'
|
|
'[22/May/2025:10:00:04 +0000] - 200 200 - POST https app.example.com'
|
|
' "/api/data" [Client 10.0.0.3] [Length 256] "python-requests/2.31" "-"\n'
|
|
'[22/May/2025:10:00:05 +0000] - 502 502 - GET https app.example.com'
|
|
' "/health" [Client 10.0.0.1] [Length 166] "kube-probe/1.28" "-"\n'
|
|
)
|
|
|
|
SAMPLE_ERROR_LOG = (
|
|
"2025/05/22 10:00:05 [error] 42#42: *123 connect() failed"
|
|
" (111: Connection refused) while connecting to upstream,"
|
|
" client: 10.0.0.1, server: app.example.com\n"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def log_dir(tmp_path, monkeypatch):
|
|
"""Create a temp log directory with sample log files."""
|
|
logs = tmp_path / "logs"
|
|
logs.mkdir()
|
|
|
|
(logs / "proxy-host-5_access.log").write_text(SAMPLE_ACCESS_LOG)
|
|
(logs / "proxy-host-5_error.log").write_text(SAMPLE_ERROR_LOG)
|
|
(logs / "proxy-host-12_access.log").write_text("")
|
|
(logs / "fallback_error.log").write_text("global error\n")
|
|
|
|
monkeypatch.setattr("npm_mcp.logs.settings.log_dir", str(logs))
|
|
return logs
|
|
|
|
|
|
@pytest.fixture
|
|
def no_log_dir(monkeypatch):
|
|
"""Ensure log_dir is unconfigured."""
|
|
monkeypatch.setattr("npm_mcp.logs.settings.log_dir", "")
|
|
|
|
|
|
class TestReadLogLines:
|
|
def test_read_access_log(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="access")
|
|
assert result["host_id"] == 5
|
|
assert result["log_type"] == "access"
|
|
assert result["file"] == "proxy-host-5_access.log"
|
|
assert result["returned_lines"] == 5
|
|
assert result["total_lines_in_file"] == 5
|
|
assert "app.example.com" in result["lines"][0]
|
|
|
|
def test_read_error_log(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="error")
|
|
assert result["log_type"] == "error"
|
|
assert result["returned_lines"] == 1
|
|
assert "Connection refused" in result["lines"][0]
|
|
|
|
def test_lines_limit(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="access", lines=2)
|
|
assert result["returned_lines"] == 2
|
|
assert "10:00:04" in result["lines"][0]
|
|
assert "10:00:05" in result["lines"][1]
|
|
|
|
def test_lines_capped_at_max(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="access", lines=9999)
|
|
assert result["returned_lines"] == 5
|
|
|
|
def test_search_filter(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="access", search="404")
|
|
assert result["returned_lines"] == 1
|
|
assert result["matched_lines"] == 1
|
|
assert result["total_lines_in_file"] is None
|
|
assert "/missing" in result["lines"][0]
|
|
|
|
def test_search_case_insensitive(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="access", search="MOZILLA")
|
|
assert result["returned_lines"] == 2
|
|
|
|
def test_search_by_ip(self, log_dir):
|
|
result = read_log_lines(host_id=5, log_type="access", search="10.0.0.1")
|
|
assert result["returned_lines"] == 3
|
|
|
|
def test_nonexistent_host(self, log_dir):
|
|
with pytest.raises(NpmLogError, match="Log file not found"):
|
|
read_log_lines(host_id=999, log_type="access")
|
|
|
|
def test_empty_log_file(self, log_dir):
|
|
with pytest.raises(NpmLogError, match="Log file not found"):
|
|
read_log_lines(host_id=12, log_type="error")
|
|
|
|
def test_invalid_log_type(self, log_dir):
|
|
with pytest.raises(NpmLogError, match="Invalid log type"):
|
|
read_log_lines(host_id=5, log_type="combined")
|
|
|
|
def test_no_log_dir_configured(self, no_log_dir):
|
|
with pytest.raises(NpmLogError, match="NPM_LOG_DIR is not configured"):
|
|
read_log_lines(host_id=5, log_type="access")
|
|
|
|
def test_nonexistent_log_dir(self, monkeypatch):
|
|
monkeypatch.setattr("npm_mcp.logs.settings.log_dir", "/nonexistent/path")
|
|
with pytest.raises(NpmLogError, match="does not exist"):
|
|
read_log_lines(host_id=5, log_type="access")
|
|
|
|
|
|
class TestIsLogDirConfigured:
|
|
def test_configured(self, log_dir):
|
|
assert is_log_dir_configured() is True
|
|
|
|
def test_not_configured(self, no_log_dir):
|
|
assert is_log_dir_configured() is False
|
|
|
|
def test_configured_but_missing(self, monkeypatch):
|
|
monkeypatch.setattr("npm_mcp.logs.settings.log_dir", "/nonexistent")
|
|
assert is_log_dir_configured() is False
|
|
|
|
|
|
class TestListAvailableLogs:
|
|
def test_lists_proxy_host_logs_only(self, log_dir):
|
|
results = list_available_logs()
|
|
files = {r["file"] for r in results}
|
|
assert "proxy-host-5_access.log" in files
|
|
assert "proxy-host-5_error.log" in files
|
|
assert "proxy-host-12_access.log" in files
|
|
assert "fallback_error.log" not in files
|
|
|
|
def test_correct_metadata(self, log_dir):
|
|
results = list_available_logs()
|
|
access_5 = next(r for r in results if r["file"] == "proxy-host-5_access.log")
|
|
assert access_5["host_id"] == 5
|
|
assert access_5["log_type"] == "access"
|
|
assert access_5["size_bytes"] > 0
|
|
|
|
def test_not_configured(self, no_log_dir):
|
|
with pytest.raises(NpmLogError, match="NPM_LOG_DIR is not configured"):
|
|
list_available_logs()
|