This commit is contained in:
2026-05-29 18:43:53 +02:00
parent a26914f244
commit b157142e97
7 changed files with 958 additions and 855 deletions

View File

@@ -1,38 +1,30 @@
FROM node:20-alpine FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Install compression tools for bz2, xz, zstd support
RUN apk add --no-cache bzip2 xz zstd
# Copy package files and install deps
COPY package.json package-lock.json* ./ COPY package.json package-lock.json* ./
RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev RUN npm ci
# Copy TypeScript config and source
COPY tsconfig.server.json ./ COPY tsconfig.server.json ./
COPY src/server/ ./src/server/ COPY src/server/ ./src/server/
RUN npx tsc -p tsconfig.server.json
FROM node:20-alpine
WORKDIR /app
RUN apk add --no-cache bzip2 xz zstd
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist-server/ ./dist-server/
COPY src/web/ ./src/web/ COPY src/web/ ./src/web/
# Install typescript for build, compile, then remove
RUN npm install typescript@5.7.2 && \
npx tsc -p tsconfig.server.json && \
npm uninstall typescript
# Copy sample logs
COPY sample-logs/ ./sample-logs/ COPY sample-logs/ ./sample-logs/
# Generate sample logs
RUN node sample-logs/generate-logs.js RUN node sample-logs/generate-logs.js
# Environment defaults
ENV LOG_SOURCES=/app/sample-logs/app.log,/app/sample-logs/nginx-access.log,/app/sample-logs/syslog,/app/sample-logs/archived.log.gz ENV LOG_SOURCES=/app/sample-logs/app.log,/app/sample-logs/nginx-access.log,/app/sample-logs/syslog,/app/sample-logs/archived.log.gz
ENV LOG_THEME=dark ENV LOG_THEME=dark
ENV LOG_REFRESH_INTERVAL=3000 ENV LOG_REFRESH_INTERVAL=3000
ENV LOG_TAIL_MODE=true ENV LOG_TAIL_MODE=true
ENV LOG_MODE=both
ENV PORT=8080 ENV PORT=8080
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/api/health || exit 1
CMD ["node", "dist-server/server.js"] CMD ["node", "dist-server/server.js"]

View File

@@ -21,6 +21,42 @@ Open http://localhost:8080 in your browser.
| Collector | `cp .env.collector .env && node dist-server/server.js` | 8081 | Receive & store logs | | Collector | `cp .env.collector .env && node dist-server/server.js` | 8081 | Receive & store logs |
| Monitor | `cp .env.monitor .env && node dist-server/server.js` | 8082 | Scan & alert on patterns | | Monitor | `cp .env.monitor .env && node dist-server/server.js` | 8082 | Scan & alert on patterns |
## Docker
```bash
docker pull git.kgessner.de/keviin/logmaster:latest
docker run -d -p 8080:8080 git.kgessner.de/keviin/logmaster:latest
```
**Wichtig:** Collector-Daten liegen im Container unter `/app/collector-data/`. Ohne Volume-Mount gehen diese bei Neustart verloren:
```bash
docker run -d -p 8080:8080 \
-v ./collector-data:/app/collector-data \
-v ./monitor-data:/app/monitor-data \
git.kgessner.de/keviin/logmaster:latest
```
**Health Check:** Der Container hat einen eingebauten HEALTHCHECK auf `/api/health`. Manuell prüfbar mit:
```bash
curl http://localhost:8080/api/health
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_MODE` | `both` | Mode: `viewer`, `collector`, `monitor`, `both`, `all` |
| `PORT` | `8080` | HTTP port |
| `HOST` | `0.0.0.0` | Bind address |
| `LOG_SOURCES` | _(empty)_ | Comma-separated file paths |
| `COLLECTOR_UDP_PORT` | `5140` | UDP syslog port |
| `MONITOR_ENABLED` | `true` | Enable monitor scanning |
| `MONITOR_SCAN_INTERVAL` | `60000` | Scan interval in ms |
| `MONITOR_RULES` | `[]` | JSON array of monitor rules |
| `REMOTE_COLLECTOR_URL` | _(empty)_ | URL to remote collector (viewer mode) |
## Documentation ## Documentation
- **[Full Documentation](DOCS.md)** — Complete reference - **[Full Documentation](DOCS.md)** — Complete reference

