Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Now edit files in `ui/src/` and see changes live in your browser!

```bash
ssh root@192.168.1.100
tail -f /userdata/jetkvm/last.log
tail -f /userdata/jetkvm/app.log
```

---
Expand Down Expand Up @@ -200,11 +200,11 @@ ssh root@192.168.1.100 ps aux | grep jetkvm

### View live logs

The file `/userdata/jetkvm/last.log` contains the JetKVM logs. You can view live logs with:
The file `/userdata/jetkvm/app.log` contains the JetKVM logs. You can view live logs with:

```bash
ssh root@192.168.1.100
tail -f /userdata/jetkvm/last.log
tail -f /userdata/jetkvm/app.log
```

### Reset everything (if stuck)
Expand All @@ -230,7 +230,7 @@ The code and GDB server will be deployed automatically.
1. Deploy your changes: `./dev_deploy.sh -r <IP>`
2. Open browser: `http://<IP>`
3. Test your feature
4. Check logs: `ssh root@<IP> tail -f /userdata/jetkvm/last.log`
4. Check logs: `ssh root@<IP> tail -f /userdata/jetkvm/app.log`

### Automated Testing

Expand Down Expand Up @@ -425,7 +425,7 @@ export JETKVM_PROXY_URL="ws://<IP>"

## Need Help?

