Skip to content

Commit 8f39779

Browse files
fix(logs): isolate non-default data-dir logs to <data-dir>/logs (#657)
The phantom "core restarts every ~10s" reports (MCP-2250, split from MCP-2207) were a logging artifact, not a real restart loop. `serve` enables file logging by default and derives the log dir purely from $HOME (internal/logs.GetLogDir), ignoring --data-dir. So every `mcpproxy serve` run — Go integration tests (temp data-dirs), e2e scripts, FE/QA Playwright harnesses, AND the real prod core — appended to the same ~/Library/Logs/mcpproxy/main.log. A reader of that shared file saw dozens of fast, interleaved boots and concluded the core was restarting every ~10s. Forensics on a 72-min window show only 2 real prod-core boots (data_dir ~/.mcpproxy) among ~42, and every shutdown was reason="signal" (no panics/self-restarts). Fix: when the resolved data dir is non-default and no explicit --log-dir was given, co-locate logs under <data-dir>/logs. The default data dir (~/.mcpproxy) is unchanged and still logs to the OS-standard location, so the tray and documented paths keep working. An explicit --log-dir still wins. - Add resolveServeLogDir helper (cmd/mcpproxy/logdir.go) with unit tests covering the full precedence: --log-dir > config log_dir > <data-dir>/logs (non-default) > OS-standard. - Wire it into runServer (cmd/mcpproxy/main.go). - Update TestE2E_MCPProxyWithLogging to assert the binary writes to <data-dir>/logs and does NOT leak into the shared OS-standard dir. - Document the resolution order in docs/logging.md. Related MCP-2250 Co-authored-by: Paperclip <noreply@paperclip.ing>
1 parent 62579bf commit 8f39779

5 files changed

Lines changed: 184 additions & 13 deletions

File tree

cmd/mcpproxy/logdir.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
7+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
8+
)
9+
10+
// defaultDataDirPath returns the default data directory (<home>/.mcpproxy),
11+
// mirroring internal/config's default resolution. It is used to decide whether
12+
// the resolved data dir is "non-default" for log co-location. On failure to
13+
// resolve the home dir it returns config.DefaultDataDir, which will simply not
14+
// match any absolute data dir and leave logs at the OS-standard location.
15+
func defaultDataDirPath() string {
16+
home, err := os.UserHomeDir()
17+
if err != nil {
18+
return config.DefaultDataDir
19+
}
20+
return filepath.Join(home, config.DefaultDataDir)
21+
}
22+
23+
// resolveServeLogDir decides the log directory for the `serve` command.
24+
//
25+
// Precedence:
26+
// 1. explicit --log-dir flag (explicitLogDir) — always wins.
27+
// 2. a log dir already set in the loaded config (configLogDir).
28+
// 3. for a NON-default data dir, co-locate logs under <data-dir>/logs.
29+
// 4. otherwise "" — meaning the OS-standard location resolved by
30+
// internal/logs.GetLogDir (e.g. ~/Library/Logs/mcpproxy on macOS).
31+
//
32+
// Step 3 is the fix for MCP-2250: Go integration tests, e2e scripts, and the
33+
// FE/QA Playwright harnesses all run `mcpproxy serve` with a custom data dir
34+
// (temp dirs, ./test-data, /tmp/mcpproxy-*) but, because the log dir was
35+
// derived purely from $HOME, every one of them appended to the SAME shared
36+
// prod log at ~/Library/Logs/mcpproxy/main.log. A reader of that file saw
37+
// dozens of fast boots interleaved and mistook it for the core restarting
38+
// ~every 10s. Co-locating logs with a non-default data dir keeps those runs
39+
// self-contained and leaves the real prod log clean. The default data dir
40+
// (~/.mcpproxy) is unchanged: it still logs to the OS-standard location so the
41+
// tray and documented paths keep working.
42+
func resolveServeLogDir(explicitLogDir, configLogDir, dataDir, defaultDataDir string) string {
43+
if explicitLogDir != "" {
44+
return explicitLogDir
45+
}
46+
if configLogDir != "" {
47+
return configLogDir
48+
}
49+
if dataDir != "" && !sameDir(dataDir, defaultDataDir) {
50+
return filepath.Join(dataDir, "logs")
51+
}
52+
return ""
53+
}
54+
55+
// sameDir reports whether two paths refer to the same directory, comparing
56+
// their cleaned absolute forms so that relative/aliased spellings of the
57+
// default data dir are still recognized as the default.
58+
func sameDir(a, b string) bool {
59+
absA, errA := filepath.Abs(a)
60+
absB, errB := filepath.Abs(b)
61+
if errA != nil || errB != nil {
62+
return filepath.Clean(a) == filepath.Clean(b)
63+
}
64+
return absA == absB
65+
}