View File

@@ -3,25 +3,25 @@
"id": "web-app", "id": "web-app",
"name": "web-app", "name": "web-app",
"createdAt": "2026-05-01T19:00:40.860Z", "createdAt": "2026-05-01T19:00:40.860Z",
"lineCount": 195, "lineCount": 387,
"lastReceived": "2026-05-24T11:43:01.562Z", "lastReceived": "2026-05-24T11:47:08.882Z",
"currentFileSize": 11308 "currentFileSize": 22227
}, },
{ {
"id": "auth-service", "id": "auth-service",
"name": "auth-service", "name": "auth-service",
"createdAt": "2026-05-01T19:00:40.862Z", "createdAt": "2026-05-01T19:00:40.862Z",
"lineCount": 226, "lineCount": 436,
"lastReceived": "2026-05-24T11:42:59.719Z", "lastReceived": "2026-05-24T11:47:08.570Z",
"currentFileSize": 13002 "currentFileSize": 24871
}, },
{ {
"id": "worker-1", "id": "worker-1",
"name": "worker-1", "name": "worker-1",
"createdAt": "2026-05-01T19:00:43.603Z", "createdAt": "2026-05-01T19:00:43.603Z",
"lineCount": 220, "lineCount": 430,
"lastReceived": "2026-05-24T11:43:00.958Z", "lastReceived": "2026-05-24T11:47:08.259Z",
"currentFileSize": 12665 "currentFileSize": 24716
}, },
{ {
"id": "app-server", "id": "app-server",
@@ -43,9 +43,9 @@
"id": "payment-api", "id": "payment-api",
"name": "payment-api", "name": "payment-api",
"createdAt": "2026-05-24T10:48:07.019Z", "createdAt": "2026-05-24T10:48:07.019Z",
"lineCount": 233, "lineCount": 432,
"lastReceived": "2026-05-24T11:43:01.262Z", "lastReceived": "2026-05-24T11:47:07.635Z",
"currentFileSize": 13352 "currentFileSize": 24781
}, },
{ {
"id": "auth-svc", "id": "auth-svc",

View File

@@ -190,6 +190,18 @@ const server = http.createServer(async (req, res) => {
// ---- API Routes ---- // ---- API Routes ----
if (pathname.startsWith('/api/')) { if (pathname.startsWith('/api/')) {
try { try {
// GET /api/health
if (pathname === '/api/health' && method === 'GET') {
return sendJson(res, {
status: 'ok',
mode: MODE,
uptime: process.uptime(),
collector: collector ? 'active' : 'off',
monitor: monitor ? 'active' : 'off',
sources: logSourceManager.getSources().length,
s3Sources: s3Sources.size,
});
}
// GET /api/mode // GET /api/mode
if (pathname === '/api/mode' && method === 'GET') { if (pathname === '/api/mode' && method === 'GET') {
return sendJson(res, { mode: MODE, hasCollector: !!collector, hasRemoteCollector: !!REMOTE_COLLECTOR_URL, s3Sources: Array.from(s3Sources.keys()) }); return sendJson(res, { mode: MODE, hasCollector: !!collector, hasRemoteCollector: !!REMOTE_COLLECTOR_URL, s3Sources: Array.from(s3Sources.keys()) });
@@ -216,17 +228,29 @@ const server = http.createServer(async (req, res) => {
logSourceManager.removeSource(sourcePath); logSourceManager.removeSource(sourcePath);
return sendJson(res, logSourceManager.getSources()); return sendJson(res, logSourceManager.getSources());
} }
// GET /api/logs?source=... // GET /api/logs?source=...&level=...&offset=...
if (pathname === '/api/logs' && method === 'GET') { if (pathname === '/api/logs' && method === 'GET') {
const source = parsedUrl.query.source; const source = parsedUrl.query.source;
const levelFilter = parsedUrl.query.level;
const offsetParam = parsedUrl.query.offset;
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
let entries;
if (source && source !== 'ALL') { if (source && source !== 'ALL') {
const entries = await logSourceManager.readLog(source); entries = await logSourceManager.readLog(source);
return sendJson(res, entries);
} }
else { else {
const entries = await logSourceManager.readAllLogs(); entries = await logSourceManager.readAllLogs();
return sendJson(res, entries);
} }
// Apply level filter
if (levelFilter && levelFilter !== 'ALL') {
const filterUpper = levelFilter.toUpperCase();
entries = entries.filter((e) => e.level && e.level.toUpperCase() === filterUpper);
}
// Apply offset
if (offset > 0 && offset < entries.length) {
entries = entries.slice(offset);
}
return sendJson(res, entries);
} }
// ---- S3 endpoints ---- // ---- S3 endpoints ----
// POST /api/s3/add { name, bucket, prefix, accessKeyId, secretAccessKey, region, endpoint? } // POST /api/s3/add { name, bucket, prefix, accessKeyId, secretAccessKey, region, endpoint? }
@@ -251,9 +275,15 @@ const server = http.createServer(async (req, res) => {
const s3 = s3Sources.get(name); const s3 = s3Sources.get(name);
if (!s3) if (!s3)
return sendError(res, `S3 source not found: ${name}`, 404); return sendError(res, `S3 source not found: ${name}`, 404);
try {
const objects = await s3.listObjects(prefix); const objects = await s3.listObjects(prefix);
return sendJson(res, objects); return sendJson(res, objects);
} }
catch (err) {
console.warn(`[S3] Connection error for source "${name}":`, err.message);
return sendError(res, `S3 connection failed for "${name}": ${err.message}`, 502);
}
}
// GET /api/s3/read?name=...&key=... // GET /api/s3/read?name=...&key=...
if (pathname === '/api/s3/read' && method === 'GET') { if (pathname === '/api/s3/read' && method === 'GET') {
const name = parsedUrl.query.name; const name = parsedUrl.query.name;
@@ -263,6 +293,7 @@ const server = http.createServer(async (req, res) => {
return sendError(res, `S3 source not found: ${name}`, 404); return sendError(res, `S3 source not found: ${name}`, 404);
if (!key) if (!key)
return sendError(res, 'Missing key parameter', 400); return sendError(res, 'Missing key parameter', 400);
try {
const content = await s3.getObject(key); const content = await s3.getObject(key);
const lines = content.split('\n').filter(l => l.trim() !== ''); const lines = content.split('\n').filter(l => l.trim() !== '');
const entries = lines.map((line, i) => ({ const entries = lines.map((line, i) => ({
@@ -274,6 +305,11 @@ const server = http.createServer(async (req, res) => {
})); }));
return sendJson(res, entries); return sendJson(res, entries);
} }
catch (err) {
console.warn(`[S3] Read error for "${name}/${key}":`, err.message);
return sendError(res, `S3 read failed: ${err.message}`, 502);
}
}
// DELETE /api/s3/remove?name=... // DELETE /api/s3/remove?name=...
if (pathname === '/api/s3/remove' && method === 'DELETE') { if (pathname === '/api/s3/remove' && method === 'DELETE') {
const name = parsedUrl.query.name; const name = parsedUrl.query.name;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -162,6 +162,19 @@ const server = http.createServer(async (req, res) => {
// ---- API Routes ---- // ---- API Routes ----
if (pathname.startsWith('/api/')) { if (pathname.startsWith('/api/')) {
try { try {
// GET /api/health
if (pathname === '/api/health' && method === 'GET') {
return sendJson(res, {
status: 'ok',
mode: MODE,
uptime: process.uptime(),
collector: collector ? 'active' : 'off',
monitor: monitor ? 'active' : 'off',
sources: logSourceManager.getSources().length,
s3Sources: s3Sources.size,
});
}
// GET /api/mode // GET /api/mode
if (pathname === '/api/mode' && method === 'GET') { if (pathname === '/api/mode' && method === 'GET') {
return sendJson(res, { mode: MODE, hasCollector: !!collector, hasRemoteCollector: !!REMOTE_COLLECTOR_URL, s3Sources: Array.from(s3Sources.keys()) }); return sendJson(res, { mode: MODE, hasCollector: !!collector, hasRemoteCollector: !!REMOTE_COLLECTOR_URL, s3Sources: Array.from(s3Sources.keys()) });
@@ -192,16 +205,32 @@ const server = http.createServer(async (req, res) => {
return sendJson(res, logSourceManager.getSources()); return sendJson(res, logSourceManager.getSources());
} }
// GET /api/logs?source=... // GET /api/logs?source=...&level=...&offset=...
if (pathname === '/api/logs' && method === 'GET') { if (pathname === '/api/logs' && method === 'GET') {
const source = parsedUrl.query.source as string | undefined; const source = parsedUrl.query.source as string | undefined;
const levelFilter = parsedUrl.query.level as string | undefined;
const offsetParam = parsedUrl.query.offset as string | undefined;
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
let entries: any[];
if (source && source !== 'ALL') { if (source && source !== 'ALL') {
const entries = await logSourceManager.readLog(source); entries = await logSourceManager.readLog(source);
return sendJson(res, entries);
} else { } else {
const entries = await logSourceManager.readAllLogs(); entries = await logSourceManager.readAllLogs();
return sendJson(res, entries);
} }
// Apply level filter
if (levelFilter && levelFilter !== 'ALL') {
const filterUpper = levelFilter.toUpperCase();
entries = entries.filter((e: any) => e.level && e.level.toUpperCase() === filterUpper);
}
// Apply offset
if (offset > 0 && offset < entries.length) {
entries = entries.slice(offset);
}
return sendJson(res, entries);
} }
// ---- S3 endpoints ---- // ---- S3 endpoints ----
@@ -229,8 +258,13 @@ const server = http.createServer(async (req, res) => {
const prefix = parsedUrl.query.prefix as string | undefined; const prefix = parsedUrl.query.prefix as string | undefined;
const s3 = s3Sources.get(name); const s3 = s3Sources.get(name);
if (!s3) return sendError(res, `S3 source not found: ${name}`, 404); if (!s3) return sendError(res, `S3 source not found: ${name}`, 404);
try {
const objects = await s3.listObjects(prefix); const objects = await s3.listObjects(prefix);
return sendJson(res, objects); return sendJson(res, objects);
} catch (err) {
console.warn(`[S3] Connection error for source "${name}":`, (err as Error).message);
return sendError(res, `S3 connection failed for "${name}": ${(err as Error).message}`, 502);
}
} }
// GET /api/s3/read?name=...&key=... // GET /api/s3/read?name=...&key=...
@@ -240,6 +274,7 @@ const server = http.createServer(async (req, res) => {
const s3 = s3Sources.get(name); const s3 = s3Sources.get(name);
if (!s3) return sendError(res, `S3 source not found: ${name}`, 404); if (!s3) return sendError(res, `S3 source not found: ${name}`, 404);
if (!key) return sendError(res, 'Missing key parameter', 400); if (!key) return sendError(res, 'Missing key parameter', 400);
try {
const content = await s3.getObject(key); const content = await s3.getObject(key);
const lines = content.split('\n').filter(l => l.trim() !== ''); const lines = content.split('\n').filter(l => l.trim() !== '');
const entries = lines.map((line, i) => ({ const entries = lines.map((line, i) => ({
@@ -250,6 +285,10 @@ const server = http.createServer(async (req, res) => {
level: extractLevel(line), level: extractLevel(line),
})); }));
return sendJson(res, entries); return sendJson(res, entries);
} catch (err) {
console.warn(`[S3] Read error for "${name}/${key}":`, (err as Error).message);
return sendError(res, `S3 read failed: ${(err as Error).message}`, 502);
}
} }
// DELETE /api/s3/remove?name=... // DELETE /api/s3/remove?name=...