1. **Check logs first:** `ssh root@<IP> tail -f /userdata/jetkvm/last.log`
1. **Check logs first:** `ssh root@<IP> tail -f /userdata/jetkvm/app.log`
2. **Search issues:** [GitHub Issues](https://github.com/jetkvm/kvm/issues)
3. **Ask on Discord:** [JetKVM Discord](https://jetkvm.com/discord)
4. **Read docs:** [JetKVM Documentation](https://jetkvm.com/docs)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/coder/websocket v1.8.14
github.com/coreos/go-oidc/v3 v3.16.0
github.com/creack/pty v1.1.24
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/logger v1.2.6
Expand Down Expand Up @@ -41,6 +42,7 @@ require (
golang.org/x/sys v0.37.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)

replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
Expand All @@ -54,7 +56,6 @@ require (
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
23 changes: 22 additions & 1 deletion internal/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/rs/zerolog"
"gopkg.in/natefinch/lumberjack.v2"
)

type Logger struct {
Expand All @@ -24,6 +26,9 @@ type Logger struct {

const (
defaultLogLevel = zerolog.ErrorLevel
AppLogPath = "/userdata/jetkvm/app.log"
appLogMaxSizeMB = 50
appLogBackups = 1
)

type logOutput struct {
Expand All @@ -44,6 +49,22 @@ func (w *logOutput) Write(p []byte) (n int, err error) {
return len(p), nil
}

func newDefaultLogOutput() io.Writer {
writers := []io.Writer{consoleLogOutput, fileLogOutput}

dir := filepath.Dir(AppLogPath)
if _, err := os.Stat(dir); err == nil {
writers = append(writers, &lumberjack.Logger{
Filename: AppLogPath,
MaxSize: appLogMaxSizeMB,
MaxBackups: appLogBackups,
Compress: false,
})
}

return zerolog.MultiLevelWriter(writers...)
}

var (
consoleLogOutput io.Writer = zerolog.ConsoleWriter{
Out: os.Stdout,
Expand All @@ -61,7 +82,7 @@ var (
},
}
fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}}
defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput)
defaultLogOutput = newDefaultLogOutput()

zerologLevels = map[string]zerolog.Level{
"DISABLE": zerolog.Disabled,
Expand Down
2 changes: 1 addition & 1 deletion internal/supervisor/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const (
ErrorDumpDir = "/userdata/jetkvm/crashdump" // The error dump directory is the directory where the error dumps are stored
ErrorDumpLastFile = "last-crash.log" // The error dump last file is the last error dump file
ErrorDumpTemplate = "jetkvm-%s.log" // The error dump template is the template for the error dump file
AppLogPath = "/userdata/jetkvm/last.log" // The application stdout/stderr log file
AppLogPath = "/userdata/jetkvm/app.log" // The application log file (managed by lumberjack)

FailsafeReasonVideoMaxRestartAttemptsReached = "failsafe::video.max_restart_attempts_reached"
)
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func Main() {
logger.Warn().Err(err).Msg("failed to get local version")
}

logger.Info().
logger.Log().
Interface("system_version", systemVersionLocal).
Interface("app_version", appVersionLocal).
Msg("starting JetKVM")
Expand Down
2 changes: 1 addition & 1 deletion ui/e2e/global-teardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default async function globalTeardown() {
fs.mkdirSync(logDir, { recursive: true });

const logs: Record<string, string> = {
"device-last.log": "cat /userdata/jetkvm/last.log",
"device-app.log": "cat /userdata/jetkvm/app.log",
"device-config.json": "cat /userdata/kvm_config.json",
"device-dmesg.txt": "dmesg | tail -200",
};
Expand Down
2 changes: 1 addition & 1 deletion ui/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ export async function restartAppViaSSH(): Promise<void> {
await sshExec("killall jetkvm_app", true);
await new Promise(r => setTimeout(r, 500));
await sshExec(
"setsid env LD_LIBRARY_PATH=/oem/usr/lib:/oem/lib /userdata/jetkvm/bin/jetkvm_app > /userdata/jetkvm/last.log 2>&1 &",
"setsid env LD_LIBRARY_PATH=/oem/usr/lib:/oem/lib /userdata/jetkvm/bin/jetkvm_app > /dev/null 2>&1 &",
true,
);
await new Promise(r => setTimeout(r, 1000));
Expand Down
59 changes: 59 additions & 0 deletions ui/e2e/log-rotation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { test, expect } from "@playwright/test";

import { sshExec, getDeviceHost, waitForDeviceReady } from "./helpers";

const APP_LOG_PATH = "/userdata/jetkvm/app.log";
const MAX_LOG_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB

test.describe("Log rotation and diagnostics", () => {
test("app.log exists and contains version banner", async () => {
const ls = await sshExec(`ls -la ${APP_LOG_PATH}`);
expect(ls).toContain("app.log");

const appVersion = await sshExec(`grep "app_version" ${APP_LOG_PATH}`);
expect(appVersion.trim().length).toBeGreaterThan(0);

const systemVersion = await sshExec(`grep "system_version" ${APP_LOG_PATH}`);
expect(systemVersion.trim().length).toBeGreaterThan(0);
});

test("app.log is within the size limit", async () => {
const sizeStr = await sshExec(`stat -c%s ${APP_LOG_PATH}`);
const size = parseInt(sizeStr.trim(), 10);
expect(size).toBeGreaterThan(0);
expect(size).toBeLessThan(MAX_LOG_SIZE_BYTES);
});

test("diagnostics zip contains app.log", async ({ page }) => {
const host = getDeviceHost();
const resp = await page.request.get(`http://${host}/diagnostics`);
expect(resp.status()).toBe(200);

const body = await resp.body();
expect(body.length).toBeGreaterThan(0);

const zipContent = body.toString("binary");
expect(zipContent).toContain("app.log");
});

test("app.log continues growing after restart", async () => {
test.setTimeout(120_000);

const host = getDeviceHost();

const beforeLines = await sshExec(`wc -l < ${APP_LOG_PATH}`);
const lineCountBefore = parseInt(beforeLines.trim(), 10);

await sshExec("killall jetkvm_app", true);
await new Promise(r => setTimeout(r, 2000));

await waitForDeviceReady(host, 60000);

const afterLines = await sshExec(`wc -l < ${APP_LOG_PATH}`);
const lineCountAfter = parseInt(afterLines.trim(), 10);
expect(lineCountAfter).toBeGreaterThan(lineCountBefore);

const versionAfterRestart = await sshExec(`grep "app_version" ${APP_LOG_PATH}`);
expect(versionAfterRestart.trim().length).toBeGreaterThan(0);
});
});
Loading