cmd/mcpproxy/logdir_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package main
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
)
7+
8+
// TestResolveServeLogDir documents the precedence that keeps non-default-data-dir
9+
// runs (Go tests, e2e scripts, FE/QA harnesses) from polluting the shared prod
10+
// log at ~/Library/Logs/mcpproxy/main.log — the root cause of the phantom
11+
// "core restarts every 10s" reports in MCP-2250.
12+
func TestResolveServeLogDir(t *testing.T) {
13+
const defaultDataDir = "/home/u/.mcpproxy"
14+
15+
cases := []struct {
16+
name string
17+
explicit string
18+
configLog string
19+
dataDir string
20+
defaultData string
21+
want string
22+
}{
23+
{
24+
name: "explicit --log-dir always wins",
25+
explicit: "/custom/logs",
26+
configLog: "/cfg/logs",
27+
dataDir: "/tmp/test-123",
28+
defaultData: defaultDataDir,
29+
want: "/custom/logs",
30+
},
31+
{
32+
name: "config Logging.LogDir wins when no flag",
33+
explicit: "",
34+
configLog: "/cfg/logs",
35+
dataDir: "/tmp/test-123",
36+
defaultData: defaultDataDir,
37+
want: "/cfg/logs",
38+
},
39+
{
40+
name: "default data dir keeps OS-standard log dir (empty => GetLogDir)",
41+
explicit: "",
42+
configLog: "",
43+
dataDir: defaultDataDir,
44+
defaultData: defaultDataDir,
45+
want: "",
46+
},
47+
{
48+
name: "non-default data dir co-locates logs under <data-dir>/logs",
49+
explicit: "",
50+
configLog: "",
51+
dataDir: "/tmp/mcpproxy-test-Foo",
52+
defaultData: defaultDataDir,
53+
want: filepath.Join("/tmp/mcpproxy-test-Foo", "logs"),
54+
},
55+
{
56+
name: "relative non-default data dir (harness ./test-data) co-locates",
57+
explicit: "",
58+
configLog: "",
59+
dataDir: "./test-data",
60+
defaultData: defaultDataDir,
61+
want: filepath.Join("test-data", "logs"),
62+
},
63+
{
64+
name: "absolute spelling of default data dir is treated as default",
65+
explicit: "",
66+
configLog: "",
67+
dataDir: "/home/u/../u/.mcpproxy",
68+
defaultData: defaultDataDir,
69+
want: "",
70+
},
71+
{
72+
name: "empty data dir is a no-op (OS-standard)",
73+
explicit: "",
74+
configLog: "",
75+
dataDir: "",
76+
defaultData: defaultDataDir,
77+
want: "",
78+
},
79+
}
80+
81+
for _, tc := range cases {
82+
t.Run(tc.name, func(t *testing.T) {
83+
got := resolveServeLogDir(tc.explicit, tc.configLog, tc.dataDir, tc.defaultData)
84+
// Compare cleaned, since the helper may return a joined relative path.
85+
if filepath.Clean(got) != filepath.Clean(tc.want) {
86+
t.Fatalf("resolveServeLogDir(%q,%q,%q,%q) = %q, want %q",
87+
tc.explicit, tc.configLog, tc.dataDir, tc.defaultData, got, tc.want)
88+
}
89+
})
90+
}
91+
}

cmd/mcpproxy/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -466,10 +466,12 @@ func runServer(cmd *cobra.Command, _ []string) error {
466466
}
467467
}
468468

