mirror of
https://github.com/b3nw/nginx-proxy-manager-mcp.git
synced 2026-05-23 16:55:48 -05:00
feat: Add get_proxy_host_logs tool for reading nginx proxy host logs
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.
This commit is contained in:
146
tests/test_logs.py
Normal file
146
tests/test_logs.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user