Final
This commit is contained in:
34
Dockerfile
34
Dockerfile
@@ -1,38 +1,30 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
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* ./
|
||||
RUN npm ci --omit=dev 2>/dev/null || npm install --omit=dev
|
||||
|
||||
# Copy TypeScript config and source
|
||||
RUN npm ci
|
||||
COPY tsconfig.server.json ./
|
||||
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/
|
||||
|
||||
# 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/
|
||||
|
||||
# Generate sample logs
|
||||
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_THEME=dark
|
||||
ENV LOG_REFRESH_INTERVAL=3000
|
||||
ENV LOG_TAIL_MODE=true
|
||||
ENV LOG_MODE=both
|
||||
ENV PORT=8080
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
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"]
|
||||
|
||||
36
README.md
36
README.md
@@ -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 |
|
||||
| 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
|
||||
|
||||
- **[Full Documentation](DOCS.md)** — Complete reference
|
||||
|
||||
@@ -3,25 +3,25 @@
|
||||
"id": "web-app",
|
||||
"name": "web-app",
|
||||
"createdAt": "2026-05-01T19:00:40.860Z",
|
||||
"lineCount": 195,
|
||||
"lastReceived": "2026-05-24T11:43:01.562Z",
|
||||
"currentFileSize": 11308
|
||||
"lineCount": 387,
|
||||
"lastReceived": "2026-05-24T11:47:08.882Z",
|
||||
"currentFileSize": 22227
|
||||
},
|
||||
{
|
||||
"id": "auth-service",
|
||||
"name": "auth-service",
|
||||
"createdAt": "2026-05-01T19:00:40.862Z",
|
||||
"lineCount": 226,
|
||||
"lastReceived": "2026-05-24T11:42:59.719Z",
|
||||
"currentFileSize": 13002
|
||||
"lineCount": 436,
|
||||
"lastReceived": "2026-05-24T11:47:08.570Z",
|
||||
"currentFileSize": 24871
|
||||
},
|
||||
{
|
||||
"id": "worker-1",
|
||||
"name": "worker-1",
|
||||
"createdAt": "2026-05-01T19:00:43.603Z",
|
||||
"lineCount": 220,
|
||||
"lastReceived": "2026-05-24T11:43:00.958Z",
|
||||
"currentFileSize": 12665
|
||||
"lineCount": 430,
|
||||
"lastReceived": "2026-05-24T11:47:08.259Z",
|
||||
"currentFileSize": 24716
|
||||
},
|
||||
{
|
||||
"id": "app-server",
|
||||
@@ -43,9 +43,9 @@
|
||||
"id": "payment-api",
|
||||
"name": "payment-api",
|
||||
"createdAt": "2026-05-24T10:48:07.019Z",
|
||||
"lineCount": 233,
|
||||
"lastReceived": "2026-05-24T11:43:01.262Z",
|
||||
"currentFileSize": 13352
|
||||
"lineCount": 432,
|
||||
"lastReceived": "2026-05-24T11:47:07.635Z",
|
||||
"currentFileSize": 24781
|
||||
},
|
||||
{
|
||||
"id": "auth-svc",
|
||||
|
||||
@@ -190,6 +190,18 @@ const server = http.createServer(async (req, res) => {
|
||||
// ---- API Routes ----
|
||||
if (pathname.startsWith('/api/')) {
|
||||
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
|
||||
if (pathname === '/api/mode' && method === 'GET') {
|
||||
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);
|
||||
return sendJson(res, logSourceManager.getSources());
|
||||
}
|
||||
// GET /api/logs?source=...
|
||||
// GET /api/logs?source=...&level=...&offset=...
|
||||
if (pathname === '/api/logs' && method === 'GET') {
|
||||
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') {
|
||||
const entries = await logSourceManager.readLog(source);
|
||||
return sendJson(res, entries);
|
||||
entries = await logSourceManager.readLog(source);
|
||||
}
|
||||
else {
|
||||
const entries = await logSourceManager.readAllLogs();
|
||||
return sendJson(res, entries);
|
||||
entries = await logSourceManager.readAllLogs();
|
||||
}
|
||||
// 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 ----
|
||||
// POST /api/s3/add { name, bucket, prefix, accessKeyId, secretAccessKey, region, endpoint? }
|
||||
@@ -251,8 +275,14 @@ const server = http.createServer(async (req, res) => {
|
||||
const s3 = s3Sources.get(name);
|
||||
if (!s3)
|
||||
return sendError(res, `S3 source not found: ${name}`, 404);
|
||||
const objects = await s3.listObjects(prefix);
|
||||
return sendJson(res, objects);
|
||||
try {
|
||||
const objects = await s3.listObjects(prefix);
|
||||
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=...
|
||||
if (pathname === '/api/s3/read' && method === 'GET') {
|
||||
@@ -263,16 +293,22 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendError(res, `S3 source not found: ${name}`, 404);
|
||||
if (!key)
|
||||
return sendError(res, 'Missing key parameter', 400);
|
||||
const content = await s3.getObject(key);
|
||||
const lines = content.split('\n').filter(l => l.trim() !== '');
|
||||
const entries = lines.map((line, i) => ({
|
||||
line,
|
||||
lineNumber: i + 1,
|
||||
source: `s3://${name}/${key}`,
|
||||
timestamp: extractTimestamp(line),
|
||||
level: extractLevel(line),
|
||||
}));
|
||||
return sendJson(res, entries);
|
||||
try {
|
||||
const content = await s3.getObject(key);
|
||||
const lines = content.split('\n').filter(l => l.trim() !== '');
|
||||
const entries = lines.map((line, i) => ({
|
||||
line,
|
||||
lineNumber: i + 1,
|
||||
source: `s3://${name}/${key}`,
|
||||
timestamp: extractTimestamp(line),
|
||||
level: extractLevel(line),
|
||||
}));
|
||||
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=...
|
||||
if (pathname === '/api/s3/remove' && method === 'DELETE') {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -162,6 +162,19 @@ const server = http.createServer(async (req, res) => {
|
||||
// ---- API Routes ----
|
||||
if (pathname.startsWith('/api/')) {
|
||||
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
|
||||
if (pathname === '/api/mode' && method === 'GET') {
|
||||
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());
|
||||
}
|
||||
|
||||
// GET /api/logs?source=...
|
||||
// GET /api/logs?source=...&level=...&offset=...
|
||||
if (pathname === '/api/logs' && method === 'GET') {
|
||||
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') {
|
||||
const entries = await logSourceManager.readLog(source);
|
||||
return sendJson(res, entries);
|
||||
entries = await logSourceManager.readLog(source);
|
||||
} else {
|
||||
const entries = await logSourceManager.readAllLogs();
|
||||
return sendJson(res, entries);
|
||||
entries = await logSourceManager.readAllLogs();
|
||||
}
|
||||
|
||||
// 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 ----
|
||||
@@ -229,8 +258,13 @@ const server = http.createServer(async (req, res) => {
|
||||
const prefix = parsedUrl.query.prefix as string | undefined;
|
||||
const s3 = s3Sources.get(name);
|
||||
if (!s3) return sendError(res, `S3 source not found: ${name}`, 404);
|
||||
const objects = await s3.listObjects(prefix);
|
||||
return sendJson(res, objects);
|
||||
try {
|
||||
const objects = await s3.listObjects(prefix);
|
||||
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=...
|
||||
@@ -240,16 +274,21 @@ const server = http.createServer(async (req, res) => {
|
||||
const s3 = s3Sources.get(name);
|
||||
if (!s3) return sendError(res, `S3 source not found: ${name}`, 404);
|
||||
if (!key) return sendError(res, 'Missing key parameter', 400);
|
||||
const content = await s3.getObject(key);
|
||||
const lines = content.split('\n').filter(l => l.trim() !== '');
|
||||
const entries = lines.map((line, i) => ({
|
||||
line,
|
||||
lineNumber: i + 1,
|
||||
source: `s3://${name}/${key}`,
|
||||
timestamp: extractTimestamp(line),
|
||||
level: extractLevel(line),
|
||||
}));
|
||||
return sendJson(res, entries);
|
||||
try {
|
||||
const content = await s3.getObject(key);
|
||||
const lines = content.split('\n').filter(l => l.trim() !== '');
|
||||
const entries = lines.map((line, i) => ({
|
||||
line,
|
||||
lineNumber: i + 1,
|
||||
source: `s3://${name}/${key}`,
|
||||
timestamp: extractTimestamp(line),
|
||||
level: extractLevel(line),
|
||||
}));
|
||||
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=...
|
||||
|
||||
Reference in New Issue
Block a user