469-
// Override log directory if specified
470-
if cmdLogDir != "" {
471-
cfg.Logging.LogDir = cmdLogDir
472-
}
469+
// Resolve the log directory. An explicit --log-dir wins; otherwise a
470+
// non-default data dir co-locates logs under <data-dir>/logs so that
471+
// tests/e2e/harness `serve` runs do not pollute the shared OS-standard
472+
// prod log (root cause of the phantom "core restarts every 10s" in
473+
// MCP-2250). The default data dir keeps the OS-standard location.
474+
cfg.Logging.LogDir = resolveServeLogDir(cmdLogDir, cfg.Logging.LogDir, cfg.DataDir, defaultDataDirPath())
473475

474476
// Setup logger with new logging system
475477
logger, err := logs.SetupLogger(cfg.Logging)

docs/logging.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ The logging system automatically selects the appropriate directory based on your
7373
- **Location**: `~/.mcpproxy/logs/`
7474
- **Used when**: OS detection fails or standard directories are inaccessible
7575

76+
### Custom data directory
77+
- **Location**: `<data-dir>/logs/`
78+
- **Used when**: the resolved data directory is **not** the default (`~/.mcpproxy`) — i.e. you ran with a custom `--data-dir` flag or a `data_dir` set in the config file — **and** no explicit `--log-dir` was given.
79+
- **Rationale**: co-locating logs with a custom data directory keeps non-default runs (integration tests, e2e scripts, throwaway/QA instances) self-contained so they never write into the shared OS-standard log (`~/Library/Logs/mcpproxy/main.log`). Mixing many short-lived `serve` processes into that single shared file previously made the log unreadable and produced misleading "the core restarts every ~10s" signals.
80+
- **Override**: an explicit `--log-dir` always takes precedence; the default data directory (`~/.mcpproxy`) is unaffected and still logs to the OS-standard location above.
81+
- **Resolution order**: `--log-dir` flag → `logging.log_dir` in config → `<data-dir>/logs` (non-default data dir) → OS-standard location.
82+
7683
## Configuration Options
7784

7885
### Log Levels
@@ -383,4 +390,4 @@ The logging system follows established standards for each operating system:
383390
- **Windows**: [Application Data Guidelines](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid)
384391
- **Linux**: [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
385392

386-
This ensures that logs are stored in the expected locations for each platform, making them easy to find and manage.
393+
This ensures that logs are stored in the expected locations for each platform, making them easy to find and manage.

internal/logs/e2e_test.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,21 @@ func TestE2E_MCPProxyWithLogging(t *testing.T) {
336336
// Wait for the command to finish
337337
_ = cmd.Wait()
338338

339-
// Check if log file was created
340-
logFilePath, err := GetLogFilePath("mcpproxy-e2e-binary.log")
341-
require.NoError(t, err)
342-
339+
// Because the config sets a non-default data_dir (tempDir) and no
340+
// --log-dir was given, logs are co-located under <data-dir>/logs rather
341+
// than the shared OS-standard location (MCP-2250). This keeps test/e2e
342+
// runs from polluting the real ~/Library/Logs/mcpproxy/main.log.
343+
logFilePath := filepath.Join(tempDir, "logs", "mcpproxy-e2e-binary.log")
343344
_, err = os.Stat(logFilePath)
344-
assert.NoError(t, err, "Log file should be created by mcpproxy binary")
345+
assert.NoError(t, err, "Log file should be created under <data-dir>/logs by mcpproxy binary")
346+
347+
// The shared OS-standard location must NOT receive this run's log.
348+
osStdPath, osErr := GetLogFilePath("mcpproxy-e2e-binary.log")
349+
require.NoError(t, osErr)
350+
if _, statErr := os.Stat(osStdPath); statErr == nil {
351+
t.Errorf("custom data_dir run leaked its log into the shared OS-standard dir: %s", osStdPath)
352+
os.Remove(osStdPath)
353+
}
345354

346355
// Read and verify log content
347356
if err == nil {
@@ -351,9 +360,6 @@ func TestE2E_MCPProxyWithLogging(t *testing.T) {
351360
contentStr := string(content)
352361
assert.Contains(t, contentStr, "Starting mcpproxy", "Log should contain startup message")
353362
assert.Contains(t, contentStr, "Log directory configured", "Log should contain directory info")
354-
355-
// Clean up
356-
os.Remove(logFilePath)
357363
}
358364
}
359365

0 commit comments

Comments
 (0)