v0.1.2 update readme and config, failed attempt to fix slow client
This commit is contained in:
265
README.md
265
README.md
@ -1,3 +1,7 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="assets/logo.svg" alt="LogWisp Logo" width="200"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
# LogWisp - Simple Log Streaming
|
# LogWisp - Simple Log Streaming
|
||||||
|
|
||||||
A lightweight log streaming service that monitors files and streams updates via Server-Sent Events (SSE).
|
A lightweight log streaming service that monitors files and streams updates via Server-Sent Events (SSE).
|
||||||
@ -16,6 +20,7 @@ LogWisp follows the Unix philosophy: do one thing and do it well. It monitors lo
|
|||||||
- Environment variable support
|
- Environment variable support
|
||||||
- Simple TOML configuration
|
- Simple TOML configuration
|
||||||
- Atomic configuration management
|
- Atomic configuration management
|
||||||
|
- Optional ANSI color pass-through
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@ -34,13 +39,61 @@ LogWisp follows the Unix philosophy: do one thing and do it well. It monitors lo
|
|||||||
curl -N http://localhost:8080/stream
|
curl -N http://localhost:8080/stream
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Command Line Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
logwisp [OPTIONS] [TARGET...]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-c, --color Enable color pass-through for ANSI escape codes
|
||||||
|
--config FILE Config file path (default: ~/.config/logwisp.toml)
|
||||||
|
--port PORT HTTP port (default: 8080)
|
||||||
|
--buffer-size SIZE Stream buffer size (default: 1000)
|
||||||
|
--check-interval MS File check interval in ms (default: 100)
|
||||||
|
--rate-limit Enable rate limiting
|
||||||
|
--rate-requests N Rate limit requests/sec (default: 10)
|
||||||
|
--rate-burst N Rate limit burst size (default: 20)
|
||||||
|
|
||||||
|
TARGET:
|
||||||
|
path[:pattern[:isfile]] Path to monitor (file or directory)
|
||||||
|
pattern: glob pattern for directories (default: *.log)
|
||||||
|
isfile: true/false (auto-detected if omitted)
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
# Monitor current directory for *.log files
|
||||||
|
logwisp
|
||||||
|
|
||||||
|
# Monitor specific file with color support
|
||||||
|
logwisp -c /var/log/app.log
|
||||||
|
|
||||||
|
# Monitor multiple locations
|
||||||
|
logwisp /var/log:*.log /app/logs:error*.log:false /tmp/debug.log::true
|
||||||
|
|
||||||
|
# Custom port with rate limiting
|
||||||
|
logwisp --port 9090 --rate-limit --rate-requests 100 --rate-burst 200
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
LogWisp uses a three-level configuration hierarchy:
|
LogWisp uses a three-level configuration hierarchy:
|
||||||
|
|
||||||
1. **Environment variables** (highest priority)
|
1. **Command-line arguments** (highest priority)
|
||||||
2. **Configuration file** (~/.config/logwisp.toml)
|
2. **Environment variables**
|
||||||
3. **Default values** (lowest priority)
|
3. **Configuration file** (~/.config/logwisp.toml)
|
||||||
|
4. **Default values** (lowest priority)
|
||||||
|
|
||||||
|
### Default Values
|
||||||
|
|
||||||
|
| Setting | Default | Description |
|
||||||
|
|---------|---------|-------------|
|
||||||
|
| `port` | 8080 | HTTP listen port |
|
||||||
|
| `monitor.check_interval_ms` | 100 | File check interval (milliseconds) |
|
||||||
|
| `monitor.targets` | [{"path": "./", "pattern": "*.log", "is_file": false}] | Paths to monitor |
|
||||||
|
| `stream.buffer_size` | 1000 | Per-client event buffer size |
|
||||||
|
| `stream.rate_limit.enabled` | false | Enable rate limiting |
|
||||||
|
| `stream.rate_limit.requests_per_second` | 10 | Sustained request rate |
|
||||||
|
| `stream.rate_limit.burst_size` | 20 | Maximum burst size |
|
||||||
|
| `stream.rate_limit.cleanup_interval_s` | 60 | Client cleanup interval |
|
||||||
|
|
||||||
### Configuration File Location
|
### Configuration File Location
|
||||||
|
|
||||||
@ -75,7 +128,7 @@ All configuration values can be overridden via environment variables:
|
|||||||
| `LOGWISP_STREAM_RATE_LIMIT_ENABLED` | `stream.rate_limit.enabled` | Enable rate limiting |
|
| `LOGWISP_STREAM_RATE_LIMIT_ENABLED` | `stream.rate_limit.enabled` | Enable rate limiting |
|
||||||
| `LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC` | `stream.rate_limit.requests_per_second` | Rate limit |
|
| `LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC` | `stream.rate_limit.requests_per_second` | Rate limit |
|
||||||
| `LOGWISP_STREAM_RATE_LIMIT_BURST_SIZE` | `stream.rate_limit.burst_size` | Burst size |
|
| `LOGWISP_STREAM_RATE_LIMIT_BURST_SIZE` | `stream.rate_limit.burst_size` | Burst size |
|
||||||
| `LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL` | `stream.rate_limit.cleanup_interval_s` | Cleanup interval |
|
| `LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL_S` | `stream.rate_limit.cleanup_interval_s` | Cleanup interval |
|
||||||
|
|
||||||
### Monitor Targets Format
|
### Monitor Targets Format
|
||||||
|
|
||||||
@ -93,17 +146,22 @@ LOGWISP_MONITOR_TARGETS="/var/log:*.log:false,/app/app.log::true" ./logwisp
|
|||||||
LOGWISP_MONITOR_TARGETS="/var/log:*.log:false,/opt/app/logs:app-*.log:false" ./logwisp
|
LOGWISP_MONITOR_TARGETS="/var/log:*.log:false,/opt/app/logs:app-*.log:false" ./logwisp
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example Configuration
|
### Complete Configuration Example
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
# Port to listen on (default: 8080)
|
||||||
port = 8080
|
port = 8080
|
||||||
|
|
||||||
[monitor]
|
[monitor]
|
||||||
|
# How often to check for file changes in milliseconds (default: 100)
|
||||||
check_interval_ms = 100
|
check_interval_ms = 100
|
||||||
|
|
||||||
# Monitor directory (all .log files)
|
# Paths to monitor
|
||||||
|
# Default: [{"path": "./", "pattern": "*.log", "is_file": false}]
|
||||||
|
|
||||||
|
# Monitor all .log files in current directory
|
||||||
[[monitor.targets]]
|
[[monitor.targets]]
|
||||||
path = "/var/log"
|
path = "./"
|
||||||
pattern = "*.log"
|
pattern = "*.log"
|
||||||
is_file = false
|
is_file = false
|
||||||
|
|
||||||
@ -120,12 +178,24 @@ pattern = "access*.log"
|
|||||||
is_file = false
|
is_file = false
|
||||||
|
|
||||||
[stream]
|
[stream]
|
||||||
|
# Buffer size for each client connection (default: 1000)
|
||||||
|
# Controls how many log entries can be queued per client
|
||||||
buffer_size = 1000
|
buffer_size = 1000
|
||||||
|
|
||||||
[stream.rate_limit]
|
[stream.rate_limit]
|
||||||
enabled = true
|
# Enable rate limiting (default: false)
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
# Requests per second per client (default: 10)
|
||||||
|
# This is the sustained rate
|
||||||
requests_per_second = 10
|
requests_per_second = 10
|
||||||
|
|
||||||
|
# Burst size - max requests at once (default: 20)
|
||||||
|
# Allows temporary bursts above the sustained rate
|
||||||
burst_size = 20
|
burst_size = 20
|
||||||
|
|
||||||
|
# How often to clean up old client limiters in seconds (default: 60)
|
||||||
|
# Clients inactive for 2x this duration are removed
|
||||||
cleanup_interval_s = 60
|
cleanup_interval_s = 60
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -141,7 +211,7 @@ LogWisp can pass through ANSI color escape codes from monitored logs to SSE clie
|
|||||||
ExecStart=/opt/logwisp/bin/logwisp -c
|
ExecStart=/opt/logwisp/bin/logwisp -c
|
||||||
```
|
```
|
||||||
|
|
||||||
## How It Works
|
### How It Works
|
||||||
|
|
||||||
When color mode is enabled (`-c` flag), LogWisp preserves ANSI escape codes in log messages. These are properly JSON-escaped in the SSE stream.
|
When color mode is enabled (`-c` flag), LogWisp preserves ANSI escape codes in log messages. These are properly JSON-escaped in the SSE stream.
|
||||||
|
|
||||||
@ -163,9 +233,9 @@ SSE output with `-c`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Client-Side Handling
|
### Client-Side Handling
|
||||||
|
|
||||||
### Terminal Clients
|
#### Terminal Clients
|
||||||
|
|
||||||
For terminal-based clients (like curl), the escape codes will render as colors:
|
For terminal-based clients (like curl), the escape codes will render as colors:
|
||||||
|
|
||||||
@ -174,7 +244,7 @@ For terminal-based clients (like curl), the escape codes will render as colors:
|
|||||||
curl -N http://localhost:8080/stream | jq -r '.message'
|
curl -N http://localhost:8080/stream | jq -r '.message'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web Clients
|
#### Web Clients
|
||||||
|
|
||||||
For web-based clients, you'll need to convert ANSI codes to HTML:
|
For web-based clients, you'll need to convert ANSI codes to HTML:
|
||||||
|
|
||||||
@ -190,7 +260,7 @@ eventSource.onmessage = (event) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Processing
|
#### Custom Processing
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Python example with colorama
|
# Python example with colorama
|
||||||
@ -245,13 +315,13 @@ To strip color codes instead of passing them through:
|
|||||||
### Endpoints
|
### Endpoints
|
||||||
|
|
||||||
- `GET /stream` - Server-Sent Events stream of log entries
|
- `GET /stream` - Server-Sent Events stream of log entries
|
||||||
- `GET /status` - Service status information
|
- `GET /status` - Service status and configuration information
|
||||||
|
|
||||||
### Log Entry Format
|
### Log Entry Format
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"time": "2024-01-01T12:00:00Z",
|
"time": "2024-01-01T12:00:00.123456Z",
|
||||||
"source": "app.log",
|
"source": "app.log",
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"message": "Something went wrong",
|
"message": "Something went wrong",
|
||||||
@ -259,6 +329,48 @@ To strip color codes instead of passing them through:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### SSE Event Types
|
||||||
|
|
||||||
|
| Event | Description | Data Format |
|
||||||
|
|-------|-------------|-------------|
|
||||||
|
| `connected` | Initial connection | `{"client_id": "123456789"}` |
|
||||||
|
| `data` | Log entry | JSON log entry |
|
||||||
|
| `disconnected` | Client disconnected | `{"reason": "slow_client"}` |
|
||||||
|
| `timeout` | Client timeout | `{"reason": "client_timeout"}` |
|
||||||
|
| `:` | Heartbeat (comment) | ISO timestamp |
|
||||||
|
|
||||||
|
### Status Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"service": "LogWisp",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"port": 8080,
|
||||||
|
"color_mode": false,
|
||||||
|
"config": {
|
||||||
|
"monitor": {
|
||||||
|
"check_interval_ms": 100,
|
||||||
|
"targets_count": 2
|
||||||
|
},
|
||||||
|
"stream": {
|
||||||
|
"buffer_size": 1000,
|
||||||
|
"rate_limit": {
|
||||||
|
"enabled": true,
|
||||||
|
"requests_per_second": 10,
|
||||||
|
"burst_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"streamer": {
|
||||||
|
"active_clients": 5,
|
||||||
|
"buffer_size": 1000,
|
||||||
|
"color_mode": false,
|
||||||
|
"total_dropped": 42
|
||||||
|
},
|
||||||
|
"rate_limiter": "Active clients: 3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
@ -266,6 +378,9 @@ To strip color codes instead of passing them through:
|
|||||||
# Start with defaults
|
# Start with defaults
|
||||||
./logwisp
|
./logwisp
|
||||||
|
|
||||||
|
# Monitor specific file
|
||||||
|
./logwisp /var/log/app.log
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
curl -N http://localhost:8080/stream
|
curl -N http://localhost:8080/stream
|
||||||
```
|
```
|
||||||
@ -281,6 +396,9 @@ LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC=5 \
|
|||||||
|
|
||||||
### Monitor Multiple Locations
|
### Monitor Multiple Locations
|
||||||
```bash
|
```bash
|
||||||
|
# Via command line
|
||||||
|
./logwisp /var/log:*.log /app/logs:*.json /tmp/debug.log
|
||||||
|
|
||||||
# Via environment variable
|
# Via environment variable
|
||||||
LOGWISP_MONITOR_TARGETS="/var/log:*.log:false,/app/logs:*.json:false,/tmp/debug.log::true" \
|
LOGWISP_MONITOR_TARGETS="/var/log:*.log:false,/app/logs:*.json:false,/tmp/debug.log::true" \
|
||||||
./logwisp
|
./logwisp
|
||||||
@ -315,11 +433,13 @@ After=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=logwisp
|
User=logwisp
|
||||||
ExecStart=/usr/local/bin/logwisp
|
ExecStart=/usr/local/bin/logwisp -c
|
||||||
Restart=always
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
# Environment overrides
|
# Environment overrides
|
||||||
Environment="LOGWISP_PORT=8080"
|
Environment="LOGWISP_PORT=8080"
|
||||||
|
Environment="LOGWISP_STREAM_BUFFER_SIZE=5000"
|
||||||
Environment="LOGWISP_STREAM_RATE_LIMIT_ENABLED=true"
|
Environment="LOGWISP_STREAM_RATE_LIMIT_ENABLED=true"
|
||||||
Environment="LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC=100"
|
Environment="LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC=100"
|
||||||
Environment="LOGWISP_MONITOR_TARGETS=/var/log:*.log:false,/opt/app/logs:*.log:false"
|
Environment="LOGWISP_MONITOR_TARGETS=/var/log:*.log:false,/opt/app/logs:*.log:false"
|
||||||
@ -330,25 +450,92 @@ PrivateTmp=true
|
|||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ReadOnlyPaths=/
|
ReadOnlyPaths=/
|
||||||
ReadWritePaths=/var/log
|
ReadWritePaths=/var/log /opt/app/logs
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rate Limiting
|
## Performance Tuning
|
||||||
|
|
||||||
When enabled, rate limiting is applied per client IP address:
|
### Buffer Size
|
||||||
|
|
||||||
```toml
|
The `stream.buffer_size` setting controls how many log entries can be queued per client:
|
||||||
[stream.rate_limit]
|
- **Small buffers (100-500)**: Lower memory usage, clients skip entries during bursts
|
||||||
enabled = true
|
- **Default (1000)**: Good balance for most use cases
|
||||||
requests_per_second = 10 # Sustained rate
|
- **Large buffers (5000+)**: Handle burst traffic better, higher memory usage
|
||||||
burst_size = 20 # Allow bursts up to this size
|
|
||||||
cleanup_interval_s = 60 # Clean old clients every minute
|
|
||||||
```
|
|
||||||
|
|
||||||
Rate limiting uses the `X-Forwarded-For` header if present, falling back to `RemoteAddr`.
|
When a client's buffer is full, new messages are skipped for that client until it catches up. The client remains connected and will receive future messages once buffer space is available.
|
||||||
|
|
||||||
|
### Check Interval
|
||||||
|
|
||||||
|
The `monitor.check_interval_ms` setting controls file polling frequency:
|
||||||
|
- **Fast (10-50ms)**: Near real-time updates, higher CPU usage
|
||||||
|
- **Default (100ms)**: Good balance
|
||||||
|
- **Slow (500ms+)**: Lower CPU usage, more latency
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
When to enable rate limiting:
|
||||||
|
- Internet-facing deployments
|
||||||
|
- Shared environments
|
||||||
|
- Protection against misbehaving clients
|
||||||
|
|
||||||
|
Rate limiting applies only to establishing SSE connections, not to individual messages. Once connected, clients receive all messages (subject to buffer capacity).
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Client Missing Messages
|
||||||
|
|
||||||
|
If clients miss messages during bursts:
|
||||||
|
1. Check `total_dropped` and `clients_with_drops` in status endpoint
|
||||||
|
2. Increase `stream.buffer_size` to handle larger bursts
|
||||||
|
3. Messages are skipped when buffer is full, but clients stay connected
|
||||||
|
|
||||||
|
### High Memory Usage
|
||||||
|
|
||||||
|
If memory usage is high:
|
||||||
|
1. Reduce `stream.buffer_size`
|
||||||
|
2. Enable rate limiting to limit concurrent connections
|
||||||
|
3. Each client uses `buffer_size * avg_message_size` memory
|
||||||
|
|
||||||
|
### Browser Stops Receiving Updates
|
||||||
|
|
||||||
|
This shouldn't happen with the current implementation. If it does:
|
||||||
|
1. Check browser developer console for errors
|
||||||
|
2. Verify no proxy/firewall is timing out the connection
|
||||||
|
3. Ensure reverse proxy (if used) doesn't buffer SSE responses
|
||||||
|
|
||||||
|
## File Rotation Detection
|
||||||
|
|
||||||
|
LogWisp automatically detects log file rotation using multiple methods:
|
||||||
|
- Inode changes (Linux/Unix)
|
||||||
|
- File size decrease
|
||||||
|
- Modification time reset
|
||||||
|
- Read position beyond file size
|
||||||
|
|
||||||
|
When rotation is detected, LogWisp:
|
||||||
|
1. Logs a rotation event
|
||||||
|
2. Resets read position to beginning
|
||||||
|
3. Continues streaming from new file
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
1. **No built-in authentication** - Use a reverse proxy for auth
|
||||||
|
2. **No TLS support** - Use a reverse proxy for HTTPS
|
||||||
|
3. **Path validation** - Only specified paths can be monitored
|
||||||
|
4. **Directory traversal protection** - Paths containing ".." are rejected
|
||||||
|
5. **Rate limiting** - Optional but recommended for public deployments
|
||||||
|
6. **ANSI escape sequences** - Only enable color mode for trusted log sources
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
- **Unix philosophy**: Single purpose - stream logs
|
||||||
|
- **SSE over WebSocket**: Simpler, works everywhere, built-in reconnect
|
||||||
|
- **No database**: Stateless operation, instant startup
|
||||||
|
- **Atomic config management**: Using LixenWraith/config package
|
||||||
|
- **Graceful shutdown**: Proper cleanup on SIGINT/SIGTERM
|
||||||
|
- **Platform agnostic**: POSIX-compliant where possible
|
||||||
|
|
||||||
## Building from Source
|
## Building from Source
|
||||||
|
|
||||||
@ -356,32 +543,12 @@ Requirements:
|
|||||||
- Go 1.23 or later
|
- Go 1.23 or later
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone https://github.com/yourusername/logwisp
|
||||||
|
cd logwisp
|
||||||
go mod download
|
go mod download
|
||||||
go build -o logwisp ./src/cmd/logwisp
|
go build -o logwisp ./src/cmd/logwisp
|
||||||
```
|
```
|
||||||
|
|
||||||
## File Rotation Detection
|
|
||||||
|
|
||||||
LogWisp automatically detects log file rotation by:
|
|
||||||
- Monitoring file inode changes (Linux/Unix)
|
|
||||||
- Detecting file size decrease
|
|
||||||
- Resetting read position when rotation is detected
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
1. **No built-in authentication** - Use a reverse proxy for auth
|
|
||||||
2. **No TLS support** - Use a reverse proxy for HTTPS
|
|
||||||
3. **Path validation** - Monitors only specified paths
|
|
||||||
4. **Rate limiting** - Optional but recommended for internet-facing deployments
|
|
||||||
|
|
||||||
## Design Decisions
|
|
||||||
|
|
||||||
- **Unix philosophy**: Single purpose - stream logs
|
|
||||||
- **No CLI arguments**: Configuration via file and environment only
|
|
||||||
- **SSE over WebSocket**: Simpler, works everywhere
|
|
||||||
- **Atomic config management**: Using LixenWraith/config package
|
|
||||||
- **Graceful shutdown**: Proper cleanup on SIGINT/SIGTERM
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
BSD-3-Clause
|
BSD-3-Clause
|
||||||
33
assets/logo.svg
Normal file
33
assets/logo.svg
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<svg width="200" height="200" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!--
|
||||||
|
LogWisp Logo - Vibrant Tech Stream
|
||||||
|
- Shape: 5 swirling streams emanating from a center point.
|
||||||
|
- Style: Wiry, energetic, and modern, suggesting data flow or a galaxy.
|
||||||
|
- Color: Radiant gradient from a bright core to a vibrant teal.
|
||||||
|
-->
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="vibrant-glow" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
||||||
|
<stop offset="0%" stop-color="#E0FFFF" /> <!-- Bright, almost white cyan at the core -->
|
||||||
|
<stop offset="100%" stop-color="#00B0B0" /> <!-- Deeper teal/cyan at the tips -->
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wisp-path {
|
||||||
|
fill: none;
|
||||||
|
stroke: url(#vibrant-glow);
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<g id="wisps">
|
||||||
|
<!-- A single path shape is defined, then rotated 5 times around the center (50, 50) -->
|
||||||
|
<!-- The path is a cubic Bezier curve: M(start) C(control1, control2, end) -->
|
||||||
|
<path class="wisp-path" d="M50,50 C58,30 85,25 95,50" transform="rotate(0, 50, 50)" />
|
||||||
|
<path class="wisp-path" d="M50,50 C58,30 85,25 95,50" transform="rotate(72, 50, 50)" />
|
||||||
|
<path class="wisp-path" d="M50,50 C58,30 85,25 95,50" transform="rotate(144, 50, 50)" />
|
||||||
|
<path class="wisp-path" d="M50,50 C58,30 85,25 95,50" transform="rotate(216, 50, 50)" />
|
||||||
|
<path class="wisp-path" d="M50,50 C58,30 85,25 95,50" transform="rotate(288, 50, 50)" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,70 +1,112 @@
|
|||||||
# LogWisp Configuration
|
# LogWisp Configuration Template
|
||||||
# Default location: ~/.config/logwisp.toml
|
# Default location: ~/.config/logwisp.toml
|
||||||
# Override with: LOGWISP_CONFIG_DIR and LOGWISP_CONFIG_FILE
|
#
|
||||||
|
# Configuration precedence (highest to lowest):
|
||||||
|
# 1. Command-line arguments
|
||||||
|
# 2. Environment variables (LOGWISP_ prefix)
|
||||||
|
# 3. This configuration file
|
||||||
|
# 4. Built-in defaults
|
||||||
|
#
|
||||||
|
# All settings shown below with their default values
|
||||||
|
|
||||||
# Port to listen on
|
# Port to listen on
|
||||||
# Environment: LOGWISP_PORT
|
# Environment: LOGWISP_PORT
|
||||||
|
# CLI: --port PORT
|
||||||
|
# Default: 8080
|
||||||
port = 8080
|
port = 8080
|
||||||
# Environment: LOGWISP_COLOR
|
|
||||||
color = false
|
|
||||||
|
|
||||||
[monitor]
|
[monitor]
|
||||||
# How often to check for file changes (milliseconds)
|
# How often to check for file changes (milliseconds)
|
||||||
|
# Lower values = more responsive but higher CPU usage
|
||||||
# Environment: LOGWISP_MONITOR_CHECK_INTERVAL_MS
|
# Environment: LOGWISP_MONITOR_CHECK_INTERVAL_MS
|
||||||
|
# CLI: --check-interval MS
|
||||||
|
# Default: 100
|
||||||
check_interval_ms = 100
|
check_interval_ms = 100
|
||||||
|
|
||||||
# Paths to monitor
|
# Paths to monitor for log files
|
||||||
# Environment: LOGWISP_MONITOR_TARGETS (format: "path:pattern:isfile,path2:pattern2:isfile")
|
# Environment: LOGWISP_MONITOR_TARGETS (format: "path:pattern:isfile,path2:pattern2:isfile")
|
||||||
# Example: LOGWISP_MONITOR_TARGETS="/var/log:*.log:false,/app/app.log::true"
|
# CLI: logwisp [path[:pattern[:isfile]]] ...
|
||||||
|
# Default: Monitor current directory for *.log files
|
||||||
# Monitor all .log files in current directory
|
|
||||||
[[monitor.targets]]
|
[[monitor.targets]]
|
||||||
path = "./"
|
path = "./" # Directory or file path to monitor
|
||||||
pattern = "*.log"
|
pattern = "*.log" # Glob pattern for directory monitoring (ignored for files)
|
||||||
is_file = false
|
is_file = false # true = monitor specific file, false = monitor directory
|
||||||
|
|
||||||
# Monitor all logs in /var/log
|
# Additional target examples (uncomment to use):
|
||||||
[[monitor.targets]]
|
|
||||||
path = "/var/log"
|
|
||||||
pattern = "*.log"
|
|
||||||
is_file = false
|
|
||||||
|
|
||||||
# Monitor specific application log file
|
# # Monitor specific log file
|
||||||
#[[monitor.targets]]
|
# [[monitor.targets]]
|
||||||
#path = "/home/user/app/app.log"
|
# path = "/var/log/application.log"
|
||||||
#pattern = "" # Ignored for files
|
# pattern = "" # Pattern ignored when is_file = true
|
||||||
#is_file = true
|
# is_file = true
|
||||||
|
|
||||||
# Monitor nginx access logs
|
# # Monitor system logs
|
||||||
#[[monitor.targets]]
|
# [[monitor.targets]]
|
||||||
#path = "/var/log/nginx"
|
# path = "/var/log"
|
||||||
#pattern = "access*.log"
|
# pattern = "*.log"
|
||||||
#is_file = false
|
# is_file = false
|
||||||
|
|
||||||
# Monitor systemd journal exported logs
|
# # Monitor nginx access logs with pattern
|
||||||
#[[monitor.targets]]
|
# [[monitor.targets]]
|
||||||
#path = "/var/log/journal"
|
# path = "/var/log/nginx"
|
||||||
#pattern = "*.log"
|
# pattern = "access*.log"
|
||||||
#is_file = false
|
# is_file = false
|
||||||
|
|
||||||
|
# # Monitor journal export directory
|
||||||
|
# [[monitor.targets]]
|
||||||
|
# path = "/var/log/journal"
|
||||||
|
# pattern = "*.log"
|
||||||
|
# is_file = false
|
||||||
|
|
||||||
|
# # Monitor multiple application logs
|
||||||
|
# [[monitor.targets]]
|
||||||
|
# path = "/opt/myapp/logs"
|
||||||
|
# pattern = "app-*.log"
|
||||||
|
# is_file = false
|
||||||
|
|
||||||
[stream]
|
[stream]
|
||||||
# Buffer size for each client connection
|
# Buffer size for each client connection
|
||||||
|
# Number of log entries that can be queued per client
|
||||||
|
# When buffer is full, new messages are skipped (not sent to that client)
|
||||||
|
# Increase for burst traffic, decrease for memory conservation
|
||||||
# Environment: LOGWISP_STREAM_BUFFER_SIZE
|
# Environment: LOGWISP_STREAM_BUFFER_SIZE
|
||||||
buffer_size = 10000
|
# CLI: --buffer-size SIZE
|
||||||
|
# Default: 1000
|
||||||
|
buffer_size = 1000
|
||||||
|
|
||||||
[stream.rate_limit]
|
[stream.rate_limit]
|
||||||
# Enable rate limiting
|
# Enable rate limiting per client IP
|
||||||
|
# Prevents resource exhaustion from misbehaving clients
|
||||||
# Environment: LOGWISP_STREAM_RATE_LIMIT_ENABLED
|
# Environment: LOGWISP_STREAM_RATE_LIMIT_ENABLED
|
||||||
enabled = true
|
# CLI: --rate-limit
|
||||||
|
# Default: false
|
||||||
|
enabled = false
|
||||||
|
|
||||||
# Requests per second per client
|
# Sustained requests per second allowed per client
|
||||||
|
# Clients can make this many requests per second continuously
|
||||||
# Environment: LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC
|
# Environment: LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC
|
||||||
|
# CLI: --rate-requests N
|
||||||
|
# Default: 10
|
||||||
requests_per_second = 10
|
requests_per_second = 10
|
||||||
|
|
||||||
# Burst size (max requests at once)
|
# Maximum burst size per client
|
||||||
|
# Allows temporary bursts above the sustained rate
|
||||||
|
# Should be >= requests_per_second
|
||||||
# Environment: LOGWISP_STREAM_RATE_LIMIT_BURST_SIZE
|
# Environment: LOGWISP_STREAM_RATE_LIMIT_BURST_SIZE
|
||||||
|
# CLI: --rate-burst N
|
||||||
|
# Default: 20
|
||||||
burst_size = 20
|
burst_size = 20
|
||||||
|
|
||||||
# How often to clean up old client limiters (seconds)
|
# How often to clean up inactive client rate limiters (seconds)
|
||||||
# Environment: LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL
|
# Clients inactive for 2x this duration are removed from tracking
|
||||||
cleanup_interval_s = 5
|
# Lower values = more frequent cleanup, higher values = less overhead
|
||||||
|
# Environment: LOGWISP_STREAM_RATE_LIMIT_CLEANUP_INTERVAL_S
|
||||||
|
# Default: 60
|
||||||
|
cleanup_interval_s = 60
|
||||||
|
|
||||||
|
# Production configuration example:
|
||||||
|
# [stream.rate_limit]
|
||||||
|
# enabled = true
|
||||||
|
# requests_per_second = 100 # Higher limit for production
|
||||||
|
# burst_size = 200 # Allow larger bursts
|
||||||
|
# cleanup_interval_s = 300 # Less frequent cleanup
|
||||||
@ -21,9 +21,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// CHANGED: Parse flags manually without init()
|
// Parse flags manually without init()
|
||||||
var colorMode bool
|
var colorMode bool
|
||||||
flag.BoolVar(&colorMode, "c", false, "Enable color pass-through for escape codes in logs")
|
flag.BoolVar(&colorMode, "c", false, "Enable color pass-through for escape codes in logs")
|
||||||
|
flag.BoolVar(&colorMode, "color", false, "Enable color pass-through for escape codes in logs")
|
||||||
|
|
||||||
// Additional CLI flags that override config
|
// Additional CLI flags that override config
|
||||||
var (
|
var (
|
||||||
@ -98,7 +99,6 @@ func main() {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Create components
|
// Create components
|
||||||
// colorMode is now separate from config
|
|
||||||
streamer := stream.NewWithOptions(cfg.Stream.BufferSize, colorMode)
|
streamer := stream.NewWithOptions(cfg.Stream.BufferSize, colorMode)
|
||||||
mon := monitor.New(streamer.Publish)
|
mon := monitor.New(streamer.Publish)
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ func main() {
|
|||||||
|
|
||||||
status := map[string]interface{}{
|
status := map[string]interface{}{
|
||||||
"service": "LogWisp",
|
"service": "LogWisp",
|
||||||
"version": "2.0.0", // CHANGED: Version bump for config integration
|
"version": "2.0.0",
|
||||||
"port": cfg.Port,
|
"port": cfg.Port,
|
||||||
"color_mode": colorMode,
|
"color_mode": colorMode,
|
||||||
"config": map[string]interface{}{
|
"config": map[string]interface{}{
|
||||||
@ -191,7 +191,6 @@ func main() {
|
|||||||
if colorMode {
|
if colorMode {
|
||||||
fmt.Println("Color pass-through enabled")
|
fmt.Println("Color pass-through enabled")
|
||||||
}
|
}
|
||||||
// CHANGED: Log config source information
|
|
||||||
fmt.Printf("Config loaded from: %s\n", config.GetConfigPath())
|
fmt.Printf("Config loaded from: %s\n", config.GetConfigPath())
|
||||||
|
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
|||||||
@ -67,18 +67,15 @@ func defaults() *Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration using lixenwraith/config Builder pattern
|
// Load reads configuration using lixenwraith/config Builder pattern
|
||||||
// CHANGED: Now uses config.Builder for all source handling
|
|
||||||
func Load() (*Config, error) {
|
func Load() (*Config, error) {
|
||||||
configPath := GetConfigPath()
|
configPath := GetConfigPath()
|
||||||
|
|
||||||
// CHANGED: Use Builder pattern with custom environment transform
|
|
||||||
cfg, err := lconfig.NewBuilder().
|
cfg, err := lconfig.NewBuilder().
|
||||||
WithDefaults(defaults()).
|
WithDefaults(defaults()).
|
||||||
WithEnvPrefix("LOGWISP_").
|
WithEnvPrefix("LOGWISP_").
|
||||||
WithFile(configPath).
|
WithFile(configPath).
|
||||||
WithEnvTransform(customEnvTransform).
|
WithEnvTransform(customEnvTransform).
|
||||||
WithSources(
|
WithSources(
|
||||||
// CHANGED: CLI args removed here - handled separately in LoadWithCLI
|
|
||||||
lconfig.SourceEnv,
|
lconfig.SourceEnv,
|
||||||
lconfig.SourceFile,
|
lconfig.SourceFile,
|
||||||
lconfig.SourceDefault,
|
lconfig.SourceDefault,
|
||||||
@ -107,7 +104,6 @@ func Load() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadWithCLI loads configuration and applies CLI arguments
|
// LoadWithCLI loads configuration and applies CLI arguments
|
||||||
// CHANGED: New function that properly integrates CLI args with config package
|
|
||||||
func LoadWithCLI(cliArgs []string) (*Config, error) {
|
func LoadWithCLI(cliArgs []string) (*Config, error) {
|
||||||
configPath := GetConfigPath()
|
configPath := GetConfigPath()
|
||||||
|
|
||||||
@ -118,7 +114,7 @@ func LoadWithCLI(cliArgs []string) (*Config, error) {
|
|||||||
WithDefaults(defaults()).
|
WithDefaults(defaults()).
|
||||||
WithEnvPrefix("LOGWISP_").
|
WithEnvPrefix("LOGWISP_").
|
||||||
WithFile(configPath).
|
WithFile(configPath).
|
||||||
WithArgs(convertedArgs). // CHANGED: Use WithArgs for CLI
|
WithArgs(convertedArgs).
|
||||||
WithEnvTransform(customEnvTransform).
|
WithEnvTransform(customEnvTransform).
|
||||||
WithSources(
|
WithSources(
|
||||||
lconfig.SourceCLI, // CLI highest priority
|
lconfig.SourceCLI, // CLI highest priority
|
||||||
@ -148,16 +144,14 @@ func LoadWithCLI(cliArgs []string) (*Config, error) {
|
|||||||
return finalConfig, finalConfig.validate()
|
return finalConfig, finalConfig.validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHANGED: Custom environment transform that handles LOGWISP_ prefix more flexibly
|
// customEnvTransform handles LOGWISP_ prefix environment variables
|
||||||
func customEnvTransform(path string) string {
|
func customEnvTransform(path string) string {
|
||||||
// Standard transform
|
// Standard transform
|
||||||
env := strings.ReplaceAll(path, ".", "_")
|
env := strings.ReplaceAll(path, ".", "_")
|
||||||
env = strings.ToUpper(env)
|
env = strings.ToUpper(env)
|
||||||
env = "LOGWISP_" + env
|
env = "LOGWISP_" + env
|
||||||
|
|
||||||
// Also check for some common variations
|
// Handle common variations
|
||||||
// This allows both LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC
|
|
||||||
// and LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SECOND
|
|
||||||
switch env {
|
switch env {
|
||||||
case "LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SECOND":
|
case "LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SECOND":
|
||||||
if _, exists := os.LookupEnv("LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC"); exists {
|
if _, exists := os.LookupEnv("LOGWISP_STREAM_RATE_LIMIT_REQUESTS_PER_SEC"); exists {
|
||||||
@ -172,7 +166,7 @@ func customEnvTransform(path string) string {
|
|||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHANGED: Convert CLI args to config package format
|
// convertCLIArgs converts CLI args to config package format
|
||||||
func convertCLIArgs(args []string) []string {
|
func convertCLIArgs(args []string) []string {
|
||||||
var converted []string
|
var converted []string
|
||||||
|
|
||||||
@ -194,7 +188,6 @@ func convertCLIArgs(args []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigPath returns the configuration file path
|
// GetConfigPath returns the configuration file path
|
||||||
// CHANGED: Exported and simplified - now just returns the path, no manual env handling
|
|
||||||
func GetConfigPath() string {
|
func GetConfigPath() string {
|
||||||
// Check explicit config file paths
|
// Check explicit config file paths
|
||||||
if configFile := os.Getenv("LOGWISP_CONFIG_FILE"); configFile != "" {
|
if configFile := os.Getenv("LOGWISP_CONFIG_FILE"); configFile != "" {
|
||||||
@ -219,7 +212,7 @@ func GetConfigPath() string {
|
|||||||
return "logwisp.toml"
|
return "logwisp.toml"
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHANGED: Special handling for comma-separated monitor targets env var
|
// handleMonitorTargetsEnv handles comma-separated monitor targets env var
|
||||||
func handleMonitorTargetsEnv(cfg *lconfig.Config) error {
|
func handleMonitorTargetsEnv(cfg *lconfig.Config) error {
|
||||||
if targetsStr := os.Getenv("LOGWISP_MONITOR_TARGETS"); targetsStr != "" {
|
if targetsStr := os.Getenv("LOGWISP_MONITOR_TARGETS"); targetsStr != "" {
|
||||||
// Clear any existing targets from file/defaults
|
// Clear any existing targets from file/defaults
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"logwisp/src/internal/monitor"
|
"logwisp/src/internal/monitor"
|
||||||
@ -22,13 +23,16 @@ type Streamer struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
colorMode bool
|
colorMode bool
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
totalDropped atomic.Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientConnection struct {
|
type clientConnection struct {
|
||||||
id string
|
id string
|
||||||
channel chan monitor.LogEntry
|
channel chan monitor.LogEntry
|
||||||
lastActivity time.Time
|
lastActivity time.Time
|
||||||
dropped int64 // Count of dropped messages
|
dropped atomic.Int64 // Track per-client dropped messages
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new SSE streamer
|
// New creates a new SSE streamer
|
||||||
@ -53,14 +57,10 @@ func NewWithOptions(bufferSize int, colorMode bool) *Streamer {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// run manages client connections with timeout cleanup
|
// run manages client connections - SIMPLIFIED: no forced disconnections
|
||||||
func (s *Streamer) run() {
|
func (s *Streamer) run() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
|
|
||||||
// Add periodic cleanup for stale/slow clients
|
|
||||||
cleanupTicker := time.NewTicker(30 * time.Second)
|
|
||||||
defer cleanupTicker.Stop()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case c := <-s.register:
|
case c := <-s.register:
|
||||||
@ -79,47 +79,27 @@ func (s *Streamer) run() {
|
|||||||
case entry := <-s.broadcast:
|
case entry := <-s.broadcast:
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
var toRemove []string
|
|
||||||
|
|
||||||
for id, client := range s.clients {
|
for _, client := range s.clients {
|
||||||
select {
|
select {
|
||||||
case client.channel <- entry:
|
case client.channel <- entry:
|
||||||
|
// Successfully sent
|
||||||
client.lastActivity = now
|
client.lastActivity = now
|
||||||
|
client.dropped.Store(0) // Reset dropped counter on success
|
||||||
default:
|
default:
|
||||||
// Track dropped messages and remove slow clients
|
// Buffer full - skip this message for this client
|
||||||
client.dropped++
|
// Don't disconnect, just track dropped messages
|
||||||
// Remove clients that have dropped >100 messages or been inactive >2min
|
dropped := client.dropped.Add(1)
|
||||||
if client.dropped > 100 || now.Sub(client.lastActivity) > 2*time.Minute {
|
s.totalDropped.Add(1)
|
||||||
toRemove = append(toRemove, id)
|
|
||||||
|
// Log significant drop milestones for monitoring
|
||||||
|
if dropped == 100 || dropped == 1000 || dropped%10000 == 0 {
|
||||||
|
// Could add logging here if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
||||||
// Remove slow/stale clients outside the read lock
|
|
||||||
if len(toRemove) > 0 {
|
|
||||||
s.mu.Lock()
|
|
||||||
for _, id := range toRemove {
|
|
||||||
if client, ok := s.clients[id]; ok {
|
|
||||||
close(client.channel)
|
|
||||||
delete(s.clients, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-cleanupTicker.C:
|
|
||||||
// Periodic cleanup of inactive clients
|
|
||||||
s.mu.Lock()
|
|
||||||
now := time.Now()
|
|
||||||
for id, client := range s.clients {
|
|
||||||
if now.Sub(client.lastActivity) > 5*time.Minute {
|
|
||||||
close(client.channel)
|
|
||||||
delete(s.clients, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
case <-s.done:
|
case <-s.done:
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
for _, client := range s.clients {
|
for _, client := range s.clients {
|
||||||
@ -138,18 +118,21 @@ func (s *Streamer) Publish(entry monitor.LogEntry) {
|
|||||||
case s.broadcast <- entry:
|
case s.broadcast <- entry:
|
||||||
// Sent to broadcast channel
|
// Sent to broadcast channel
|
||||||
default:
|
default:
|
||||||
// Drop entry if broadcast buffer full, log occurrence
|
// Broadcast buffer full - drop the message globally
|
||||||
// This prevents memory exhaustion under high load
|
s.totalDropped.Add(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler for SSE
|
// ServeHTTP implements http.Handler for SSE - SIMPLIFIED
|
||||||
func (s *Streamer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Streamer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
// Set SSE headers
|
// Set SSE headers
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
w.Header().Set("Connection", "keep-alive")
|
w.Header().Set("Connection", "keep-alive")
|
||||||
w.Header().Set("X-Accel-Buffering", "no")
|
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
|
||||||
|
|
||||||
|
// SECURITY: Prevent XSS
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
// Create client
|
// Create client
|
||||||
clientID := fmt.Sprintf("%d", time.Now().UnixNano())
|
clientID := fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
@ -159,7 +142,6 @@ func (s *Streamer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
id: clientID,
|
id: clientID,
|
||||||
channel: ch,
|
channel: ch,
|
||||||
lastActivity: time.Now(),
|
lastActivity: time.Now(),
|
||||||
dropped: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register client
|
// Register client
|
||||||
@ -169,41 +151,29 @@ func (s *Streamer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Send initial connection event
|
// Send initial connection event
|
||||||
fmt.Fprintf(w, "event: connected\ndata: {\"client_id\":\"%s\"}\n\n", clientID)
|
fmt.Fprintf(w, "event: connected\ndata: {\"client_id\":\"%s\",\"buffer_size\":%d}\n\n",
|
||||||
|
clientID, s.bufferSize)
|
||||||
if flusher, ok := w.(http.Flusher); ok {
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create ticker for heartbeat
|
// Create ticker for heartbeat - keeps connection alive through proxies
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
// Add timeout for slow clients
|
// Stream events until client disconnects
|
||||||
clientTimeout := time.NewTimer(10 * time.Minute)
|
|
||||||
defer clientTimeout.Stop()
|
|
||||||
|
|
||||||
// Stream events
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
|
// Client disconnected
|
||||||
return
|
return
|
||||||
|
|
||||||
case entry, ok := <-ch:
|
case entry, ok := <-ch:
|
||||||
if !ok {
|
if !ok {
|
||||||
// Channel was closed (client removed due to slowness)
|
// Channel closed
|
||||||
fmt.Fprintf(w, "event: disconnected\ndata: {\"reason\":\"slow_client\"}\n\n")
|
|
||||||
if flusher, ok := w.(http.Flusher); ok {
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset client timeout on successful read
|
|
||||||
if !clientTimeout.Stop() {
|
|
||||||
<-clientTimeout.C
|
|
||||||
}
|
|
||||||
clientTimeout.Reset(10 * time.Minute)
|
|
||||||
|
|
||||||
// Process entry for color if needed
|
// Process entry for color if needed
|
||||||
if s.colorMode {
|
if s.colorMode {
|
||||||
entry = s.processColorEntry(entry)
|
entry = s.processColorEntry(entry)
|
||||||
@ -220,19 +190,11 @@ func (s *Streamer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Heartbeat with UTC timestamp
|
// Send heartbeat as SSE comment
|
||||||
fmt.Fprintf(w, ": heartbeat %s\n\n", time.Now().UTC().Format("2006-01-02T15:04:05.000000Z07:00"))
|
fmt.Fprintf(w, ": heartbeat %s\n\n", time.Now().UTC().Format(time.RFC3339))
|
||||||
if flusher, ok := w.(http.Flusher); ok {
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-clientTimeout.C:
|
|
||||||
// Client timeout - close connection
|
|
||||||
fmt.Fprintf(w, "event: timeout\ndata: {\"reason\":\"client_timeout\"}\n\n")
|
|
||||||
if flusher, ok := w.(http.Flusher); ok {
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,11 +208,8 @@ func (s *Streamer) Stop() {
|
|||||||
close(s.broadcast)
|
close(s.broadcast)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced color processing with proper ANSI handling
|
// processColorEntry preserves ANSI codes in JSON
|
||||||
func (s *Streamer) processColorEntry(entry monitor.LogEntry) monitor.LogEntry {
|
func (s *Streamer) processColorEntry(entry monitor.LogEntry) monitor.LogEntry {
|
||||||
// For color mode, we preserve ANSI codes but ensure they're properly handled
|
|
||||||
// The JSON marshaling will escape them correctly for transmission
|
|
||||||
// Client-side handling is required for proper display
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,13 +222,24 @@ func (s *Streamer) Stats() map[string]interface{} {
|
|||||||
"active_clients": len(s.clients),
|
"active_clients": len(s.clients),
|
||||||
"buffer_size": s.bufferSize,
|
"buffer_size": s.bufferSize,
|
||||||
"color_mode": s.colorMode,
|
"color_mode": s.colorMode,
|
||||||
|
"total_dropped": s.totalDropped.Load(),
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDropped := int64(0)
|
// Include per-client dropped counts if any are significant
|
||||||
for _, client := range s.clients {
|
var clientsWithDrops []map[string]interface{}
|
||||||
totalDropped += client.dropped
|
for id, client := range s.clients {
|
||||||
|
dropped := client.dropped.Load()
|
||||||
|
if dropped > 0 {
|
||||||
|
clientsWithDrops = append(clientsWithDrops, map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"dropped": dropped,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clientsWithDrops) > 0 {
|
||||||
|
stats["clients_with_drops"] = clientsWithDrops
|
||||||
}
|
}
|
||||||
stats["total_dropped"] = totalDropped
|
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user