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
|
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"]
|
||||||
|
|||||||
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 |
|
| 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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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=...
|
||||||
|
|||||||
Reference in New Issue
Block a user