v0.1.11 configurable logging added, minor refactoring, orgnized docs added

This commit is contained in:
2025-07-10 01:17:06 -04:00
parent bc4ce1d0ae
commit 5936f82970
40 changed files with 5745 additions and 1701 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
data data
dev dev
log log
logs
cert cert
bin bin
script script

764
README.md
View File

@ -1,750 +1,82 @@
# LogWisp - Multi-Stream Log Monitoring Service <table>
<tr>
<td width="200" valign="middle">
<img src="asset/logwisp-logo.svg" alt="LogWisp Logo" width="200"/>
</td>
<td valign="middle">
<h1>LogWisp</h1>
<p>
<a href="https://golang.org"><img src="https://img.shields.io/badge/Go-1.24-00ADD8?style=flat&logo=go" alt="Go"></a>
<a href="https://opensource.org/licenses/BSD-3-Clause"><img src="https://img.shields.io/badge/License-BSD_3--Clause-blue.svg" alt="License"></a>
<a href="doc/"><img src="https://img.shields.io/badge/Docs-Available-green.svg" alt="Documentation"></a>
</p>
</td>
</tr>
</table>
<p align="center">
<img src="assets/logwisp-logo.svg" alt="LogWisp Logo" width="200"/>
</p>
A high-performance log streaming service with multi-stream architecture, supporting both TCP and HTTP/SSE protocols with real-time file monitoring, rotation detection, regex-based filtering, and rate limiting. **Multi-stream log monitoring with real-time streaming over HTTP/SSE and TCP**
## Features LogWisp watches log files and streams updates to connected clients in real-time. Perfect for monitoring multiple applications, filtering noise, and centralizing log access.
- **Multi-Stream Architecture**: Run multiple independent log streams, each with its own configuration ## 🚀 Quick Start
- **Dual Protocol Support**: TCP (raw streaming) and HTTP/SSE (browser-friendly)
- **Real-time Monitoring**: Instant updates with per-stream configurable check intervals
- **File Rotation Detection**: Automatic detection and handling of log rotation
- **Regex-based Filtering**: Include/exclude patterns with AND/OR logic per stream
- **Path-based Routing**: Optional HTTP router for consolidated access
- **Rate Limiting**: Per-IP or global rate limiting with token bucket algorithm
- **Connection Limiting**: Configurable concurrent connection limits per IP
- **Per-Stream Configuration**: Independent settings including check intervals, filters, and rate limits
- **Connection Statistics**: Real-time monitoring of active connections, filter, and rate limit metrics
- **Flexible Targets**: Monitor individual files or entire directories
- **Version Management**: Git tag-based versioning with build information
- **Configurable Heartbeats**: Keep connections alive with customizable formats
- **Minimal Direct Dependencies**: panjf2000/gnet/v2, valyala/fasthttp, lixenwraith/config, and stdlib
## Quick Start
```bash ```bash
# Build with version information # Install
make build go install github.com/yourusername/logwisp/src/cmd/logwisp@latest
# Run with default configuration if ~/.config/logwisp.toml doesn't exists # Run with defaults (monitors *.log in current directory)
./logwisp logwisp
# Run with custom config # Stream logs (from another terminal)
./logwisp --config /etc/logwisp/production.toml curl -N http://localhost:8080/stream
# Run with HTTP router (path-based routing)
./logwisp --router
# Show version information
./logwisp --version
``` ```
## Architecture ## ✨ Key Features
LogWisp uses a service-oriented architecture where each stream is an independent pipeline: - **📡 Real-time Streaming** - SSE (HTTP) and TCP protocols
- **🔍 Pattern Filtering** - Include/exclude logs with regex patterns
- **🛡️ Rate Limiting** - Protect against abuse with configurable limits
- **📊 Multi-stream** - Monitor different log sources simultaneously
- **🔄 Rotation Aware** - Handles log rotation seamlessly
- **⚡ High Performance** - Minimal CPU/memory footprint
``` ## 📖 Documentation
LogStream Service
├── Stream["app-logs"]
│ ├── Monitor (watches files)
│ ├── Filter Chain (optional)
│ ├── Rate Limiter (optional)
│ ├── TCP Server (optional)
│ └── HTTP Server (optional)
├── Stream["system-logs"]
│ ├── Monitor
│ ├── Filter Chain (optional)
│ ├── Rate Limiter (optional)
│ └── HTTP Server
└── HTTP Router (optional, for path-based routing)
```
## Configuration Complete documentation is available in the [`doc/`](doc/) directory:
Default configuration file location: `~/.config/logwisp.toml` - [**Quick Start Guide**](doc/quickstart.md) - Get running in 5 minutes
- [**Configuration**](doc/configuration.md) - All configuration options
- [**CLI Reference**](doc/cli.md) - Command-line interface
- [**Examples**](doc/examples/) - Ready-to-use configurations
### Basic Multi-Stream Configuration ## 💻 Basic Usage
### Monitor application logs with filtering:
```toml ```toml
# Application logs transport # ~/.config/logwisp.toml
[[streams]] [[streams]]
name = "app" name = "myapp"
[streams.monitor] [streams.monitor]
# Per-transport check interval in milliseconds targets = [{ path = "/var/log/myapp", pattern = "*.log" }]
check_interval_ms = 100
targets = [
{ path = "/var/log/myapp", pattern = "*.log", is_file = false },
{ path = "/var/log/myapp/app.log", is_file = true }
]
# Filter configuration (optional)
[[streams.filters]] [[streams.filters]]
type = "include" # Only show matching logs type = "include"
logic = "or" # Match any pattern patterns = ["ERROR", "WARN", "CRITICAL"]
patterns = [
"(?i)error", # Case-insensitive error
"(?i)warn", # Case-insensitive warning
"(?i)fatal" # Fatal errors
]
[streams.httpserver] [streams.httpserver]
enabled = true enabled = true
port = 8080 port = 8080
buffer_size = 2000
stream_path = "/stream"
status_path = "/status"
# Heartbeat configuration
[streams.httpserver.heartbeat]
enabled = true
interval_seconds = 30
format = "comment" # or "json" for structured events
include_timestamp = true
include_stats = false
# Rate limiting configuration
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 10.0
burst_size = 20
limit_by = "ip"
response_code = 429
response_message = "Rate limit exceeded"
max_connections_per_ip = 5
# System logs transport with slower check interval
[[streams]]
name = "system"
[streams.monitor]
# Check every 60 seconds for slowly updating logs
check_interval_ms = 60000
targets = [
{ path = "/var/log/syslog", is_file = true },
{ path = "/var/log/auth.log", is_file = true }
]
# Exclude debug logs
[[streams.filters]]
type = "exclude"
patterns = ["DEBUG", "TRACE"]
[streams.tcpserver]
enabled = true
port = 9090
buffer_size = 5000
# TCP heartbeat (always JSON format)
[streams.tcpserver.heartbeat]
enabled = true
interval_seconds = 300 # 5 minutes
include_timestamp = true
include_stats = true
# TCP rate limiting
[streams.tcpserver.rate_limit]
enabled = true
requests_per_second = 5.0
burst_size = 10
limit_by = "ip"
``` ```
### Target Configuration ### Run multiple streams:
Monitor targets support both files and directories:
```toml
# Directory monitoring with pattern
{ path = "/var/log", pattern = "*.log", is_file = false }
# Specific file monitoring
{ path = "/var/log/app.log", is_file = true }
# All .log files in a directory
{ path = "./logs", pattern = "*.log", is_file = false }
```
### Filter Configuration
Control which logs are streamed using regex patterns:
```toml
# Include filter - only matching logs pass
[[streams.filters]]
type = "include"
logic = "or" # Match ANY pattern
patterns = [
"ERROR",
"WARN",
"CRITICAL"
]
# Exclude filter - matching logs are dropped
[[streams.filters]]
type = "exclude"
logic = "or" # Drop if ANY pattern matches
patterns = [
"DEBUG",
"healthcheck",
"/metrics"
]
# Complex filter with AND logic
[[streams.filters]]
type = "include"
logic = "and" # Must match ALL patterns
patterns = [
"database", # Must contain "database"
"error", # AND must contain "error"
"connection" # AND must contain "connection"
]
```
Multiple filters are applied sequentially - all must pass for a log to be streamed.
### Check Interval Configuration
Each stream can have its own check interval based on log update frequency:
- **High-frequency logs**: 50-100ms (e.g., application debug logs)
- **Normal logs**: 100-1000ms (e.g., application logs)
- **Low-frequency logs**: 10000-60000ms (e.g., system logs, archives)
### Rate Limiting Configuration
Control request rates and connection limits per stream:
```toml
[streams.httpserver.rate_limit]
enabled = true # Enable/disable rate limiting
requests_per_second = 10.0 # Token refill rate
burst_size = 20 # Maximum burst capacity
limit_by = "ip" # "ip" or "global"
response_code = 429 # HTTP response code when limited
response_message = "Too many requests"
max_connections_per_ip = 5 # Max concurrent connections per IP
max_total_connections = 100 # Max total connections (global)
```
### Heartbeat Configuration
Keep connections alive and detect stale clients with configurable heartbeats:
```toml
[streams.httpserver.heartbeat]
enabled = true
interval_seconds = 30
format = "comment" # "comment" for SSE comments, "json" for events
include_timestamp = true # Add timestamp to heartbeat
include_stats = true # Include connection count and uptime
```
**Heartbeat Formats**:
Comment format (SSE):
```
: heartbeat 2025-01-07T10:30:00Z clients=5 uptime=3600s
```
JSON format (SSE):
```
event: heartbeat
data: {"type":"heartbeat","timestamp":"2025-01-07T10:30:00Z","active_clients":5,"uptime_seconds":3600}
```
TCP always uses JSON format with newline delimiter.
## Usage Modes
### 1. Standalone Mode (Default)
Each stream runs on its configured ports:
```bash ```bash
./logwisp logwisp --router --config /etc/logwisp/multi-stream.toml
# Stream endpoints:
# - app: http://localhost:8080/stream
# - system: tcp://localhost:9090 and https://localhost:8443/logs
``` ```
### 2. Router Mode ## 📄 License
All HTTP streams share ports with path-based routing:
```bash
./logwisp --router
# Routed endpoints:
# - app: http://localhost:8080/app/stream
# - system: http://localhost:8080/system/logs
# - global: http://localhost:8080/status
```
## Client Examples
### HTTP/SSE Stream
```bash
# Connect to a transport
curl -N http://localhost:8080/stream
# Check transport status (includes filter and rate limit stats)
curl http://localhost:8080/status
# With authentication (when implemented)
curl -u admin:password -N https://localhost:8443/logs
```
### TCP Stream
```bash
# Using netcat
nc localhost 9090
# Using telnet
telnet localhost 9090
# With TLS (when implemented)
openssl s_client -connect localhost:9443
```
### JavaScript Client
```javascript
const eventSource = new EventSource('http://localhost:8080/stream');
eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
console.log('Connected with ID:', data.client_id);
});
eventSource.addEventListener('message', (e) => {
const logEntry = JSON.parse(e.data);
console.log(`[${logEntry.time}] ${logEntry.level}: ${logEntry.message}`);
});
eventSource.addEventListener('heartbeat', (e) => {
const heartbeat = JSON.parse(e.data);
console.log('Heartbeat:', heartbeat);
});
eventSource.addEventListener('error', (e) => {
if (e.status === 429) {
console.error('Rate limited - backing off');
// Implement exponential backoff
}
});
```
## Log Entry Format
All log entries are streamed as JSON:
```json
{
"time": "2024-01-01T12:00:00.123456Z",
"source": "app.log",
"level": "ERROR",
"message": "Connection timeout",
"fields": {
"user_id": "12345",
"request_id": "abc-def-ghi"
}
}
```
## API Endpoints
### Stream Endpoints (per stream)
- `GET {stream_path}` - SSE log stream
- `GET {status_path}` - Stream statistics and configuration
### Global Endpoints (router mode)
- `GET /status` - Aggregated status for all streams
- `GET /{stream_name}/{path}` - Stream-specific endpoints
### Status Response
```json
{
"service": "LogWisp",
"version": "v1.0.0",
"server": {
"type": "http",
"port": 8080,
"active_clients": 5,
"uptime_seconds": 3600
},
"monitor": {
"active_watchers": 3,
"total_entries": 15420,
"dropped_entries": 0
},
"filters": {
"filter_count": 2,
"total_processed": 15420,
"total_passed": 1234,
"filters": [
{
"type": "include",
"logic": "or",
"pattern_count": 3,
"total_processed": 15420,
"total_matched": 1234,
"total_dropped": 0
}
]
},
"features": {
"rate_limit": {
"enabled": true,
"total_requests": 45678,
"blocked_requests": 234,
"active_ips": 23,
"total_connections": 5,
"config": {
"requests_per_second": 10,
"burst_size": 20,
"limit_by": "ip"
}
}
}
}
```
## Real-time Statistics
LogWisp provides comprehensive statistics at multiple levels:
- **Per-Stream Stats**: Monitor performance, connection counts, data throughput
- **Per-Watcher Stats**: File size, position, entries read, rotation count
- **Filter Stats**: Processed entries, matched patterns, dropped logs
- **Rate Limit Stats**: Total requests, blocked requests, active IPs
- **Global Stats**: Aggregated view of all streams (in router mode)
Access statistics via status endpoints or watch the console output:
```
[15:04:05] Active streams: 2
app: watchers=3 entries=1542 tcp_conns=2 http_conns=5
system: watchers=2 entries=8901 tcp_conns=0 http_conns=3
```
## Advanced Features
### Log Filtering
LogWisp implements powerful regex-based filtering:
- **Include Filters**: Whitelist patterns - only matching logs pass
- **Exclude Filters**: Blacklist patterns - matching logs are dropped
- **Logic Options**: OR (match any) or AND (match all) for pattern combinations
- **Filter Chains**: Multiple filters applied sequentially
- **Performance**: Patterns compiled once at startup for efficiency
Filter statistics help monitor effectiveness:
```bash
# Watch filter statistics
watch -n 1 'curl -s http://localhost:8080/status | jq .filters'
```
### Rate Limiting
LogWisp implements token bucket rate limiting with:
- **Per-IP limiting**: Each IP gets its own token bucket
- **Global limiting**: All clients share a single token bucket
- **Connection limits**: Restrict concurrent connections per IP
- **Automatic cleanup**: Stale IP entries removed after 5 minutes
- **Non-blocking**: Excess requests are immediately rejected with 429 status
Monitor rate limiting effectiveness:
```bash
# Watch rate limit statistics
watch -n 1 'curl -s http://localhost:8080/status | jq .features.rate_limit'
```
### File Rotation Detection
LogWisp automatically detects log rotation through multiple methods:
- Inode change detection
- File size decrease
- Modification time anomalies
- Position beyond file size
When rotation is detected, a special log entry is generated:
```json
{
"level": "INFO",
"message": "Log rotation detected (#1): inode change"
}
```
### Buffer Management
- **Non-blocking delivery**: Messages are dropped rather than blocking when buffers fill
- **Per-client buffers**: Each client has independent buffer space
- **Configurable sizes**: Adjust buffer sizes based on expected load
### Per-Stream Check Intervals
Optimize resource usage by configuring check intervals based on log update frequency:
```toml
# High-frequency application logs
[streams.monitor]
check_interval_ms = 50 # Check every 50ms
# Low-frequency system logs
[streams.monitor]
check_interval_ms = 60000 # Check every minute
```
## Performance Tuning
### Monitor Settings
- `check_interval_ms`: Lower values = faster detection, higher CPU usage
- Configure per-stream based on expected update frequency
- Use 10000ms+ for archival or slowly updating logs
### Filter Optimization
- Place most selective filters first
- Use simple patterns when possible
- Consider combining patterns: `"ERROR|WARN"` vs separate patterns
- Monitor filter statistics to identify bottlenecks
### Rate Limiting
- `requests_per_second`: Balance between protection and availability
- `burst_size`: Set to 2-3x the per-second rate for traffic spikes
- `max_connections_per_ip`: Prevent resource exhaustion from single IPs
### File Watcher Optimization
- Use specific file paths when possible (more efficient than directory scanning)
- Adjust patterns to minimize unnecessary file checks
- Consider separate streams for different update frequencies
### Network Optimization
- TCP: Best for high-volume, low-latency requirements
- HTTP/SSE: Best for browser compatibility and firewall traversal
- Router mode: Reduces port usage but adds slight routing overhead
## Building from Source
```bash
# Clone repository
git clone https://github.com/lixenwraith/logwisp
cd logwisp
# Install dependencies
go mod init logwisp
go get github.com/panjf2000/gnet/v2
go get github.com/valyala/fasthttp
go get github.com/lixenwraith/config
# Build with version information
make build
# Run tests
make test
# Test rate limiting
./test_ratelimit.sh
# Test router functionality
./test_router.sh
# Create a release
make release TAG=v1.0.0
```
### Makefile Targets
- `make build` - Build binary with version information
- `make install` - Install to /usr/local/bin
- `make clean` - Remove built binary
- `make test` - Run test suite
- `make release TAG=vX.Y.Z` - Create and push git tag
## Deployment
### Systemd Service
```ini
[Unit]
Description=LogWisp Multi-Stream Log Monitor
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/logwisp --config /etc/logwisp/production.toml
Restart=always
User=logwisp
Group=logwisp
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/var/log
# Rate limiting at system level
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
```
### Docker
```dockerfile
FROM golang:1.24 AS builder
WORKDIR /app
COPY . .
RUN make build
FROM debian:bookworm-slim
RUN useradd -r -s /bin/false logwisp
COPY --from=builder /app/logwisp /usr/local/bin/
USER logwisp
EXPOSE 8080 9090
CMD ["logwisp"]
```
### Docker Compose
```yaml
version: '3.8'
services:
logwisp:
build: .
volumes:
- /var/log:/var/log:ro
- ./config.toml:/etc/logwisp/config.toml:ro
ports:
- "8080:8080"
- "9090:9090"
restart: unless-stopped
command: ["logwisp", "--config", "/etc/logwisp/config.toml"]
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
```
## Security Considerations
### Current Implementation
- Read-only file access
- Regex pattern validation at startup
- Rate limiting for DDoS protection
- Connection limits to prevent resource exhaustion
- No authentication (placeholder configuration only)
- No TLS/SSL support (placeholder configuration only)
### Filter Security
⚠️ **SECURITY**: Be aware of potential ReDoS (Regular Expression Denial of Service) attacks:
- Complex nested patterns can cause CPU spikes
- Patterns are validated at startup but not for complexity
- Monitor filter processing time in production
- Consider pattern complexity limits for public-facing streams
### Planned Security Features
- **Authentication**: Basic, Bearer/JWT, mTLS
- **TLS/SSL**: For both HTTP and TCP streams
- **IP Filtering**: Whitelist/blacklist support
- **Audit Logging**: Access and authentication events
- **RBAC**: Role-based access control per stream
### Best Practices
1. Run with minimal privileges (read-only access to log files)
2. Configure appropriate rate limits based on expected traffic
3. Use network-level security until authentication is implemented
4. Place behind a reverse proxy for production HTTPS
5. Monitor rate limit statistics for potential attacks
6. Regularly update dependencies
7. Test filter patterns for performance impact
8. Limit regex complexity in production environments
### Rate Limiting Best Practices
- Start with conservative limits and adjust based on monitoring
- Use per-IP limiting for public endpoints
- Use global limiting for resource protection
- Set connection limits to prevent memory exhaustion
- Monitor blocked request statistics for anomalies
## Troubleshooting
### Filter Issues
1. Check filter statistics to see matched/dropped counts
2. Test patterns with sample log entries
3. Verify filter type (include vs exclude)
4. Check filter logic (or vs and)
5. Monitor CPU usage for complex patterns
### Rate Limit Issues
1. Check rate limit statistics in status endpoint
2. Verify appropriate `requests_per_second` for your use case
3. Ensure `burst_size` accommodates normal traffic spikes
4. Monitor for distributed attacks if per-IP limiting isn't effective
### No Log Entries Appearing
1. Check file permissions (LogWisp needs read access)
2. Verify file paths in configuration
3. Ensure files match the specified patterns
4. Check monitor statistics in status endpoint
5. Verify check_interval_ms is appropriate for log update frequency
6. Review filter configuration - logs might be filtered out
### High Memory Usage
1. Reduce buffer sizes in configuration
2. Lower the number of concurrent watchers
3. Enable rate limiting to prevent connection floods
4. Increase check interval for less critical logs
5. Use TCP instead of HTTP for high-volume streams
6. Check for complex regex patterns causing backtracking
### Connection Drops
1. Check heartbeat configuration
2. Verify network stability
3. Monitor client-side errors
4. Review dropped entry statistics
5. Check if rate limits are too restrictive
### Version Information
Use `./logwisp --version` to see:
- Version tag (from git tags)
- Git commit hash
- Build timestamp
## License
BSD-3-Clause BSD-3-Clause
## Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
## Roadmap
- [x] Multi-stream architecture
- [x] File and directory monitoring
- [x] TCP and HTTP/SSE streaming
- [x] Path-based HTTP routing
- [x] Per-stream check intervals
- [x] Version management
- [x] Configurable heartbeats
- [x] Rate and connection limiting
- [x] Regex-based log filtering
- [ ] Log transformation (field extraction, formatting)
- [ ] Configurable logging/stdout support
- [ ] Service/non-interactive setup
- [ ] Live config change support
- [ ] Authentication (Basic, JWT, mTLS)
- [ ] TLS/SSL support
- [ ] Prometheus metrics export
- [ ] WebSocket support

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,10 +1,68 @@
# LogWisp Configuration File # LogWisp Configuration File - Complete Reference
# Default path: ~/.config/logwisp.toml # Default path: ~/.config/logwisp.toml
# Override with: ./logwisp --config /path/to/config.toml # Override with: ./logwisp --config /path/to/config.toml
# This is a complete configuration reference showing all available options. # This is a complete configuration reference showing all available options.
# Default values are uncommented, alternatives and examples are commented. # Default values are uncommented, alternatives and examples are commented.
# ==============================================================================
# LOGGING CONFIGURATION (LogWisp's own operational logs)
# ==============================================================================
# Controls where and how LogWisp logs its own operational messages.
# This is separate from the logs being monitored and streamed.
[logging]
# Output mode: where to write LogWisp's operational logs
# Options: "file", "stdout", "stderr", "both", "none"
# - file: Write only to log files
# - stdout: Write only to standard output
# - stderr: Write only to standard error (default for containers)
# - both: Write to both file and console
# - none: Disable logging (⚠️ SECURITY: Not recommended)
output = "stderr"
# Minimum log level for operational logs
# Options: "debug", "info", "warn", "error"
# - debug: Maximum verbosity, includes internal state changes
# - info: Normal operational messages (default)
# - warn: Warnings and errors only
# - error: Errors only
level = "info"
# File output configuration (used when output includes "file" or "both")
[logging.file]
# Directory for log files
directory = "./logs"
# Base name for log files (will append timestamp and .log)
name = "logwisp"
# Maximum size per log file before rotation (megabytes)
max_size_mb = 100
# Maximum total size of all log files (megabytes)
# Oldest files are deleted when limit is reached
max_total_size_mb = 1000
# How long to keep log files (hours)
# 0 = no time-based deletion
retention_hours = 168.0 # 7 days
# Console output configuration
[logging.console]
# Target for console output
# Options: "stdout", "stderr", "split"
# - stdout: All logs to standard output
# - stderr: All logs to standard error (default)
# - split: INFO/DEBUG to stdout, WARN/ERROR to stderr (planned)
target = "stderr"
# Output format
# Options: "txt", "json"
# - txt: Human-readable text format
# - json: Structured JSON for log aggregation
format = "txt"
# ============================================================================== # ==============================================================================
# STREAM CONFIGURATION # STREAM CONFIGURATION
# ============================================================================== # ==============================================================================
@ -16,22 +74,35 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
[[streams]] [[streams]]
# Stream identifier used in logs, metrics, and router paths # Stream identifier used in logs, metrics, and router paths
# Must be unique across all streams
name = "default" name = "default"
# File monitoring configuration # File monitoring configuration
[streams.monitor] [streams.monitor]
# How often to check for new log entries (milliseconds) # How often to check for new log entries (milliseconds)
# Lower = faster detection but more CPU usage # Lower = faster detection but more CPU usage
# Range: 10-60000 (0.01 to 60 seconds)
check_interval_ms = 100 check_interval_ms = 100
# Targets to monitor - can be files or directories # Targets to monitor - can be files or directories
# At least one target is required
targets = [ targets = [
# Monitor all .log files in current directory # Monitor all .log files in current directory
{ path = "./", pattern = "*.log", is_file = false }, { path = "./", pattern = "*.log", is_file = false },
# Example: Monitor specific file
# { path = "/var/log/app.log", is_file = true },
# Example: Multiple patterns in a directory
# { path = "/logs", pattern = "*.log", is_file = false },
# { path = "/logs", pattern = "*.txt", is_file = false },
] ]
# Filter configuration (optional) - controls which logs are streamed # Filter configuration (optional) - controls which logs are streamed
# Multiple filters are applied sequentially - all must pass # Multiple filters are applied sequentially - all must pass
# Empty patterns array means "match everything"
# Example: Include only errors and warnings
# [[streams.filters]] # [[streams.filters]]
# type = "include" # "include" (whitelist) or "exclude" (blacklist) # type = "include" # "include" (whitelist) or "exclude" (blacklist)
# logic = "or" # "or" (match any) or "and" (match all) # logic = "or" # "or" (match any) or "and" (match all)
@ -40,31 +111,124 @@ targets = [
# "(?i)warn" # Case-insensitive warning matching # "(?i)warn" # Case-insensitive warning matching
# ] # ]
# Example: Exclude debug and trace logs
# [[streams.filters]]
# type = "exclude"
# patterns = ["DEBUG", "TRACE", "VERBOSE"]
# HTTP Server configuration (SSE/Server-Sent Events) # HTTP Server configuration (SSE/Server-Sent Events)
[streams.httpserver] [streams.httpserver]
# Enable/disable HTTP server for this stream
enabled = true enabled = true
# Port to listen on (1-65535)
# Each stream needs a unique port unless using router mode
port = 8080 port = 8080
buffer_size = 1000 # Per-client buffer size (messages)
stream_path = "/stream" # Endpoint for SSE stream # Per-client buffer size (number of messages)
status_path = "/status" # Endpoint for statistics # Larger = handles bursts better, more memory per client
buffer_size = 1000
# Endpoint paths (must start with /)
stream_path = "/stream" # SSE stream endpoint
status_path = "/status" # Statistics endpoint
# Keep-alive heartbeat configuration # Keep-alive heartbeat configuration
# Prevents connection timeout on quiet logs
[streams.httpserver.heartbeat] [streams.httpserver.heartbeat]
# Enable/disable heartbeat messages
enabled = true enabled = true
interval_seconds = 30 # Send heartbeat every 30 seconds
format = "comment" # SSE comment format (: heartbeat) # Interval between heartbeats (seconds)
include_timestamp = true # Include timestamp in heartbeat # Range: 1-3600 (1 second to 1 hour)
include_stats = false # Include connection stats interval_seconds = 30
# Heartbeat format
# Options: "comment", "json"
# - comment: SSE comment format (: heartbeat)
# - json: JSON event format (data: {"type":"heartbeat"})
format = "comment"
# Include timestamp in heartbeat
include_timestamp = true
# Include connection statistics
include_stats = false
# Rate limiting configuration (disabled by default) # Rate limiting configuration (disabled by default)
# Protects against abuse and resource exhaustion
[streams.httpserver.rate_limit] [streams.httpserver.rate_limit]
# Enable/disable rate limiting
enabled = false enabled = false
# requests_per_second = 10.0 # Token refill rate
# burst_size = 20 # Max burst capacity # Token refill rate (requests per second)
# limit_by = "ip" # "ip" or "global" # Float value, e.g., 0.5 = 1 request every 2 seconds
# response_code = 429 # HTTP Too Many Requests # requests_per_second = 10.0
# Maximum burst capacity (token bucket size)
# Should be 2-3x requests_per_second for normal usage
# burst_size = 20
# Rate limit strategy
# Options: "ip", "global"
# - ip: Each client IP gets its own limit
# - global: All clients share one limit
# limit_by = "ip"
# HTTP response code when rate limited
# Common: 429 (Too Many Requests), 503 (Service Unavailable)
# response_code = 429
# Response message when rate limited
# response_message = "Rate limit exceeded" # response_message = "Rate limit exceeded"
# max_connections_per_ip = 5 # Max SSE connections per IP
# Maximum concurrent connections per IP address
# 0 = unlimited
# max_connections_per_ip = 5
# Maximum total concurrent connections
# 0 = unlimited
# max_total_connections = 100
# SSL/TLS configuration (planned feature)
# [streams.httpserver.ssl]
# enabled = false
# cert_file = "/path/to/cert.pem"
# key_file = "/path/to/key.pem"
# min_version = "TLS1.2" # Minimum TLS version
# client_auth = false # Require client certificates
# TCP Server configuration (optional)
# Raw TCP streaming for high-performance scenarios
# [streams.tcpserver]
# enabled = false
# port = 9090
# buffer_size = 5000 # Larger buffer for TCP
#
# [streams.tcpserver.heartbeat]
# enabled = true
# interval_seconds = 60
# include_timestamp = true
# include_stats = false
#
# [streams.tcpserver.rate_limit]
# enabled = false
# requests_per_second = 5.0
# burst_size = 10
# limit_by = "ip"
# Authentication configuration (planned feature)
# [streams.auth]
# type = "none" # Options: "none", "basic", "bearer"
#
# # Basic authentication
# [streams.auth.basic_auth]
# users_file = "/etc/logwisp/users.htpasswd"
# realm = "LogWisp"
#
# # IP-based access control
# ip_whitelist = ["192.168.1.0/24", "10.0.0.0/8"]
# ip_blacklist = []
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Example: Application Logs Stream with Error Filtering # Example: Application Logs Stream with Error Filtering
@ -92,6 +256,8 @@ enabled = false
# "(?i)\\bcritical\\b", # critical # "(?i)\\bcritical\\b", # critical
# "(?i)exception", # exception anywhere # "(?i)exception", # exception anywhere
# "(?i)fail(ed|ure)?", # fail, failed, failure # "(?i)fail(ed|ure)?", # fail, failed, failure
# "panic", # Go panics
# "traceback", # Python tracebacks
# ] # ]
# #
# # Filter 2: Exclude health check noise # # Filter 2: Exclude health check noise
@ -100,7 +266,10 @@ enabled = false
# patterns = [ # patterns = [
# "/health", # "/health",
# "/metrics", # "/metrics",
# "GET /ping" # "/ping",
# "GET /favicon.ico",
# "ELB-HealthChecker",
# "kube-probe"
# ] # ]
# #
# [streams.httpserver] # [streams.httpserver]
@ -125,6 +294,7 @@ enabled = false
# burst_size = 50 # burst_size = 50
# limit_by = "ip" # limit_by = "ip"
# max_connections_per_ip = 10 # max_connections_per_ip = 10
# max_total_connections = 200
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Example: System Logs Stream (TCP + HTTP) with Security Filtering # Example: System Logs Stream (TCP + HTTP) with Security Filtering
@ -138,6 +308,7 @@ enabled = false
# { path = "/var/log/syslog", is_file = true }, # { path = "/var/log/syslog", is_file = true },
# { path = "/var/log/auth.log", is_file = true }, # { path = "/var/log/auth.log", is_file = true },
# { path = "/var/log/kern.log", is_file = true }, # { path = "/var/log/kern.log", is_file = true },
# { path = "/var/log/messages", is_file = true },
# ] # ]
# #
# # Include only security-relevant logs # # Include only security-relevant logs
@ -152,7 +323,12 @@ enabled = false
# "(?i)permission", # "(?i)permission",
# "(?i)denied", # "(?i)denied",
# "(?i)unauthorized", # "(?i)unauthorized",
# "kernel:.*audit" # "(?i)security",
# "(?i)selinux",
# "kernel:.*audit",
# "COMMAND=", # sudo commands
# "session opened",
# "session closed"
# ] # ]
# #
# # TCP Server for high-performance streaming # # TCP Server for high-performance streaming
@ -182,9 +358,15 @@ enabled = false
# buffer_size = 1000 # buffer_size = 1000
# stream_path = "/stream" # stream_path = "/stream"
# status_path = "/status" # status_path = "/status"
#
# [streams.httpserver.rate_limit]
# enabled = true
# requests_per_second = 5.0
# burst_size = 10
# max_connections_per_ip = 2 # Strict for security logs
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Example: High-Volume Debug Logs with Filtering # Example: High-Volume Debug Logs with Performance Filtering
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# [[streams]] # [[streams]]
# name = "debug" # name = "debug"
@ -193,6 +375,7 @@ enabled = false
# check_interval_ms = 5000 # Check every 5 seconds (high volume) # check_interval_ms = 5000 # Check every 5 seconds (high volume)
# targets = [ # targets = [
# { path = "/tmp/debug", pattern = "*.debug", is_file = false }, # { path = "/tmp/debug", pattern = "*.debug", is_file = false },
# { path = "/var/log/debug", pattern = "debug-*.log", is_file = false },
# ] # ]
# #
# # Exclude verbose debug output # # Exclude verbose debug output
@ -203,15 +386,19 @@ enabled = false
# "VERBOSE", # "VERBOSE",
# "entering function", # "entering function",
# "exiting function", # "exiting function",
# "memory dump" # "memory dump",
# "hex dump",
# "stack trace",
# "goroutine [0-9]+"
# ] # ]
# #
# # Include only specific modules # # Include only specific modules
# [[streams.filters]] # [[streams.filters]]
# type = "include" # type = "include"
# patterns = [ # patterns = [
# "module:(api|database|auth)", # "module=(api|database|auth)",
# "component:(router|handler)" # "component=(router|handler)",
# "service=(payment|order|user)"
# ] # ]
# #
# [streams.httpserver] # [streams.httpserver]
@ -232,260 +419,295 @@ enabled = false
# burst_size = 5 # burst_size = 5
# limit_by = "ip" # limit_by = "ip"
# max_connections_per_ip = 1 # One connection per IP # max_connections_per_ip = 1 # One connection per IP
# response_code = 503 # Service Unavailable
# response_message = "Debug stream overloaded"
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Example: Database Logs with Complex Filtering # Example: Multi-Application with Router Mode
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Run with: logwisp --router
#
# [[streams]]
# name = "frontend"
# [streams.monitor]
# targets = [{ path = "/var/log/nginx", pattern = "*.log" }]
# [[streams.filters]]
# type = "exclude"
# patterns = ["GET /static/", "GET /assets/"]
# [streams.httpserver]
# enabled = true
# port = 8080 # Same port OK in router mode
#
# [[streams]]
# name = "backend"
# [streams.monitor]
# targets = [{ path = "/var/log/api", pattern = "*.log" }]
# [[streams.filters]]
# type = "include"
# patterns = ["ERROR", "WARN", "timeout", "failed"]
# [streams.httpserver]
# enabled = true
# port = 8080 # Shared port in router mode
#
# [[streams]] # [[streams]]
# name = "database" # name = "database"
#
# [streams.monitor] # [streams.monitor]
# check_interval_ms = 200 # targets = [{ path = "/var/log/postgresql", pattern = "*.log" }]
# targets = [
# { path = "/var/log/postgresql", pattern = "*.log", is_file = false },
# ]
#
# # Complex AND filter - must match all patterns
# [[streams.filters]]
# type = "include"
# logic = "and" # Must match ALL patterns
# patterns = [
# "(?i)error|fail", # Must contain error or fail
# "(?i)connection|query", # AND must be about connections or queries
# "(?i)timeout|deadlock" # AND must involve timeout or deadlock
# ]
#
# # Exclude routine maintenance
# [[streams.filters]]
# type = "exclude"
# patterns = [
# "VACUUM",
# "ANALYZE",
# "checkpoint"
# ]
#
# [streams.tcpserver]
# enabled = true
# port = 9091
# buffer_size = 2000
# ------------------------------------------------------------------------------
# Example: API Access Logs with Pattern Extraction
# ------------------------------------------------------------------------------
# [[streams]]
# name = "api-access"
#
# [streams.monitor]
# check_interval_ms = 100
# targets = [
# { path = "/var/log/nginx/access.log", is_file = true },
# ]
#
# # Include only API endpoints
# [[streams.filters]]
# type = "include"
# patterns = [
# '"/api/v[0-9]+/', # API versioned endpoints
# '"(GET|POST|PUT|DELETE) /api/' # API requests
# ]
#
# # Exclude specific status codes
# [[streams.filters]]
# type = "exclude"
# patterns = [
# '" 200 ', # Success responses
# '" 204 ', # No content
# '" 304 ', # Not modified
# 'OPTIONS ' # CORS preflight
# ]
#
# [streams.httpserver] # [streams.httpserver]
# enabled = true # enabled = true
# port = 8084 # port = 8080
# buffer_size = 3000
# ------------------------------------------------------------------------------
# Example: Security/Audit Logs with Strict Filtering
# ------------------------------------------------------------------------------
# [[streams]]
# name = "security"
# #
# [streams.monitor] # # Access via:
# check_interval_ms = 100 # # http://localhost:8080/frontend/stream
# targets = [ # # http://localhost:8080/backend/stream
# { path = "/var/log/audit", pattern = "audit.log*", is_file = false }, # # http://localhost:8080/database/stream
# ] # # http://localhost:8080/status (global)
#
# # Security-focused patterns
# [[streams.filters]]
# type = "include"
# logic = "or"
# patterns = [
# "type=USER_AUTH",
# "type=USER_LOGIN",
# "type=USER_LOGOUT",
# "type=USER_ERR",
# "type=CRED_", # All credential operations
# "type=PRIV_", # All privilege operations
# "type=ANOM_", # All anomalies
# "type=RESP_", # All responses
# "failed|failure",
# "denied|unauthorized",
# "violation",
# "attack|intrusion"
# ]
#
# [streams.httpserver]
# enabled = true
# port = 8443 # HTTPS port (for future TLS)
# buffer_size = 1000
# stream_path = "/audit"
# status_path = "/health"
#
# # Strict rate limiting for security logs
# [streams.httpserver.rate_limit]
# enabled = true
# requests_per_second = 2.0 # Very limited access
# burst_size = 3
# limit_by = "ip"
# max_connections_per_ip = 1 # Single connection per IP
# response_code = 403 # Forbidden instead of rate limit
# response_message = "Access restricted"
#
# # Future: SSL/TLS configuration
# # [streams.httpserver.ssl]
# # enabled = true
# # cert_file = "/etc/logwisp/certs/server.crt"
# # key_file = "/etc/logwisp/certs/server.key"
# # min_version = "TLS1.2"
#
# # Future: Authentication
# # [streams.auth]
# # type = "basic"
# # [streams.auth.basic_auth]
# # users_file = "/etc/logwisp/security.users"
# # realm = "Security Logs"
# ------------------------------------------------------------------------------
# Example: Multi-Application Logs with Service Filtering
# ------------------------------------------------------------------------------
# [[streams]]
# name = "microservices"
#
# [streams.monitor]
# check_interval_ms = 100
# targets = [
# { path = "/var/log/containers", pattern = "*.log", is_file = false },
# ]
#
# # Filter by service name
# [[streams.filters]]
# type = "include"
# patterns = [
# "service=(api|auth|user|order)", # Specific services
# "pod=(api|auth|user|order)-" # Kubernetes pods
# ]
#
# # Exclude Kubernetes noise
# [[streams.filters]]
# type = "exclude"
# patterns = [
# "kube-system",
# "kube-proxy",
# "Readiness probe",
# "Liveness probe"
# ]
#
# [streams.httpserver]
# enabled = true
# port = 8085
# buffer_size = 5000
# ============================================================================== # ==============================================================================
# FILTER PATTERN EXAMPLES # FILTER PATTERN REFERENCE
# ============================================================================== # ==============================================================================
# #
# Basic Patterns: # Basic Patterns:
# - "ERROR" # Exact match # - "ERROR" # Exact match (case sensitive)
# - "(?i)error" # Case-insensitive # - "(?i)error" # Case-insensitive
# - "\\berror\\b" # Word boundary (won't match "errorCode") # - "\\berror\\b" # Word boundary (won't match "errorCode")
# - "error|warn|fatal" # Multiple options # - "error|warn|fatal" # Multiple options (OR)
# - "(error|warn) level" # Group with context
#
# Position Patterns:
# - "^\\[ERROR\\]" # Line starts with [ERROR]
# - "ERROR:$" # Line ends with ERROR:
# - "^\\d{4}-\\d{2}-\\d{2}" # Line starts with date
# #
# Complex Patterns: # Complex Patterns:
# - "^\\[ERROR\\]" # Line starts with [ERROR]
# - "status=[4-5][0-9]{2}" # HTTP 4xx or 5xx status codes # - "status=[4-5][0-9]{2}" # HTTP 4xx or 5xx status codes
# - "duration>[0-9]{4}ms" # Duration over 999ms # - "duration>[0-9]{4}ms" # Duration over 999ms
# - "user_id=\"[^\"]+\"" # Extract user_id values # - "user_id=\"[^\"]+\"" # Extract user_id values
# - "\\[ERROR\\].*database" # ERROR followed by database
# - "(?i)\\b(error|fail|critical)\\b" # Multiple error words
#
# Log Level Patterns:
# - "\\[(ERROR|WARN|FATAL)\\]" # Common formats
# - "level=(error|warning|critical)" # Key-value format
# - "ERROR\\s*:" # ERROR with optional space
# - "<(Error|Warning)>" # XML-style
#
# Application Patterns:
# - "com\\.mycompany\\..*Exception" # Java exceptions
# - "at .+\\(.+\\.java:[0-9]+\\)" # Java stack traces
# - "File \".+\", line [0-9]+" # Python tracebacks
# - "panic: .+" # Go panics
# - "/api/v[0-9]+/" # API versioned paths
#
# Performance Patterns:
# - "took [0-9]{4,}ms" # Operations over 999ms
# - "memory usage: [8-9][0-9]%" # High memory usage
# - "queue size: [0-9]{4,}" # Large queues
# - "timeout|timed out" # Timeouts
#
# Security Patterns:
# - "unauthorized|forbidden" # Access denied
# - "invalid token|expired token" # Auth failures
# - "SQL injection|XSS" # Security threats
# - "failed login.*IP: ([0-9.]+)" # Failed logins with IP
# #
# Performance Tips: # Performance Tips:
# - Avoid nested quantifiers: "((a+)+)+" can cause catastrophic backtracking # - Avoid nested quantifiers: "((a+)+)+" causes catastrophic backtracking
# - Use anchors when possible: "^ERROR" is faster than "ERROR" # - Use anchors when possible: "^ERROR" is faster than "ERROR"
# - Prefer character classes: "[0-9]" over "\\d" for clarity # - Prefer character classes: "[0-9]" over "\\d" for clarity
# - Use non-capturing groups: "(?:error|warn)" when not extracting
# - Test complex patterns with sample data before deployment # - Test complex patterns with sample data before deployment
# - Consider using multiple simple patterns instead of one complex pattern
# #
# Security Considerations: # Security Considerations:
# - Be aware of ReDoS (Regular Expression Denial of Service) # - Be aware of ReDoS (Regular Expression Denial of Service)
# - Limit pattern complexity for public-facing streams # - Limit pattern complexity for public-facing streams
# - Monitor filter processing time in statistics # - Monitor filter processing time in statistics
# - Consider pre-filtering very high volume streams # - Consider pre-filtering very high volume streams
# - Use explicit allow-lists for sensitive logs
# ============================================================================== # ==============================================================================
# USAGE EXAMPLES # RATE LIMITING GUIDE
# ============================================================================== # ==============================================================================
#
# 1. Basic usage (single stream): # Token Bucket Algorithm:
# ./logwisp # - Each client (IP) or global limit gets a bucket with 'burst_size' tokens
# - Monitors current directory for *.log files # - Tokens refill at 'requests_per_second' rate
# - Access logs at: http://localhost:8080/stream # - Each request consumes one token
# - View stats at: http://localhost:8080/status # - Provides smooth rate limiting without hard cutoffs
#
# 2. Multi-stream configuration: # Configuration Examples:
# - Uncomment additional [[streams]] sections above #
# - Each stream runs independently on its own port # Light Protection (default for most streams):
# - Different check intervals for different log types # requests_per_second = 10.0
# - Different filters for each stream # burst_size = 20 # Handle short bursts
#
# 3. Router mode (consolidated access): # Moderate Protection (public endpoints):
# ./logwisp --router # requests_per_second = 5.0
# - All streams accessible via paths: /streamname/stream # burst_size = 15
# - Global status at: /status # max_connections_per_ip = 5
# - Example: http://localhost:8080/app/stream #
# Strict Protection (sensitive logs):
# 4. Production deployment: # requests_per_second = 1.0
# - Enable filters to reduce noise and bandwidth # burst_size = 3
# - Enable rate limiting on public-facing streams # max_connections_per_ip = 1
# - Use TCP for internal high-volume streams # limit_by = "ip"
# - Set appropriate check intervals (higher = less CPU) #
# - Configure heartbeats for long-lived connections # Global Limiting (shared resource):
# requests_per_second = 50.0 # Total for all clients
# 5. Monitoring: # burst_size = 100
# curl http://localhost:8080/status | jq . # limit_by = "global"
# - Check active connections # max_total_connections = 50
# - Monitor filter statistics (matched/dropped) #
# - Monitor rate limit statistics # Behavior:
# - Track log entry counts # - HTTP: Returns response_code (default 429) with JSON error
# - TCP: Silently drops connection (no error message)
# - Cleanup: Inactive IPs removed after 5 minutes
# - Statistics: Available in /status endpoint
#
# Best Practices:
# - Set burst_size to 2-3x requests_per_second
# - Use per-IP limiting for fairness
# - Use global limiting for resource protection
# - Monitor rate limit statistics for tuning
# - Consider different limits for different streams
# - Enable for any public-facing endpoints
# ============================================================================== # ==============================================================================
# ENVIRONMENT VARIABLES # PERFORMANCE TUNING
# ============================================================================== # ==============================================================================
# Configuration can be overridden via environment variables: #
# LOGWISP_STREAMS_0_MONITOR_CHECK_INTERVAL_MS=50 # Monitor Check Interval:
# LOGWISP_STREAMS_0_HTTPSERVER_PORT=8090 # - 10-50ms: Real-time monitoring, higher CPU usage
# LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_ENABLED=true # - 100-500ms: Good balance for active logs
# LOGWISP_STREAMS_0_FILTERS_0_TYPE=include # - 1000-5000ms: Low-activity logs, minimal CPU
# LOGWISP_STREAMS_0_FILTERS_0_PATTERNS='["ERROR","WARN"]' # - 10000ms+: Very slow changing logs
#
# Buffer Sizes:
# - HTTP: 100-1000 for normal use, 5000+ for high volume
# - TCP: 1000-5000 typical, 10000+ for bulk streaming
# - Larger = more memory per client, handles bursts better
#
# Connection Limits:
# - Development: No limits needed
# - Production: 5-10 connections per IP typical
# - Public: 1-3 connections per IP
# - Total: Based on available memory (each uses ~1-5MB)
#
# Filter Performance:
# - Simple patterns: ~1μs per check
# - Complex patterns: ~10-100μs per check
# - Many patterns: Consider multiple streams instead
# - Use exclude filters to drop noise early
#
# Memory Usage (approximate):
# - Base process: ~10-20MB
# - Per stream: ~5-10MB
# - Per HTTP client: ~1-2MB
# - Per TCP client: ~0.5-1MB
# - Filter chain: ~1-5MB depending on patterns
# ============================================================================== # ==============================================================================
# NOTES # DEPLOYMENT SCENARIOS
# ============================================================================== # ==============================================================================
# - Filters are processed sequentially - all must pass #
# - Empty filter patterns means "pass everything" # Single Application:
# - Rate limiting is disabled by default for backward compatibility # - One stream with basic filtering
# - Each stream can have different rate limit settings # - Moderate rate limiting
# - TCP connections are silently dropped when rate limited # - Standard check interval (100ms)
# - HTTP returns 429 (or configured code) with JSON error #
# - IP tracking is cleaned up after 5 minutes of inactivity # Microservices:
# - Token bucket algorithm provides smooth rate limiting # - Multiple streams, one per service
# - Connection limits prevent resource exhaustion # - Router mode for unified access
# - Regex patterns are compiled once at startup for performance # - Different filter rules per service
# - Complex patterns can impact performance - monitor statistics # - Service-specific rate limits
#
# High Security:
# - Strict include filters
# - Low rate limits (1-2 req/sec)
# - Single connection per IP
# - TCP for internal, HTTP for external
#
# High Performance:
# - TCP streaming preferred
# - Large buffers (10000+)
# - Minimal filtering
# - Higher check intervals
# - No heartbeats
#
# Development/Testing:
# - Multiple streams for different log levels
# - No rate limiting
# - Debug level logging
# - Fast check intervals
# - All filters disabled
# ==============================================================================
# TROUBLESHOOTING
# ==============================================================================
#
# Common Issues:
#
# "No logs appearing":
# - Check file paths and permissions
# - Verify pattern matches filenames
# - Check filters aren't too restrictive
# - Enable debug logging: --log-level debug
#
# "High CPU usage":
# - Increase check_interval_ms
# - Reduce number of filter patterns
# - Use simpler regex patterns
# - Check for runaway log growth
#
# "Clients disconnecting":
# - Enable heartbeats
# - Check rate limiting settings
# - Verify network connectivity
# - Increase buffer sizes
#
# "Memory growth":
# - Check client connection count
# - Verify buffer sizes are reasonable
# - Look for memory leaks in filters
# - Enable connection limits
#
# Debug Commands:
# - Check status: curl http://localhost:8080/status
# - Test stream: curl -N http://localhost:8080/stream
# - View logs: logwisp --log-level debug --log-output stderr
# - Test filters: Use simple patterns first
# ==============================================================================
# FUTURE FEATURES (Roadmap)
# ==============================================================================
#
# Authentication:
# - Basic auth with htpasswd files
# - Bearer token authentication
# - JWT validation
# - mTLS client certificates
#
# SSL/TLS:
# - HTTPS endpoints
# - TLS for TCP streams
# - Certificate management
# - Let's Encrypt integration
#
# Advanced Filtering:
# - Lua scripting for complex logic
# - Rate-based filtering (N per minute)
# - Statistical anomaly detection
# - Multi-line pattern matching
#
# Output Formats:
# - JSON transformation
# - Field extraction
# - Custom formatting templates
# - Compression (gzip)
#
# Integrations:
# - Prometheus metrics
# - OpenTelemetry traces
# - Webhook notifications
# - Cloud storage backends

View File

@ -1,120 +0,0 @@
# LogWisp Configuration Example
# Default path: ~/.config/logwisp.toml
# Application logs - public facing
[[streams]]
name = "app-public"
[streams.monitor]
check_interval_ms = 100
targets = [
{ path = "/var/log/nginx", pattern = "access.log*", is_file = false },
{ path = "/var/log/app", pattern = "production.log", is_file = true }
]
[streams.httpserver]
enabled = true
port = 8080
buffer_size = 2000
stream_path = "/logs"
status_path = "/health"
[streams.httpserver.heartbeat]
enabled = true
interval_seconds = 30
format = "json"
include_timestamp = true
include_stats = true
# Rate limiting for public endpoint
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 50.0
burst_size = 100
limit_by = "ip"
response_code = 429
response_message = "Rate limit exceeded. Please retry after 60 seconds."
max_connections_per_ip = 5
max_total_connections = 100
# System logs - internal only
[[streams]]
name = "system"
[streams.monitor]
check_interval_ms = 5000 # Check every 5 seconds
targets = [
{ path = "/var/log/syslog", is_file = true },
{ path = "/var/log/auth.log", is_file = true },
{ path = "/var/log/kern.log", is_file = true }
]
# TCP for internal consumers
[streams.tcpserver]
enabled = true
port = 9090
buffer_size = 5000
[streams.tcpserver.heartbeat]
enabled = true
interval_seconds = 60
include_timestamp = true
# Moderate rate limiting for internal use
[streams.tcpserver.rate_limit]
enabled = true
requests_per_second = 10.0
burst_size = 20
limit_by = "ip"
# Security audit logs - restricted access
[[streams]]
name = "security"
[streams.monitor]
check_interval_ms = 100
targets = [
{ path = "/var/log/audit", pattern = "*.log", is_file = false },
{ path = "/var/log/fail2ban.log", is_file = true }
]
[streams.httpserver]
enabled = true
port = 8443
buffer_size = 1000
stream_path = "/audit/stream"
status_path = "/audit/status"
# Strict rate limiting
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 1.0
burst_size = 3
limit_by = "ip"
max_connections_per_ip = 1
response_code = 403
response_message = "Access denied"
# Application debug logs - development team only
[[streams]]
name = "debug"
[streams.monitor]
check_interval_ms = 1000
targets = [
{ path = "/var/log/app", pattern = "debug-*.log", is_file = false }
]
[streams.httpserver]
enabled = true
port = 8090
buffer_size = 5000
stream_path = "/debug"
status_path = "/debug/status"
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 100.0 # Higher limit for internal use
burst_size = 200
limit_by = "ip"
max_connections_per_ip = 10

View File

@ -1,7 +1,7 @@
# LogWisp Minimal Configuration Example # LogWisp Minimal Configuration
# Save as: ~/.config/logwisp.toml # Save as: ~/.config/logwisp.toml
# Monitor application logs # Basic stream monitoring application logs
[[streams]] [[streams]]
name = "app" name = "app"
@ -11,20 +11,32 @@ targets = [
{ path = "/var/log/myapp", pattern = "*.log", is_file = false } { path = "/var/log/myapp", pattern = "*.log", is_file = false }
] ]
# Optional: Filter for errors and warnings only
# [[streams.filters]]
# type = "include"
# patterns = ["ERROR", "WARN", "CRITICAL"]
[streams.httpserver] [streams.httpserver]
enabled = true enabled = true
port = 8080 port = 8080
stream_path = "/stream" stream_path = "/stream"
status_path = "/status" status_path = "/status"
# Optional: Enable rate limiting # Optional additions:
# 1. Filter for errors only:
# [[streams.filters]]
# type = "include"
# patterns = ["ERROR", "WARN", "CRITICAL", "FATAL"]
# 2. Enable rate limiting:
# [streams.httpserver.rate_limit] # [streams.httpserver.rate_limit]
# enabled = true # enabled = true
# requests_per_second = 10.0 # requests_per_second = 10.0
# burst_size = 20 # burst_size = 20
# limit_by = "ip" # limit_by = "ip"
# 3. Add heartbeat:
# [streams.httpserver.heartbeat]
# enabled = true
# interval_seconds = 30
# 4. Change LogWisp's own logging:
# [logging]
# output = "file"
# level = "info"

76
doc/README.md Normal file
View File

@ -0,0 +1,76 @@
# LogWisp Documentation
Welcome to the LogWisp documentation. This guide covers all aspects of installing, configuring, and using LogWisp for multi-stream log monitoring.
## 📚 Documentation Index
### Getting Started
- **[Installation Guide](installation.md)** - How to install LogWisp on various platforms
- **[Quick Start](quickstart.md)** - Get up and running in 5 minutes
- **[Architecture Overview](architecture.md)** - System design and components
### Configuration
- **[Configuration Guide](configuration.md)** - Complete configuration reference
- **[Environment Variables](environment.md)** - Environment variable reference
- **[Command Line Options](cli.md)** - CLI flags and parameters
### Features
- **[Filters Guide](filters.md)** - Pattern-based log filtering
- **[Rate Limiting](ratelimiting.md)** - Request and connection limiting
- **[Router Mode](router.md)** - Path-based multi-stream routing
- **[Authentication](authentication.md)** - Securing your log streams *(planned)*
### Operations
- **[Monitoring & Status](monitoring.md)** - Health checks and statistics
- **[Performance Tuning](performance.md)** - Optimization guidelines
- **[Troubleshooting](troubleshooting.md)** - Common issues and solutions
### Advanced Topics
- **[Security Best Practices](security.md)** - Hardening your deployment
- **[Integration Examples](integrations.md)** - Working with other tools
- **[Development Guide](development.md)** - Contributing to LogWisp
## 🚀 Quick Links
- **[Example Configurations](examples/)** - Ready-to-use config templates
- **[API Reference](api.md)** - SSE/TCP protocol documentation
- **[Changelog](../CHANGELOG.md)** - Version history and updates
## 💡 Common Use Cases
### Single Application Monitoring
Monitor logs from one application with basic filtering:
```toml
[[streams]]
name = "myapp"
[streams.monitor]
targets = [{ path = "/var/log/myapp", pattern = "*.log" }]
[[streams.filters]]
type = "include"
patterns = ["ERROR", "WARN"]
```
### Multi-Service Architecture
Monitor multiple services with different configurations:
```bash
logwisp --router --config /etc/logwisp/services.toml
```
### High-Security Environments
Enable authentication and rate limiting:
```toml
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 10.0
max_connections_per_ip = 3
```
## 🔍 Finding Help
- **GitHub Issues**: [Report bugs or request features](https://github.com/logwisp/logwisp/issues)
- **Discussions**: [Ask questions and share ideas](https://github.com/logwisp/logwisp/discussions)
- **Examples**: Check the [examples directory](examples/) for common scenarios
## 📝 License
BSD-3-Clause

View File

@ -1,422 +0,0 @@
# LogWisp Architecture and Project Structure
## Directory Structure
```
logwisp/
├── Makefile # Build automation with version injection
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── README.md # Project documentation
├── config/
│ ├── logwisp.toml.defaults # Default configuration and guide
│ ├── logwisp.toml.example # Example configuration
│ └── logwisp.toml.minimal # Minimal configuration template
├── doc/
│ └── architecture.md # This file - architecture documentation
└── src/
├── cmd/
│ └── logwisp/
│ └── main.go # Application entry point, CLI handling
└── internal/
├── config/
│ ├── auth.go # Authentication configuration structures
│ ├── config.go # Main configuration structures
│ ├── loader.go # Configuration loading with lixenwraith/config
│ ├── server.go # TCP/HTTP server configurations with rate limiting
│ ├── ssl.go # SSL/TLS configuration structures
│ ├── stream.go # Stream-specific configurations with filters
│ └── validation.go # Configuration validation including filters and rate limits
├── filter/
│ ├── filter.go # Regex-based log filtering implementation
│ └── chain.go # Sequential filter chain management
├── monitor/
│ ├── file_watcher.go # File watching and rotation detection
│ └── monitor.go # Log monitoring interface and implementation
├── ratelimit/
│ ├── ratelimit.go # Token bucket algorithm implementation
│ └── limiter.go # Per-stream rate limiter with IP tracking
├── service/
│ ├── httprouter.go # HTTP router for path-based routing
│ ├── logstream.go # Stream lifecycle management
│ ├── routerserver.go # Router server implementation
│ └── service.go # Multi-stream service orchestration
├── transport/
│ ├── httpstreamer.go # HTTP/SSE streaming with rate limiting
│ ├── noop_logger.go # Silent logger for gnet
│ ├── tcpserver.go # TCP server with rate limiting (gnet)
│ └── tcpstreamer.go # TCP streaming implementation
└── version/
└── version.go # Version information management
```
## Configuration System
### Configuration Hierarchy (Highest to Lowest Priority)
1. **CLI Arguments**: Direct command-line flags
2. **Environment Variables**: `LOGWISP_` prefixed variables
3. **Configuration File**: TOML format configuration
4. **Built-in Defaults**: Hardcoded default values
### Configuration Locations
```bash
# Default configuration file location
~/.config/logwisp.toml
# Override via environment variable
export LOGWISP_CONFIG_FILE=/etc/logwisp/production.toml
# Override config directory
export LOGWISP_CONFIG_DIR=/etc/logwisp
export LOGWISP_CONFIG_FILE=production.toml # Relative to CONFIG_DIR
# Direct CLI override
./logwisp --config /path/to/config.toml
```
### Environment Variable Mapping
Environment variables follow a structured naming pattern:
- Prefix: `LOGWISP_`
- Path separator: `_` (underscore)
- Array index: Numeric suffix (0-based)
Examples:
```bash
# Stream-specific settings
LOGWISP_STREAMS_0_NAME=app
LOGWISP_STREAMS_0_MONITOR_CHECK_INTERVAL_MS=50
LOGWISP_STREAMS_0_HTTPSERVER_PORT=8080
LOGWISP_STREAMS_0_HTTPSERVER_BUFFER_SIZE=2000
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_ENABLED=true
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_FORMAT=json
# Filter configuration
LOGWISP_STREAMS_0_FILTERS_0_TYPE=include
LOGWISP_STREAMS_0_FILTERS_0_LOGIC=or
LOGWISP_STREAMS_0_FILTERS_0_PATTERNS='["ERROR","WARN"]'
# Rate limiting configuration
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_ENABLED=true
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_REQUESTS_PER_SECOND=10.0
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_BURST_SIZE=20
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_LIMIT_BY=ip
# Multiple streams
LOGWISP_STREAMS_1_NAME=system
LOGWISP_STREAMS_1_MONITOR_CHECK_INTERVAL_MS=1000
LOGWISP_STREAMS_1_TCPSERVER_PORT=9090
```
## Component Architecture
### Core Components
1. **Service (`logstream.Service`)**
- Manages multiple log streams
- Handles lifecycle (creation, shutdown)
- Provides global statistics
- Thread-safe stream registry
2. **LogStream (`logstream.LogStream`)**
- Represents a single log monitoring pipeline
- Contains: Monitor + Filter Chain + Rate Limiter + Servers (TCP/HTTP)
- Independent configuration
- Per-stream statistics with filter and rate limit metrics
3. **Monitor (`monitor.Monitor`)**
- Watches files and directories
- Detects log rotation
- Publishes log entries to subscribers
- Configurable check intervals
4. **Filter (`filter.Filter`)**
- Regex-based log filtering
- Include (whitelist) or Exclude (blacklist) modes
- OR/AND logic for multiple patterns
- Per-filter statistics (processed, matched, dropped)
5. **Filter Chain (`filter.Chain`)**
- Sequential application of multiple filters
- All filters must pass for entry to be streamed
- Aggregate statistics across filter chain
6. **Rate Limiter (`ratelimit.Limiter`)**
- Token bucket algorithm for smooth rate limiting
- Per-IP or global limiting strategies
- Connection tracking and limits
- Automatic cleanup of stale entries
- Non-blocking rejection of excess requests
7. **Streamers**
- **HTTPStreamer**: SSE-based streaming over HTTP
- Rate limit enforcement before request handling
- Connection tracking for per-IP limits
- Configurable 429 responses
- **TCPStreamer**: Raw JSON streaming over TCP
- Silent connection drops when rate limited
- Per-IP connection tracking
- Both support configurable heartbeats
- Non-blocking client management
8. **HTTPRouter (`logstream.HTTPRouter`)**
- Optional component for path-based routing
- Consolidates multiple HTTP streams on shared ports
- Provides global status endpoint
- Longest-prefix path matching
- Dynamic stream registration/deregistration
### Data Flow
```
File System → Monitor → LogEntry Channel → Filter Chain → [Rate Limiter] → Streamer → Network Client
↑ ↓ ↓ ↓
└── Rotation Detection Pattern Match Rate Limit Check
↓ ↓
Pass/Drop Accept/Reject
```
### Filter Architecture
```
Log Entry → Filter Chain → Filter 1 → Filter 2 → ... → Output
↓ ↓
Include? Exclude?
↓ ↓
OR/AND OR/AND
Logic Logic
```
### Rate Limiting Architecture
```
Client Request → Rate Limiter → Token Bucket Check → Allow/Deny
↓ ↓
IP Tracking Refill Rate
Cleanup Timer
```
### Configuration Structure
```toml
[[streams]]
name = "stream-name"
[streams.monitor]
check_interval_ms = 100 # Per-transport check interval
targets = [
{ path = "/path/to/logs", pattern = "*.log", is_file = false },
{ path = "/path/to/file.log", is_file = true }
]
# Filter configuration (optional)
[[streams.filters]]
type = "include" # "include" or "exclude"
logic = "or" # "or" or "and"
patterns = [
"(?i)error", # Case-insensitive error matching
"(?i)warn" # Case-insensitive warning matching
]
[[streams.filters]]
type = "exclude"
patterns = ["DEBUG", "TRACE"]
[streams.httpserver]
enabled = true
port = 8080
buffer_size = 1000
stream_path = "/stream"
status_path = "/status"
[streams.httpserver.heartbeat]
enabled = true
interval_seconds = 30
format = "comment" # or "json"
include_timestamp = true
include_stats = false
[streams.httpserver.rate_limit]
enabled = false # Disabled by default
requests_per_second = 10.0 # Token refill rate
burst_size = 20 # Token bucket capacity
limit_by = "ip" # "ip" or "global"
response_code = 429 # HTTP response code
response_message = "Rate limit exceeded"
max_connections_per_ip = 5 # Concurrent connection limit
max_total_connections = 100 # Global connection limit
[streams.tcpserver]
enabled = true
port = 9090
buffer_size = 5000
[streams.tcpserver.heartbeat]
enabled = true
interval_seconds = 60
include_timestamp = true
include_stats = true
[streams.tcpserver.rate_limit]
enabled = false
requests_per_second = 5.0
burst_size = 10
limit_by = "ip"
```
## Filter Implementation
### Filter Types
1. **Include Filter**: Only logs matching patterns are streamed (whitelist)
2. **Exclude Filter**: Logs matching patterns are dropped (blacklist)
### Pattern Logic
- **OR Logic**: Log matches if ANY pattern matches
- **AND Logic**: Log matches only if ALL patterns match
### Filter Chain
- Multiple filters are applied sequentially
- All filters must pass for a log to be streamed
- Efficient short-circuit evaluation
### Performance Considerations
- Regex patterns compiled once at startup
- Cached for efficient matching
- Statistics tracked without locks in hot path
## Rate Limiting Implementation
### Token Bucket Algorithm
- Each IP (or global limiter) gets a bucket with configurable capacity
- Tokens refill at `requests_per_second` rate
- Each request/connection consumes one token
- Smooth rate limiting without hard cutoffs
### Limiting Strategies
1. **Per-IP**: Each client IP gets its own token bucket
2. **Global**: All clients share a single token bucket
### Connection Limits
- Per-IP connection limits prevent single client resource exhaustion
- Global connection limits protect overall system resources
- Checked before rate limits to prevent connection hanging
### Cleanup
- IP entries older than 5 minutes are automatically removed
- Prevents unbounded memory growth
- Runs every minute in background
## Build System
### Makefile Targets
```bash
make build # Build with version information
make install # Install to /usr/local/bin
make clean # Remove built binary
make test # Run test suite
make release TAG=v1.0.0 # Create and push git tag
```
### Version Management
Version information is injected at compile time:
```bash
# Automatic version detection from git
VERSION := $(shell git describe --tags --always --dirty)
GIT_COMMIT := $(shell git rev-parse --short HEAD)
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
# Manual build with version
go build -ldflags "-X 'logwisp/src/internal/version.Version=v1.0.0'" \
-o logwisp ./src/cmd/logwisp
```
## Operating Modes
### 1. Standalone Mode (Default)
- Each stream runs its own HTTP/TCP servers
- Direct port access per stream
- Simple configuration
- Best for single-stream or distinct-port setups
### 2. Router Mode (`--router`)
- HTTP streams share ports via path-based routing
- Consolidated access through URL paths
- Global status endpoint with aggregated statistics
- Best for multi-stream setups with limited ports
- Streams accessible at `/{stream_name}/{path}`
## Testing
### Test Suites
1. **Router Testing** (`test_router.sh`)
- Path routing verification
- Client isolation between streams
- Statistics aggregation
- Graceful shutdown
- Port conflict handling
2. **Rate Limiting Testing** (`test_ratelimit.sh`)
- Per-IP rate limiting
- Global rate limiting
- Connection limits
- Rate limit recovery
- Statistics accuracy
- Stress testing
3. **Filter Testing** (recommended)
- Pattern matching accuracy
- Include/exclude logic
- OR/AND combination logic
- Performance with complex patterns
- Filter chain behavior
### Running Tests
```bash
# Test router functionality
./test_router.sh
# Test rate limiting
./test_ratelimit.sh
# Run all tests
make test
```
## Performance Considerations
### Filter Overhead
- Regex compilation: One-time cost at startup
- Pattern matching: O(n*m) where n=patterns, m=text length
- Use simple patterns when possible
- Consider pattern order (most likely matches first)
### Rate Limiting Overhead
- Token bucket checks: O(1) time complexity
- Memory: ~100 bytes per tracked IP
- Cleanup: Runs asynchronously every minute
- Minimal impact when disabled
### Optimization Guidelines
- Use specific patterns to reduce regex complexity
- Place most selective filters first in chain
- Use per-IP limiting for fairness
- Use global limiting for resource protection
- Set burst size to 2-3x requests_per_second
- Monitor rate limit statistics for tuning
- Higher check_interval_ms for low-activity logs
## Security Architecture
### Current Security Features
- Read-only file access
- Rate limiting for DDoS protection
- Connection limits for resource protection
- Non-blocking request rejection
- Regex pattern validation at startup

155
doc/cli.md Normal file
View File

@ -0,0 +1,155 @@
# Command Line Interface
LogWisp provides a comprehensive set of command-line options for controlling its behavior without modifying configuration files.
## Synopsis
```bash
logwisp [options]
```
## General Options
### `--config <path>`
Specify the configuration file location.
- **Default**: `~/.config/logwisp.toml`
- **Example**: `logwisp --config /etc/logwisp/production.toml`
### `--router`
Enable HTTP router mode for path-based routing of multiple streams.
- **Default**: `false` (standalone mode)
- **Use case**: Consolidate multiple HTTP streams on shared ports
- **Example**: `logwisp --router`
### `--version`
Display version information and exit.
- **Example**: `logwisp --version`
### `--background`
Run LogWisp as a background process.
- **Default**: `false` (foreground mode)
- **Example**: `logwisp --background`
## Logging Options
These options override the corresponding configuration file settings.
### `--log-output <mode>`
Control where LogWisp writes its own operational logs.
- **Values**: `file`, `stdout`, `stderr`, `both`, `none`
- **Default**: Configured value or `stderr`
- **Example**: `logwisp --log-output both`
#### Output Modes:
- `file`: Write logs only to files
- `stdout`: Write logs only to standard output
- `stderr`: Write logs only to standard error
- `both`: Write logs to both files and console
- `none`: Disable logging (⚠️ SECURITY: Not recommended)
### `--log-level <level>`
Set the minimum log level for LogWisp's operational logs.
- **Values**: `debug`, `info`, `warn`, `error`
- **Default**: Configured value or `info`
- **Example**: `logwisp --log-level debug`
### `--log-file <path>`
Specify the log file path when using file output.
- **Default**: Configured value or `./logs/logwisp.log`
- **Example**: `logwisp --log-output file --log-file /var/log/logwisp/app.log`
### `--log-dir <directory>`
Specify the log directory when using file output.
- **Default**: Configured value or `./logs`
- **Example**: `logwisp --log-output file --log-dir /var/log/logwisp`
### `--log-console <target>`
Control console output destination when using `stdout`, `stderr`, or `both` modes.
- **Values**: `stdout`, `stderr`, `split`
- **Default**: `stderr`
- **Example**: `logwisp --log-output both --log-console split`
#### Console Targets:
- `stdout`: All logs to standard output
- `stderr`: All logs to standard error
- `split`: INFO/DEBUG to stdout, WARN/ERROR to stderr (planned)
## Examples
### Basic Usage
```bash
# Start with default configuration
logwisp
# Use a specific configuration file
logwisp --config /etc/logwisp/production.toml
```
### Development Mode
```bash
# Enable debug logging to console
logwisp --log-output stderr --log-level debug
# Debug with file output
logwisp --log-output both --log-level debug --log-dir ./debug-logs
```
### Production Deployment
```bash
# File logging with info level
logwisp --log-output file --log-dir /var/log/logwisp --log-level info
# Background mode with custom config
logwisp --background --config /etc/logwisp/prod.toml
# Router mode for multiple services
logwisp --router --config /etc/logwisp/services.toml
```
### Troubleshooting
```bash
# Maximum verbosity to stderr
logwisp --log-output stderr --log-level debug
# Check version
logwisp --version
# Test configuration without backgrounding
logwisp --config test.toml --log-level debug
```
## Priority Order
Configuration values are applied in the following priority order (highest to lowest):
1. **Command-line flags** - Explicitly specified options
2. **Environment variables** - `LOGWISP_*` prefixed variables
3. **Configuration file** - TOML configuration
4. **Built-in defaults** - Hardcoded fallback values
## Exit Codes
- `0`: Successful execution
- `1`: General error (configuration, startup failure)
- `2`: Invalid command-line arguments
## Signals
LogWisp responds to the following signals:
- `SIGINT` (Ctrl+C): Graceful shutdown
- `SIGTERM`: Graceful shutdown
- `SIGKILL`: Immediate termination (not recommended)
During graceful shutdown, LogWisp will:
1. Stop accepting new connections
2. Finish streaming to existing clients
3. Flush all buffers
4. Close all file handles
5. Exit cleanly
## See Also
- [Configuration Guide](configuration.md) - Complete configuration reference
- [Environment Variables](environment.md) - Environment variable options
- [Router Mode](router.md) - Path-based routing details

354
doc/configuration.md Normal file
View File

@ -0,0 +1,354 @@
# Configuration Guide
LogWisp uses TOML format for configuration with sensible defaults for all settings.
## Configuration File Location
Default search order:
1. Command line: `--config /path/to/config.toml`
2. Environment: `$LOGWISP_CONFIG_FILE`
3. User config: `~/.config/logwisp.toml`
4. Current directory: `./logwisp.toml`
## Configuration Structure
```toml
# Optional: LogWisp's own logging configuration
[logging]
output = "stderr" # file, stdout, stderr, both, none
level = "info" # debug, info, warn, error
# Required: At least one stream
[[streams]]
name = "default" # Unique identifier
[streams.monitor] # Required: What to monitor
# ... monitor settings ...
[streams.httpserver] # Optional: HTTP/SSE server
# ... HTTP settings ...
[streams.tcpserver] # Optional: TCP server
# ... TCP settings ...
[[streams.filters]] # Optional: Log filtering
# ... filter settings ...
```
## Logging Configuration
Controls LogWisp's operational logging (not the logs being monitored).
```toml
[logging]
output = "stderr" # Where to write LogWisp's logs
level = "info" # Minimum log level
# File output settings (when output includes "file")
[logging.file]
directory = "./logs" # Log directory
name = "logwisp" # Base filename
max_size_mb = 100 # Rotate at this size
max_total_size_mb = 1000 # Total size limit
retention_hours = 168 # Keep for 7 days
# Console output settings
[logging.console]
target = "stderr" # stdout, stderr, split
format = "txt" # txt or json
```
## Stream Configuration
Each `[[streams]]` section defines an independent log monitoring pipeline.
### Monitor Settings
What files or directories to watch:
```toml
[streams.monitor]
check_interval_ms = 100 # How often to check for new entries
# Monitor targets (at least one required)
targets = [
# Watch all .log files in a directory
{ path = "/var/log/myapp", pattern = "*.log", is_file = false },
# Watch a specific file
{ path = "/var/log/app.log", is_file = true },
# Multiple patterns
{ path = "/logs", pattern = "app-*.log", is_file = false },
{ path = "/logs", pattern = "error-*.txt", is_file = false }
]
```
### HTTP Server (SSE)
Server-Sent Events streaming over HTTP:
```toml
[streams.httpserver]
enabled = true
port = 8080
buffer_size = 1000 # Per-client event buffer
stream_path = "/stream" # SSE endpoint
status_path = "/status" # Statistics endpoint
# Keep-alive heartbeat
[streams.httpserver.heartbeat]
enabled = true
interval_seconds = 30
format = "comment" # "comment" or "json"
include_timestamp = true
include_stats = false
# Rate limiting (optional)
[streams.httpserver.rate_limit]
enabled = false
requests_per_second = 10.0
burst_size = 20
limit_by = "ip" # "ip" or "global"
response_code = 429
response_message = "Rate limit exceeded"
max_connections_per_ip = 5
max_total_connections = 100
```
### TCP Server
Raw TCP streaming for high performance:
```toml
[streams.tcpserver]
enabled = true
port = 9090
buffer_size = 5000 # Larger buffer for TCP
# Heartbeat (always JSON format for TCP)
[streams.tcpserver.heartbeat]
enabled = true
interval_seconds = 60
include_timestamp = true
include_stats = false
# Rate limiting
[streams.tcpserver.rate_limit]
enabled = false
requests_per_second = 5.0
burst_size = 10
limit_by = "ip"
```
### Filters
Control which log entries are streamed:
```toml
# Include filter - only matching logs pass
[[streams.filters]]
type = "include"
logic = "or" # "or" = match any, "and" = match all
patterns = [
"ERROR",
"WARN",
"CRITICAL"
]
# Exclude filter - matching logs are dropped
[[streams.filters]]
type = "exclude"
patterns = [
"DEBUG",
"health check"
]
```
## Complete Examples
### Minimal Configuration
```toml
[[streams]]
name = "simple"
[streams.monitor]
targets = [{ path = "./logs", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080
```
### Production Web Application
```toml
[logging]
output = "file"
level = "info"
[logging.file]
directory = "/var/log/logwisp"
max_size_mb = 500
retention_hours = 336 # 14 days
[[streams]]
name = "webapp"
[streams.monitor]
check_interval_ms = 50
targets = [
{ path = "/var/log/nginx", pattern = "access.log*" },
{ path = "/var/log/nginx", pattern = "error.log*" },
{ path = "/var/log/myapp", pattern = "*.log" }
]
# Only errors and warnings
[[streams.filters]]
type = "include"
logic = "or"
patterns = [
"\\b(ERROR|error|Error)\\b",
"\\b(WARN|WARNING|warn|warning)\\b",
"\\b(CRITICAL|FATAL|critical|fatal)\\b",
"status=[4-5][0-9][0-9]" # HTTP errors
]
# Exclude noise
[[streams.filters]]
type = "exclude"
patterns = [
"/health",
"/metrics",
"ELB-HealthChecker"
]
[streams.httpserver]
enabled = true
port = 8080
buffer_size = 2000
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 25.0
burst_size = 50
max_connections_per_ip = 10
```
### Multi-Service with Router
```toml
# Run with: logwisp --router
# Service 1: API
[[streams]]
name = "api"
[streams.monitor]
targets = [{ path = "/var/log/api", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080 # All streams can use same port in router mode
# Service 2: Database
[[streams]]
name = "database"
[streams.monitor]
targets = [{ path = "/var/log/postgresql", pattern = "*.log" }]
[[streams.filters]]
type = "include"
patterns = ["ERROR", "FATAL", "deadlock", "timeout"]
[streams.httpserver]
enabled = true
port = 8080
# Service 3: System
[[streams]]
name = "system"
[streams.monitor]
targets = [
{ path = "/var/log/syslog", is_file = true },
{ path = "/var/log/auth.log", is_file = true }
]
[streams.tcpserver]
enabled = true
port = 9090
```
### High-Security Configuration
```toml
[logging]
output = "file"
level = "warn" # Less verbose
[[streams]]
name = "secure"
[streams.monitor]
targets = [{ path = "/var/log/secure", pattern = "*.log" }]
# Only security events
[[streams.filters]]
type = "include"
patterns = [
"auth",
"sudo",
"ssh",
"login",
"failed",
"denied"
]
[streams.httpserver]
enabled = true
port = 8443
# Strict rate limiting
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 2.0
burst_size = 3
limit_by = "ip"
max_connections_per_ip = 1
response_code = 403 # Forbidden instead of 429
# Future: Authentication
# [streams.auth]
# type = "basic"
# [streams.auth.basic_auth]
# users_file = "/etc/logwisp/users.htpasswd"
```
## Configuration Tips
### Performance Tuning
- **check_interval_ms**: Higher values reduce CPU usage
- **buffer_size**: Larger buffers handle bursts better
- **rate_limit**: Essential for public-facing streams
### Filter Patterns
- Use word boundaries: `\\berror\\b` (won't match "errorCode")
- Case-insensitive: `(?i)error`
- Anchors for speed: `^ERROR` faster than `ERROR`
- Test complex patterns before deployment
### Resource Limits
- Each stream uses ~10-50MB RAM (depending on buffers)
- CPU usage scales with check_interval and file activity
- Network bandwidth depends on log volume and client count
## Validation
LogWisp validates configuration on startup:
- Required fields (name, monitor targets)
- Port conflicts between streams
- Pattern syntax for filters
- Path accessibility
## See Also
- [Environment Variables](environment.md) - Override via environment
- [CLI Options](cli.md) - Override via command line
- [Filter Guide](filters.md) - Advanced filtering patterns
- [Examples](examples/) - Ready-to-use configurations

275
doc/environment.md Normal file
View File

@ -0,0 +1,275 @@
# Environment Variables
LogWisp supports comprehensive configuration through environment variables, allowing deployment without configuration files or dynamic overrides in containerized environments.
## Naming Convention
Environment variables follow a structured pattern:
- **Prefix**: `LOGWISP_`
- **Path separator**: `_` (underscore)
- **Array indices**: Numeric suffix (0-based)
- **Case**: UPPERCASE
### Examples:
- Config file setting: `logging.level = "debug"`
- Environment variable: `LOGWISP_LOGGING_LEVEL=debug`
- Array element: `streams[0].name = "app"`
- Environment variable: `LOGWISP_STREAMS_0_NAME=app`
## General Variables
### `LOGWISP_CONFIG_FILE`
Path to the configuration file.
- **Default**: `~/.config/logwisp.toml`
- **Example**: `LOGWISP_CONFIG_FILE=/etc/logwisp/config.toml`
### `LOGWISP_CONFIG_DIR`
Directory containing configuration files.
- **Usage**: Combined with `LOGWISP_CONFIG_FILE` for relative paths
- **Example**:
```bash
export LOGWISP_CONFIG_DIR=/etc/logwisp
export LOGWISP_CONFIG_FILE=production.toml
# Loads: /etc/logwisp/production.toml
```
### `LOGWISP_DISABLE_STATUS_REPORTER`
Disable the periodic status reporter.
- **Values**: `1` (disable), `0` or unset (enable)
- **Default**: `0` (enabled)
- **Example**: `LOGWISP_DISABLE_STATUS_REPORTER=1`
### `LOGWISP_BACKGROUND`
Internal marker for background process detection.
- **Note**: Set automatically by `--background` flag
- **Values**: `1` (background), unset (foreground)
## Logging Variables
### `LOGWISP_LOGGING_OUTPUT`
LogWisp's operational log output mode.
- **Values**: `file`, `stdout`, `stderr`, `both`, `none`
- **Example**: `LOGWISP_LOGGING_OUTPUT=both`
### `LOGWISP_LOGGING_LEVEL`
Minimum log level for operational logs.
- **Values**: `debug`, `info`, `warn`, `error`
- **Example**: `LOGWISP_LOGGING_LEVEL=debug`
### File Logging
```bash
LOGWISP_LOGGING_FILE_DIRECTORY=/var/log/logwisp
LOGWISP_LOGGING_FILE_NAME=logwisp
LOGWISP_LOGGING_FILE_MAX_SIZE_MB=100
LOGWISP_LOGGING_FILE_MAX_TOTAL_SIZE_MB=1000
LOGWISP_LOGGING_FILE_RETENTION_HOURS=168 # 7 days
```
### Console Logging
```bash
LOGWISP_LOGGING_CONSOLE_TARGET=stderr # stdout, stderr, split
LOGWISP_LOGGING_CONSOLE_FORMAT=txt # txt, json
```
## Stream Configuration
Streams are configured using array indices (0-based).
### Basic Stream Settings
```bash
# First stream (index 0)
LOGWISP_STREAMS_0_NAME=app
LOGWISP_STREAMS_0_MONITOR_CHECK_INTERVAL_MS=100
# Second stream (index 1)
LOGWISP_STREAMS_1_NAME=system
LOGWISP_STREAMS_1_MONITOR_CHECK_INTERVAL_MS=1000
```
### Monitor Targets
```bash
# Single file target
LOGWISP_STREAMS_0_MONITOR_TARGETS_0_PATH=/var/log/app.log
LOGWISP_STREAMS_0_MONITOR_TARGETS_0_IS_FILE=true
# Directory with pattern
LOGWISP_STREAMS_0_MONITOR_TARGETS_1_PATH=/var/log/myapp
LOGWISP_STREAMS_0_MONITOR_TARGETS_1_PATTERN="*.log"
LOGWISP_STREAMS_0_MONITOR_TARGETS_1_IS_FILE=false
```
### Filters
```bash
# Include filter
LOGWISP_STREAMS_0_FILTERS_0_TYPE=include
LOGWISP_STREAMS_0_FILTERS_0_LOGIC=or
LOGWISP_STREAMS_0_FILTERS_0_PATTERNS='["ERROR","WARN","CRITICAL"]'
# Exclude filter
LOGWISP_STREAMS_0_FILTERS_1_TYPE=exclude
LOGWISP_STREAMS_0_FILTERS_1_PATTERNS='["DEBUG","TRACE"]'
```
### HTTP Server
```bash
LOGWISP_STREAMS_0_HTTPSERVER_ENABLED=true
LOGWISP_STREAMS_0_HTTPSERVER_PORT=8080
LOGWISP_STREAMS_0_HTTPSERVER_BUFFER_SIZE=1000
LOGWISP_STREAMS_0_HTTPSERVER_STREAM_PATH=/stream
LOGWISP_STREAMS_0_HTTPSERVER_STATUS_PATH=/status
# Heartbeat
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_ENABLED=true
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_INTERVAL_SECONDS=30
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_FORMAT=comment
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_INCLUDE_TIMESTAMP=true
LOGWISP_STREAMS_0_HTTPSERVER_HEARTBEAT_INCLUDE_STATS=false
# Rate Limiting
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_ENABLED=true
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_REQUESTS_PER_SECOND=10.0
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_BURST_SIZE=20
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_LIMIT_BY=ip
LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_MAX_CONNECTIONS_PER_IP=5
```
### TCP Server
```bash
LOGWISP_STREAMS_0_TCPSERVER_ENABLED=true
LOGWISP_STREAMS_0_TCPSERVER_PORT=9090
LOGWISP_STREAMS_0_TCPSERVER_BUFFER_SIZE=5000
# Rate Limiting
LOGWISP_STREAMS_0_TCPSERVER_RATE_LIMIT_ENABLED=true
LOGWISP_STREAMS_0_TCPSERVER_RATE_LIMIT_REQUESTS_PER_SECOND=5.0
LOGWISP_STREAMS_0_TCPSERVER_RATE_LIMIT_BURST_SIZE=10
```
## Complete Example
Here's a complete example configuring two streams via environment variables:
```bash
#!/bin/bash
# Logging configuration
export LOGWISP_LOGGING_OUTPUT=both
export LOGWISP_LOGGING_LEVEL=info
export LOGWISP_LOGGING_FILE_DIRECTORY=/var/log/logwisp
export LOGWISP_LOGGING_FILE_MAX_SIZE_MB=100
# Stream 0: Application logs
export LOGWISP_STREAMS_0_NAME=app
export LOGWISP_STREAMS_0_MONITOR_CHECK_INTERVAL_MS=50
export LOGWISP_STREAMS_0_MONITOR_TARGETS_0_PATH=/var/log/myapp
export LOGWISP_STREAMS_0_MONITOR_TARGETS_0_PATTERN="*.log"
export LOGWISP_STREAMS_0_MONITOR_TARGETS_0_IS_FILE=false
# Stream 0: Filters
export LOGWISP_STREAMS_0_FILTERS_0_TYPE=include
export LOGWISP_STREAMS_0_FILTERS_0_PATTERNS='["ERROR","WARN"]'
# Stream 0: HTTP server
export LOGWISP_STREAMS_0_HTTPSERVER_ENABLED=true
export LOGWISP_STREAMS_0_HTTPSERVER_PORT=8080
export LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_ENABLED=true
export LOGWISP_STREAMS_0_HTTPSERVER_RATE_LIMIT_REQUESTS_PER_SECOND=25.0
# Stream 1: System logs
export LOGWISP_STREAMS_1_NAME=system
export LOGWISP_STREAMS_1_MONITOR_CHECK_INTERVAL_MS=1000
export LOGWISP_STREAMS_1_MONITOR_TARGETS_0_PATH=/var/log/syslog
export LOGWISP_STREAMS_1_MONITOR_TARGETS_0_IS_FILE=true
# Stream 1: TCP server
export LOGWISP_STREAMS_1_TCPSERVER_ENABLED=true
export LOGWISP_STREAMS_1_TCPSERVER_PORT=9090
# Start LogWisp
logwisp
```
## Docker/Kubernetes Usage
Environment variables are ideal for containerized deployments:
### Docker
```dockerfile
FROM logwisp:latest
ENV LOGWISP_LOGGING_OUTPUT=stdout
ENV LOGWISP_STREAMS_0_NAME=container
ENV LOGWISP_STREAMS_0_MONITOR_TARGETS_0_PATH=/var/log/app
ENV LOGWISP_STREAMS_0_HTTPSERVER_PORT=8080
```
### Docker Compose
```yaml
version: '3'
services:
logwisp:
image: logwisp:latest
environment:
- LOGWISP_LOGGING_OUTPUT=stdout
- LOGWISP_STREAMS_0_NAME=webapp
- LOGWISP_STREAMS_0_MONITOR_TARGETS_0_PATH=/logs
- LOGWISP_STREAMS_0_HTTPSERVER_PORT=8080
volumes:
- ./logs:/logs:ro
ports:
- "8080:8080"
```
### Kubernetes ConfigMap
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: logwisp-config
data:
LOGWISP_LOGGING_LEVEL: "info"
LOGWISP_STREAMS_0_NAME: "k8s-app"
LOGWISP_STREAMS_0_HTTPSERVER_PORT: "8080"
```
## Precedence Rules
When the same setting is configured multiple ways, this precedence applies:
1. **Command-line flags** (highest priority)
2. **Environment variables**
3. **Configuration file**
4. **Default values** (lowest priority)
Example:
```bash
# Config file has: logging.level = "info"
export LOGWISP_LOGGING_LEVEL=warn
logwisp --log-level debug
# Result: log level will be "debug" (CLI flag wins)
```
## Debugging
To see which environment variables LogWisp recognizes:
```bash
# List all LOGWISP variables
env | grep ^LOGWISP_
# Test configuration parsing
LOGWISP_LOGGING_LEVEL=debug logwisp --version
```
## Security Considerations
- **Sensitive Values**: Avoid putting passwords or tokens in environment variables
- **Process Visibility**: Environment variables may be visible to other processes
- **Container Security**: Use secrets management for sensitive configuration
- **Logging**: Be careful not to log environment variable values
## See Also
- [Configuration Guide](configuration.md) - Complete configuration reference
- [CLI Options](cli.md) - Command-line interface
- [Docker Deployment](integrations.md#docker) - Container-specific guidance

439
doc/filters.md Normal file
View File

@ -0,0 +1,439 @@
# Filter Guide
LogWisp's filtering system allows you to control which log entries are streamed to clients, reducing noise and focusing on what matters.
## How Filters Work
Filters use regular expressions to match log entries. Each filter can either:
- **Include**: Only matching logs pass through (whitelist)
- **Exclude**: Matching logs are dropped (blacklist)
Multiple filters are applied sequentially - a log entry must pass ALL filters to be streamed.
## Filter Configuration
### Basic Structure
```toml
[[streams.filters]]
type = "include" # or "exclude"
logic = "or" # or "and"
patterns = [
"pattern1",
"pattern2"
]
```
### Filter Types
#### Include Filter (Whitelist)
Only logs matching the patterns are streamed:
```toml
[[streams.filters]]
type = "include"
logic = "or"
patterns = [
"ERROR",
"WARN",
"CRITICAL"
]
# Result: Only ERROR, WARN, or CRITICAL logs are streamed
```
#### Exclude Filter (Blacklist)
Logs matching the patterns are dropped:
```toml
[[streams.filters]]
type = "exclude"
patterns = [
"DEBUG",
"TRACE",
"/health"
]
# Result: DEBUG, TRACE, and health check logs are filtered out
```
### Logic Operators
#### OR Logic (Default)
Log matches if ANY pattern matches:
```toml
[[streams.filters]]
type = "include"
logic = "or"
patterns = ["ERROR", "FAIL", "EXCEPTION"]
# Matches: "ERROR: disk full" OR "FAIL: connection timeout" OR "NullPointerException"
```
#### AND Logic
Log matches only if ALL patterns match:
```toml
[[streams.filters]]
type = "include"
logic = "and"
patterns = ["database", "timeout", "ERROR"]
# Matches: "ERROR: database connection timeout"
# Doesn't match: "ERROR: file not found" (missing "database" and "timeout")
```
## Pattern Syntax
LogWisp uses Go's regular expression syntax (RE2):
### Basic Patterns
```toml
patterns = [
"ERROR", # Exact substring match
"(?i)error", # Case-insensitive
"\\berror\\b", # Word boundaries
"^ERROR", # Start of line
"ERROR$", # End of line
"ERR(OR)?", # Optional group
"error|fail|exception" # Alternatives
]
```
### Common Pattern Examples
#### Log Levels
```toml
# Standard log levels
patterns = [
"\\[(ERROR|WARN|INFO|DEBUG)\\]", # [ERROR] format
"(?i)\\b(error|warning|info|debug)\\b", # Word boundaries
"level=(error|warn|info|debug)", # key=value format
"<(Error|Warning|Info|Debug)>" # XML-style
]
# Severity patterns
patterns = [
"(?i)(fatal|critical|severe)",
"(?i)(error|fail|exception)",
"(?i)(warn|warning|caution)",
"panic:", # Go panics
"Traceback", # Python errors
]
```
#### Application Errors
```toml
# Java/JVM
patterns = [
"Exception",
"\\.java:[0-9]+", # Stack trace lines
"at com\\.mycompany\\.", # Company packages
"NullPointerException|ClassNotFoundException"
]
# Python
patterns = [
"Traceback \\(most recent call last\\)",
"File \".+\\.py\", line [0-9]+",
"(ValueError|TypeError|KeyError)"
]
# Go
patterns = [
"panic:",
"goroutine [0-9]+",
"runtime error:"
]
# Node.js
patterns = [
"Error:",
"at .+ \\(.+\\.js:[0-9]+:[0-9]+\\)",
"UnhandledPromiseRejection"
]
```
#### Performance Issues
```toml
patterns = [
"took [0-9]{4,}ms", # Operations over 999ms
"duration>[0-9]{3,}s", # Long durations
"timeout|timed out", # Timeouts
"slow query", # Database
"memory pressure", # Memory issues
"high cpu|cpu usage: [8-9][0-9]%" # CPU issues
]
```
#### Security Patterns
```toml
patterns = [
"(?i)(unauthorized|forbidden|denied)",
"(?i)(auth|authentication) fail",
"invalid (token|session|credentials)",
"SQL injection|XSS|CSRF",
"brute force|rate limit",
"suspicious activity"
]
```
#### HTTP Patterns
```toml
# Error status codes
patterns = [
"status[=:][4-5][0-9]{2}", # status=404, status:500
"HTTP/[0-9.]+ [4-5][0-9]{2}", # HTTP/1.1 404
"\"status\":\\s*[4-5][0-9]{2}" # JSON "status": 500
]
# Specific endpoints
patterns = [
"\"(GET|POST|PUT|DELETE) /api/",
"/api/v[0-9]+/users",
"path=\"/admin"
]
```
## Filter Chains
Multiple filters create a processing chain. Each filter must pass for the log to be streamed.
### Example: Error Monitoring
```toml
# Step 1: Include only errors and warnings
[[streams.filters]]
type = "include"
logic = "or"
patterns = [
"(?i)\\b(error|fail|exception)\\b",
"(?i)\\b(warn|warning)\\b",
"(?i)\\b(critical|fatal|severe)\\b"
]
# Step 2: Exclude known non-issues
[[streams.filters]]
type = "exclude"
patterns = [
"Error: Expected behavior",
"Warning: Deprecated API",
"INFO.*error in message" # INFO logs talking about errors
]
# Step 3: Exclude noisy sources
[[streams.filters]]
type = "exclude"
patterns = [
"/health",
"/metrics",
"ELB-HealthChecker",
"Googlebot"
]
```
### Example: API Monitoring
```toml
# Include only API calls
[[streams.filters]]
type = "include"
patterns = [
"/api/",
"/v[0-9]+/"
]
# Exclude successful requests
[[streams.filters]]
type = "exclude"
patterns = [
"\" 200 ", # HTTP 200 OK
"\" 201 ", # HTTP 201 Created
"\" 204 ", # HTTP 204 No Content
"\" 304 " # HTTP 304 Not Modified
]
# Exclude OPTIONS requests (CORS)
[[streams.filters]]
type = "exclude"
patterns = [
"OPTIONS "
]
```
### Example: Security Audit
```toml
# Include security-relevant events
[[streams.filters]]
type = "include"
logic = "or"
patterns = [
"(?i)auth",
"(?i)login|logout",
"(?i)sudo|root",
"(?i)ssh|sftp|ftp",
"(?i)firewall|iptables",
"COMMAND=", # sudo commands
"USER=", # user actions
"SELINUX"
]
# Must also contain failure/success indicators
[[streams.filters]]
type = "include"
logic = "or"
patterns = [
"(?i)(fail|denied|error)",
"(?i)(success|accepted|granted)",
"(?i)(invalid|unauthorized)"
]
```
## Performance Considerations
### Pattern Complexity
Simple patterns are fast (~1μs per check):
```toml
patterns = ["ERROR", "WARN", "FATAL"]
```
Complex patterns are slower (~10-100μs per check):
```toml
patterns = [
"^\\[\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\]\\s+\\[(ERROR|WARN)\\]\\s+\\[([^\\]]+)\\]\\s+(.+)$"
]
```
### Optimization Tips
1. **Use anchors when possible**:
```toml
"^ERROR" # Faster than "ERROR"
```
2. **Avoid nested quantifiers**:
```toml
# BAD: Can cause exponential backtracking
"((a+)+)+"
# GOOD: Linear time
"a+"
```
3. **Use non-capturing groups**:
```toml
"(?:error|warn)" # Instead of "(error|warn)"
```
4. **Order patterns by frequency**:
```toml
# Most common first
patterns = ["ERROR", "WARN", "INFO", "DEBUG"]
```
5. **Prefer character classes**:
```toml
"[0-9]" # Instead of "\\d"
"[a-zA-Z]" # Instead of "\\w"
```
## Testing Filters
### Test Configuration
Create a test configuration with sample logs:
```toml
[[streams]]
name = "test"
[streams.monitor]
targets = [{ path = "./test-logs", pattern = "*.log" }]
[[streams.filters]]
type = "include"
patterns = ["YOUR_PATTERN_HERE"]
[streams.httpserver]
enabled = true
port = 8888
```
### Generate Test Logs
```bash
# Create test log entries
echo "[ERROR] Database connection failed" >> test-logs/app.log
echo "[INFO] User logged in" >> test-logs/app.log
echo "[WARN] High memory usage: 85%" >> test-logs/app.log
# Run LogWisp with debug logging
logwisp --config test.toml --log-level debug
# Check what passes through
curl -N http://localhost:8888/stream
```
### Debug Filter Behavior
Enable debug logging to see filter decisions:
```bash
logwisp --log-level debug --log-output stderr
```
Look for messages like:
```
Entry filtered out component=filter_chain filter_index=0 filter_type=include
Entry passed all filters component=filter_chain
```
## Common Pitfalls
### Case Sensitivity
By default, patterns are case-sensitive:
```toml
# Won't match "error" or "Error"
patterns = ["ERROR"]
# Use case-insensitive flag
patterns = ["(?i)error"]
```
### Partial Matches
Patterns match substrings by default:
```toml
# Matches "ERROR", "ERRORS", "TERROR"
patterns = ["ERROR"]
# Use word boundaries for exact words
patterns = ["\\bERROR\\b"]
```
### Special Characters
Remember to escape regex special characters:
```toml
# Won't work as expected
patterns = ["[ERROR]"]
# Correct: escape brackets
patterns = ["\\[ERROR\\]"]
```
### Performance Impact
Too many complex patterns can impact performance:
```toml
# Consider splitting into multiple streams instead
[[streams.filters]]
patterns = [
# 50+ complex patterns...
]
```
## Best Practices
1. **Start Simple**: Begin with basic patterns and refine as needed
2. **Test Thoroughly**: Use test logs to verify filter behavior
3. **Monitor Performance**: Check filter statistics in `/status`
4. **Document Patterns**: Comment complex patterns for maintenance
5. **Use Multiple Streams**: Instead of complex filters, consider separate streams
6. **Regular Review**: Periodically review and optimize filter rules
## See Also
- [Configuration Guide](configuration.md) - Complete configuration reference
- [Performance Tuning](performance.md) - Optimization guidelines
- [Examples](examples/) - Real-world filter configurations

591
doc/installation.md Normal file
View File

@ -0,0 +1,591 @@
# Installation Guide
This guide covers installing LogWisp on various platforms and deployment scenarios.
## Requirements
### System Requirements
- **OS**: Linux, macOS, FreeBSD, Windows (with WSL)
- **Architecture**: amd64, arm64
- **Memory**: 64MB minimum, 256MB recommended
- **Disk**: 10MB for binary, plus log storage
- **Go**: 1.23+ (for building from source)
### Runtime Dependencies
LogWisp is a single static binary with no runtime dependencies. It only requires:
- Read access to monitored log files
- Network access for serving streams
- Write access for operational logs (optional)
## Installation Methods
### Pre-built Binaries
Download the latest release:
```bash
# Linux (amd64)
wget https://github.com/yourusername/logwisp/releases/latest/download/logwisp-linux-amd64
chmod +x logwisp-linux-amd64
sudo mv logwisp-linux-amd64 /usr/local/bin/logwisp
# macOS (Intel)
wget https://github.com/yourusername/logwisp/releases/latest/download/logwisp-darwin-amd64
chmod +x logwisp-darwin-amd64
sudo mv logwisp-darwin-amd64 /usr/local/bin/logwisp
# macOS (Apple Silicon)
wget https://github.com/yourusername/logwisp/releases/latest/download/logwisp-darwin-arm64
chmod +x logwisp-darwin-arm64
sudo mv logwisp-darwin-arm64 /usr/local/bin/logwisp
```
Verify installation:
```bash
logwisp --version
```
### From Source
Build from source code:
```bash
# Clone repository
git clone https://github.com/yourusername/logwisp.git
cd logwisp
# Build
make build
# Install
sudo make install
# Or install to custom location
make install PREFIX=/opt/logwisp
```
### Using Go Install
Install directly with Go:
```bash
go install github.com/yourusername/logwisp/src/cmd/logwisp@latest
```
Note: This installs to `$GOPATH/bin` (usually `~/go/bin`)
### Docker
Official Docker image:
```bash
# Pull image
docker pull yourusername/logwisp:latest
# Run with volume mount
docker run -d \
--name logwisp \
-p 8080:8080 \
-v /var/log:/logs:ro \
-v $PWD/config.toml:/config/logwisp.toml:ro \
yourusername/logwisp:latest \
--config /config/logwisp.toml
```
Build your own image:
```dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /build
COPY . .
RUN go build -o logwisp ./src/cmd/logwisp
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /build/logwisp /usr/local/bin/
ENTRYPOINT ["logwisp"]
```
## Platform-Specific Instructions
### Linux
#### Debian/Ubuntu
Create package (planned):
```bash
# Future feature
sudo apt install logwisp
```
Manual installation:
```bash
# Download binary
wget https://github.com/yourusername/logwisp/releases/latest/download/logwisp-linux-amd64 -O logwisp
chmod +x logwisp
sudo mv logwisp /usr/local/bin/
# Create config directory
sudo mkdir -p /etc/logwisp
sudo cp config/logwisp.toml.example /etc/logwisp/logwisp.toml
# Create systemd service
sudo tee /etc/systemd/system/logwisp.service << EOF
[Unit]
Description=LogWisp Log Monitoring Service
After=network.target
[Service]
Type=simple
User=logwisp
Group=logwisp
ExecStart=/usr/local/bin/logwisp --config /etc/logwisp/logwisp.toml
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=logwisp
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/var/log
ReadWritePaths=/var/log/logwisp
[Install]
WantedBy=multi-user.target
EOF
# Create user
sudo useradd -r -s /bin/false logwisp
# Create log directory
sudo mkdir -p /var/log/logwisp
sudo chown logwisp:logwisp /var/log/logwisp
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable logwisp
sudo systemctl start logwisp
```
#### Red Hat/CentOS/Fedora
```bash
# Similar to Debian, but use:
sudo yum install wget # or dnf on newer versions
# SELinux context (if enabled)
sudo semanage fcontext -a -t bin_t /usr/local/bin/logwisp
sudo restorecon -v /usr/local/bin/logwisp
```
#### Arch Linux
AUR package (community maintained):
```bash
# Future feature
yay -S logwisp
```
### macOS
#### Homebrew
Formula (planned):
```bash
# Future feature
brew install logwisp
```
#### Manual Installation
```bash
# Download and install
curl -L https://github.com/yourusername/logwisp/releases/latest/download/logwisp-darwin-$(uname -m) -o logwisp
chmod +x logwisp
sudo mv logwisp /usr/local/bin/
# Create LaunchDaemon
sudo tee /Library/LaunchDaemons/com.logwisp.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.logwisp</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/logwisp</string>
<string>--config</string>
<string>/usr/local/etc/logwisp/logwisp.toml</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/usr/local/var/log/logwisp.log</string>
<key>StandardErrorPath</key>
<string>/usr/local/var/log/logwisp.error.log</string>
</dict>
</plist>
EOF
# Load service
sudo launchctl load /Library/LaunchDaemons/com.logwisp.plist
```
### FreeBSD
#### Ports
```bash
# Future feature
cd /usr/ports/sysutils/logwisp
make install clean
```
#### Manual Installation
```bash
# Download
fetch https://github.com/yourusername/logwisp/releases/latest/download/logwisp-freebsd-amd64
chmod +x logwisp-freebsd-amd64
mv logwisp-freebsd-amd64 /usr/local/bin/logwisp
# RC script
cat > /usr/local/etc/rc.d/logwisp << 'EOF'
#!/bin/sh
# PROVIDE: logwisp
# REQUIRE: DAEMON
# KEYWORD: shutdown
. /etc/rc.subr
name="logwisp"
rcvar="${name}_enable"
command="/usr/local/bin/logwisp"
command_args="--config /usr/local/etc/logwisp/logwisp.toml"
pidfile="/var/run/${name}.pid"
load_rc_config $name
: ${logwisp_enable:="NO"}
run_rc_command "$1"
EOF
chmod +x /usr/local/etc/rc.d/logwisp
# Enable
sysrc logwisp_enable="YES"
service logwisp start
```
### Windows
#### Windows Subsystem for Linux (WSL)
```bash
# Inside WSL, follow Linux instructions
wget https://github.com/yourusername/logwisp/releases/latest/download/logwisp-linux-amd64
chmod +x logwisp-linux-amd64
./logwisp-linux-amd64
```
#### Native Windows (planned)
Future support for native Windows service.
## Container Deployment
### Docker Compose
```yaml
version: '3.8'
services:
logwisp:
image: yourusername/logwisp:latest
container_name: logwisp
restart: unless-stopped
ports:
- "8080:8080"
- "9090:9090" # If using TCP
volumes:
- /var/log:/logs:ro
- ./logwisp.toml:/config/logwisp.toml:ro
command: ["--config", "/config/logwisp.toml"]
environment:
- LOGWISP_LOGGING_LEVEL=info
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/status"]
interval: 30s
timeout: 3s
retries: 3
```
### Kubernetes
Deployment manifest:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: logwisp
labels:
app: logwisp
spec:
replicas: 1
selector:
matchLabels:
app: logwisp
template:
metadata:
labels:
app: logwisp
spec:
containers:
- name: logwisp
image: yourusername/logwisp:latest
args:
- --config
- /config/logwisp.toml
ports:
- containerPort: 8080
name: http
- containerPort: 9090
name: tcp
volumeMounts:
- name: logs
mountPath: /logs
readOnly: true
- name: config
mountPath: /config
livenessProbe:
httpGet:
path: /status
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /status
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: logs
hostPath:
path: /var/log
- name: config
configMap:
name: logwisp-config
---
apiVersion: v1
kind: Service
metadata:
name: logwisp
spec:
selector:
app: logwisp
ports:
- name: http
port: 8080
targetPort: 8080
- name: tcp
port: 9090
targetPort: 9090
---
apiVersion: v1
kind: ConfigMap
metadata:
name: logwisp-config
data:
logwisp.toml: |
[[streams]]
name = "k8s"
[streams.monitor]
targets = [{ path = "/logs", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080
```
## Post-Installation
### Verify Installation
1. Check version:
```bash
logwisp --version
```
2. Test configuration:
```bash
logwisp --config /etc/logwisp/logwisp.toml --log-level debug
```
3. Check service status:
```bash
# systemd
sudo systemctl status logwisp
# macOS
sudo launchctl list | grep logwisp
# FreeBSD
service logwisp status
```
4. Test streaming:
```bash
curl -N http://localhost:8080/stream
```
### Security Hardening
1. **Create dedicated user**:
```bash
sudo useradd -r -s /bin/false -d /var/lib/logwisp logwisp
```
2. **Set file permissions**:
```bash
sudo chown root:root /usr/local/bin/logwisp
sudo chmod 755 /usr/local/bin/logwisp
sudo chown -R logwisp:logwisp /etc/logwisp
sudo chmod 640 /etc/logwisp/logwisp.toml
```
3. **Configure firewall**:
```bash
# UFW
sudo ufw allow 8080/tcp comment "LogWisp HTTP"
# firewalld
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
```
4. **Enable SELinux/AppArmor** (if applicable)
### Initial Configuration
1. Copy example configuration:
```bash
sudo cp /usr/local/share/logwisp/examples/logwisp.toml.example /etc/logwisp/logwisp.toml
```
2. Edit configuration:
```bash
sudo nano /etc/logwisp/logwisp.toml
```
3. Set up log monitoring:
```toml
[[streams]]
name = "myapp"
[streams.monitor]
targets = [
{ path = "/var/log/myapp", pattern = "*.log" }
]
```
4. Restart service:
```bash
sudo systemctl restart logwisp
```
## Uninstallation
### Linux
```bash
# Stop service
sudo systemctl stop logwisp
sudo systemctl disable logwisp
# Remove files
sudo rm /usr/local/bin/logwisp
sudo rm /etc/systemd/system/logwisp.service
sudo rm -rf /etc/logwisp
sudo rm -rf /var/log/logwisp
# Remove user
sudo userdel logwisp
```
### macOS
```bash
# Stop service
sudo launchctl unload /Library/LaunchDaemons/com.logwisp.plist
# Remove files
sudo rm /usr/local/bin/logwisp
sudo rm /Library/LaunchDaemons/com.logwisp.plist
sudo rm -rf /usr/local/etc/logwisp
```
### Docker
```bash
docker stop logwisp
docker rm logwisp
docker rmi yourusername/logwisp:latest
```
## Troubleshooting Installation
### Permission Denied
If you get permission errors:
```bash
# Check file ownership
ls -la /usr/local/bin/logwisp
# Fix permissions
sudo chmod +x /usr/local/bin/logwisp
# Check log directory
sudo mkdir -p /var/log/logwisp
sudo chown logwisp:logwisp /var/log/logwisp
```
### Service Won't Start
Check logs:
```bash
# systemd
sudo journalctl -u logwisp -f
# Manual run
sudo -u logwisp /usr/local/bin/logwisp --config /etc/logwisp/logwisp.toml
```
### Port Already in Use
Find conflicting process:
```bash
sudo lsof -i :8080
# or
sudo netstat -tlnp | grep 8080
```
## See Also
- [Quick Start](quickstart.md) - Get running quickly
- [Configuration Guide](configuration.md) - Configure LogWisp
- [Troubleshooting](troubleshooting.md) - Common issues
- [Security Best Practices](security.md) - Hardening guide

511
doc/monitoring.md Normal file
View File

@ -0,0 +1,511 @@
# Monitoring & Status Guide
LogWisp provides comprehensive monitoring capabilities through status endpoints, operational logs, and metrics.
## Status Endpoints
### Stream Status
Each stream exposes its own status endpoint:
```bash
# Standalone mode
curl http://localhost:8080/status
# Router mode
curl http://localhost:8080/streamname/status
```
Example response:
```json
{
"service": "LogWisp",
"version": "1.0.0",
"server": {
"type": "http",
"port": 8080,
"active_clients": 5,
"buffer_size": 1000,
"uptime_seconds": 3600,
"mode": {
"standalone": true,
"router": false
}
},
"monitor": {
"active_watchers": 3,
"total_entries": 152341,
"dropped_entries": 12,
"start_time": "2024-01-20T10:00:00Z",
"last_entry_time": "2024-01-20T11:00:00Z"
},
"filters": {
"filter_count": 2,
"total_processed": 152341,
"total_passed": 48234,
"filters": [
{
"type": "include",
"logic": "or",
"pattern_count": 3,
"total_processed": 152341,
"total_matched": 48234,
"total_dropped": 0
}
]
},
"features": {
"heartbeat": {
"enabled": true,
"interval": 30,
"format": "comment"
},
"rate_limit": {
"enabled": true,
"total_requests": 8234,
"blocked_requests": 89,
"active_ips": 12,
"total_connections": 5
}
}
}
```
### Global Status (Router Mode)
In router mode, a global status endpoint provides aggregated information:
```bash
curl http://localhost:8080/status
```
## Key Metrics
### Monitor Metrics
Track file watching performance:
| Metric | Description | Healthy Range |
|--------|-------------|---------------|
| `active_watchers` | Number of files being watched | 1-1000 |
| `total_entries` | Total log entries processed | Increasing |
| `dropped_entries` | Entries dropped due to buffer full | < 1% of total |
| `entries_per_second` | Current processing rate | Varies |
### Connection Metrics
Monitor client connections:
| Metric | Description | Warning Signs |
|--------|-------------|---------------|
| `active_clients` | Current SSE connections | Near limit |
| `tcp_connections` | Current TCP connections | Near limit |
| `total_connections` | All active connections | > 80% of max |
### Filter Metrics
Understand filtering effectiveness:
| Metric | Description | Optimization |
|--------|-------------|--------------|
| `total_processed` | Entries checked | - |
| `total_passed` | Entries that passed | Very low = too restrictive |
| `total_dropped` | Entries filtered out | Very high = review patterns |
### Rate Limit Metrics
Track rate limiting impact:
| Metric | Description | Action Needed |
|--------|-------------|---------------|
| `blocked_requests` | Rejected requests | High = increase limits |
| `active_ips` | Unique clients | High = scale out |
| `blocked_percentage` | Rejection rate | > 10% = review |
## Operational Logging
### Log Levels
Configure LogWisp's operational logging:
```toml
[logging]
output = "both" # file and stderr
level = "info" # info for production
```
Log levels and their use:
- **DEBUG**: Detailed internal operations
- **INFO**: Normal operations, connections
- **WARN**: Recoverable issues
- **ERROR**: Errors requiring attention
### Important Log Messages
#### Startup Messages
```
LogWisp starting version=1.0.0 config_file=/etc/logwisp.toml
Stream registered with router stream=app
TCP endpoint configured transport=system port=9090
HTTP endpoints configured transport=app stream_url=http://localhost:8080/stream
```
#### Connection Events
```
HTTP client connected remote_addr=192.168.1.100:54231 active_clients=6
HTTP client disconnected remote_addr=192.168.1.100:54231 active_clients=5
TCP connection opened remote_addr=192.168.1.100:54232 active_connections=3
```
#### Error Conditions
```
Failed to open file for checking path=/var/log/app.log error=permission denied
Scanner error while reading file path=/var/log/huge.log error=token too long
Request rate limited ip=192.168.1.100
Connection limit exceeded ip=192.168.1.100 connections=5 limit=5
```
#### Performance Warnings
```
Dropped log entry - subscriber buffer full
Dropped entry for slow client remote_addr=192.168.1.100
Check interval too small: 5ms (min: 10ms)
```
## Health Checks
### Basic Health Check
Simple up/down check:
```bash
#!/bin/bash
# health_check.sh
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/status)
if [ "$STATUS" -eq 200 ]; then
echo "LogWisp is healthy"
exit 0
else
echo "LogWisp is unhealthy (status: $STATUS)"
exit 1
fi
```
### Advanced Health Check
Check specific conditions:
```bash
#!/bin/bash
# advanced_health_check.sh
RESPONSE=$(curl -s http://localhost:8080/status)
# Check if processing logs
ENTRIES=$(echo "$RESPONSE" | jq -r '.monitor.total_entries')
if [ "$ENTRIES" -eq 0 ]; then
echo "WARNING: No log entries processed"
exit 1
fi
# Check dropped entries
DROPPED=$(echo "$RESPONSE" | jq -r '.monitor.dropped_entries')
TOTAL=$(echo "$RESPONSE" | jq -r '.monitor.total_entries')
DROP_PERCENT=$(( DROPPED * 100 / TOTAL ))
if [ "$DROP_PERCENT" -gt 5 ]; then
echo "WARNING: High drop rate: ${DROP_PERCENT}%"
exit 1
fi
# Check connections
CONNECTIONS=$(echo "$RESPONSE" | jq -r '.server.active_clients')
echo "OK: Processing logs, $CONNECTIONS active clients"
exit 0
```
### Container Health Check
Docker/Kubernetes configuration:
```dockerfile
# Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD curl -f http://localhost:8080/status || exit 1
```
```yaml
# Kubernetes
livenessProbe:
httpGet:
path: /status
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /status
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
```
## Monitoring Integration
### Prometheus Metrics
Export metrics in Prometheus format:
```bash
#!/bin/bash
# prometheus_exporter.sh
while true; do
STATUS=$(curl -s http://localhost:8080/status)
# Extract metrics
CLIENTS=$(echo "$STATUS" | jq -r '.server.active_clients')
ENTRIES=$(echo "$STATUS" | jq -r '.monitor.total_entries')
DROPPED=$(echo "$STATUS" | jq -r '.monitor.dropped_entries')
# Output Prometheus format
cat << EOF
# HELP logwisp_active_clients Number of active streaming clients
# TYPE logwisp_active_clients gauge
logwisp_active_clients $CLIENTS
# HELP logwisp_total_entries Total log entries processed
# TYPE logwisp_total_entries counter
logwisp_total_entries $ENTRIES
# HELP logwisp_dropped_entries Total log entries dropped
# TYPE logwisp_dropped_entries counter
logwisp_dropped_entries $DROPPED
EOF
sleep 60
done
```
### Grafana Dashboard
Key panels for Grafana:
1. **Active Connections**
- Query: `logwisp_active_clients`
- Visualization: Graph
- Alert: > 80% of max
2. **Log Processing Rate**
- Query: `rate(logwisp_total_entries[5m])`
- Visualization: Graph
- Alert: < 1 entry/min
3. **Drop Rate**
- Query: `rate(logwisp_dropped_entries[5m]) / rate(logwisp_total_entries[5m])`
- Visualization: Gauge
- Alert: > 5%
4. **Rate Limit Rejections**
- Query: `rate(logwisp_blocked_requests[5m])`
- Visualization: Graph
- Alert: > 10/min
### Datadog Integration
Send custom metrics:
```bash
#!/bin/bash
# datadog_metrics.sh
while true; do
STATUS=$(curl -s http://localhost:8080/status)
# Send metrics to Datadog
echo "$STATUS" | jq -r '
"logwisp.connections:\(.server.active_clients)|g",
"logwisp.entries:\(.monitor.total_entries)|c",
"logwisp.dropped:\(.monitor.dropped_entries)|c"
' | while read metric; do
echo "$metric" | nc -u -w1 localhost 8125
done
sleep 60
done
```
## Performance Monitoring
### CPU Usage
Monitor CPU usage by component:
```bash
# Check process CPU
top -p $(pgrep logwisp) -b -n 1
# Profile CPU usage
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
```
Common CPU consumers:
- File watching (reduce check_interval_ms)
- Regex filtering (simplify patterns)
- JSON encoding (reduce clients)
### Memory Usage
Track memory consumption:
```bash
# Check process memory
ps aux | grep logwisp
# Detailed memory stats
cat /proc/$(pgrep logwisp)/status | grep -E "Vm(RSS|Size)"
```
Memory optimization:
- Reduce buffer sizes
- Limit connections
- Simplify filters
### Network Bandwidth
Monitor streaming bandwidth:
```bash
# Network statistics
netstat -i
iftop -i eth0 -f "port 8080"
# Connection count
ss -tan | grep :8080 | wc -l
```
## Alerting
### Basic Alerts
Essential alerts to configure:
| Alert | Condition | Severity |
|-------|-----------|----------|
| Service Down | Status endpoint fails | Critical |
| High Drop Rate | > 10% entries dropped | Warning |
| No Log Activity | 0 entries/min for 5 min | Warning |
| Connection Limit | > 90% of max connections | Warning |
| Rate Limit High | > 20% requests blocked | Warning |
### Alert Script
Example monitoring script:
```bash
#!/bin/bash
# monitor_alerts.sh
check_alert() {
local name=$1
local condition=$2
local message=$3
if eval "$condition"; then
echo "ALERT: $name - $message"
# Send to alerting system
# curl -X POST https://alerts.example.com/...
fi
}
while true; do
STATUS=$(curl -s http://localhost:8080/status)
if [ -z "$STATUS" ]; then
check_alert "SERVICE_DOWN" "true" "LogWisp not responding"
sleep 60
continue
fi
# Extract metrics
DROPPED=$(echo "$STATUS" | jq -r '.monitor.dropped_entries')
TOTAL=$(echo "$STATUS" | jq -r '.monitor.total_entries')
CLIENTS=$(echo "$STATUS" | jq -r '.server.active_clients')
# Check conditions
check_alert "HIGH_DROP_RATE" \
"[ $((DROPPED * 100 / TOTAL)) -gt 10 ]" \
"Drop rate above 10%"
check_alert "HIGH_CONNECTIONS" \
"[ $CLIENTS -gt 90 ]" \
"Near connection limit: $CLIENTS/100"
sleep 60
done
```
## Troubleshooting with Monitoring
### No Logs Appearing
Check monitor stats:
```bash
curl -s http://localhost:8080/status | jq '.monitor'
```
Look for:
- `active_watchers` = 0 (no files found)
- `total_entries` not increasing (files not updating)
### High CPU Usage
Enable debug logging:
```bash
logwisp --log-level debug --log-output stderr
```
Watch for:
- Frequent "checkFile" messages (reduce check_interval)
- Many filter operations (optimize patterns)
### Memory Growth
Monitor over time:
```bash
while true; do
ps aux | grep logwisp | grep -v grep
curl -s http://localhost:8080/status | jq '.server.active_clients'
sleep 10
done
```
### Connection Issues
Check connection stats:
```bash
# Current connections
curl -s http://localhost:8080/status | jq '.server'
# Rate limit stats
curl -s http://localhost:8080/status | jq '.features.rate_limit'
```
## Best Practices
1. **Regular Monitoring**: Check status endpoints every 30-60 seconds
2. **Set Alerts**: Configure alerts for critical conditions
3. **Log Rotation**: Rotate LogWisp's own logs to prevent disk fill
4. **Baseline Metrics**: Establish normal ranges for your environment
5. **Capacity Planning**: Monitor trends for scaling decisions
6. **Test Monitoring**: Verify alerts work before issues occur
## See Also
- [Performance Tuning](performance.md) - Optimization guide
- [Troubleshooting](troubleshooting.md) - Common issues
- [Configuration Guide](configuration.md) - Monitoring configuration
- [Integration Examples](integrations.md) - Monitoring system integration

209
doc/quickstart.md Normal file
View File

@ -0,0 +1,209 @@
# Quick Start Guide
Get LogWisp up and running in 5 minutes!
## Installation
### From Source
```bash
# Clone the repository
git clone https://github.com/yourusername/logwisp.git
cd logwisp
# Build and install
make install
# Or just build
make build
./logwisp --version
```
### Using Go Install
```bash
go install github.com/yourusername/logwisp/src/cmd/logwisp@latest
```
## Basic Usage
### 1. Monitor Current Directory
Start LogWisp with defaults (monitors `*.log` files in current directory):
```bash
logwisp
```
### 2. Stream Logs
In another terminal, connect to the log stream:
```bash
# Using curl (SSE stream)
curl -N http://localhost:8080/stream
# Check status
curl http://localhost:8080/status | jq .
```
### 3. Create Some Logs
Generate test logs to see streaming in action:
```bash
# In a third terminal
echo "[ERROR] Something went wrong!" >> test.log
echo "[INFO] Application started" >> test.log
echo "[WARN] Low memory warning" >> test.log
```
## Common Scenarios
### Monitor Specific Directory
Create a configuration file `~/.config/logwisp.toml`:
```toml
[[streams]]
name = "myapp"
[streams.monitor]
targets = [
{ path = "/var/log/myapp", pattern = "*.log", is_file = false }
]
[streams.httpserver]
enabled = true
port = 8080
```
Run LogWisp:
```bash
logwisp
```
### Filter Only Errors and Warnings
Add filters to your configuration:
```toml
[[streams]]
name = "errors"
[streams.monitor]
targets = [
{ path = "./", pattern = "*.log" }
]
[[streams.filters]]
type = "include"
patterns = ["ERROR", "WARN", "CRITICAL", "FATAL"]
[streams.httpserver]
enabled = true
port = 8080
```
### Multiple Log Sources
Monitor different applications on different ports:
```toml
# Stream 1: Web application
[[streams]]
name = "webapp"
[streams.monitor]
targets = [{ path = "/var/log/nginx", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080
# Stream 2: Database
[[streams]]
name = "database"
[streams.monitor]
targets = [{ path = "/var/log/postgresql", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8081
```
### TCP Streaming
For high-performance streaming, use TCP:
```toml
[[streams]]
name = "highperf"
[streams.monitor]
targets = [{ path = "/var/log/app", pattern = "*.log" }]
[streams.tcpserver]
enabled = true
port = 9090
buffer_size = 5000
```
Connect with netcat:
```bash
nc localhost 9090
```
### Router Mode
Consolidate multiple streams on one port using router mode:
```bash
# With the multi-stream config above
logwisp --router
# Access streams at:
# http://localhost:8080/webapp/stream
# http://localhost:8080/database/stream
# http://localhost:8080/status (global status)
```
## Quick Tips
### Enable Debug Logging
```bash
logwisp --log-level debug --log-output stderr
```
### Run in Background
```bash
logwisp --background --config /etc/logwisp/prod.toml
```
### Rate Limiting
Protect your streams from abuse:
```toml
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 10.0
burst_size = 20
max_connections_per_ip = 5
```
### JSON Output Format
For structured logging:
```toml
[logging.console]
format = "json"
```
## What's Next?
- Read the [Configuration Guide](configuration.md) for all options
- Learn about [Filters](filters.md) for advanced pattern matching
- Explore [Rate Limiting](ratelimiting.md) for production deployments
- Check out [Example Configurations](examples/) for more scenarios
## Getting Help
- Run `logwisp --help` for CLI options
- Check `http://localhost:8080/status` for runtime statistics
- Enable debug logging for troubleshooting
- Visit our [GitHub repository](https://github.com/yourusername/logwisp) for issues and discussions

526
doc/ratelimiting.md Normal file
View File

@ -0,0 +1,526 @@
# Rate Limiting Guide
LogWisp provides configurable rate limiting to protect against abuse, prevent resource exhaustion, and ensure fair access to log streams.
## How Rate Limiting Works
LogWisp uses a **token bucket algorithm** for smooth, burst-tolerant rate limiting:
1. Each client (or globally) gets a bucket with a fixed capacity
2. Tokens are added to the bucket at a configured rate
3. Each request consumes one token
4. If no tokens are available, the request is rejected
5. The bucket can accumulate tokens up to its capacity for bursts
## Configuration
### Basic Configuration
```toml
[streams.httpserver.rate_limit]
enabled = true # Enable rate limiting
requests_per_second = 10.0 # Token refill rate
burst_size = 20 # Maximum tokens (bucket capacity)
limit_by = "ip" # "ip" or "global"
```
### Complete Options
```toml
[streams.httpserver.rate_limit]
# Core settings
enabled = true # Enable/disable rate limiting
requests_per_second = 10.0 # Token generation rate (float)
burst_size = 20 # Token bucket capacity
# Limiting strategy
limit_by = "ip" # "ip" or "global"
# Connection limits
max_connections_per_ip = 5 # Max concurrent connections per IP
max_total_connections = 100 # Max total concurrent connections
# Response configuration
response_code = 429 # HTTP status code when limited
response_message = "Rate limit exceeded" # Error message
# Same options available for TCP
[streams.tcpserver.rate_limit]
enabled = true
requests_per_second = 5.0
burst_size = 10
limit_by = "ip"
```
## Limiting Strategies
### Per-IP Limiting (Default)
Each client IP address gets its own token bucket:
```toml
[streams.httpserver.rate_limit]
enabled = true
limit_by = "ip"
requests_per_second = 10.0
burst_size = 20
```
**Use cases:**
- Fair access for multiple users
- Prevent single client from monopolizing resources
- Public-facing endpoints
**Example behavior:**
- Client A: Can make 10 req/sec
- Client B: Also can make 10 req/sec
- Total: Up to 10 × number of clients
### Global Limiting
All clients share a single token bucket:
```toml
[streams.httpserver.rate_limit]
enabled = true
limit_by = "global"
requests_per_second = 50.0
burst_size = 100
```
**Use cases:**
- Protect backend resources
- Control total system load
- Internal services with known clients
**Example behavior:**
- All clients combined: 50 req/sec max
- One aggressive client can consume all tokens
## Connection Limits
In addition to request rate limiting, you can limit concurrent connections:
### Per-IP Connection Limit
```toml
[streams.httpserver.rate_limit]
max_connections_per_ip = 5 # Each IP can have max 5 connections
```
**Behavior:**
- Prevents connection exhaustion attacks
- Limits resource usage per client
- Checked before rate limits
### Total Connection Limit
```toml
[streams.httpserver.rate_limit]
max_total_connections = 100 # Max 100 connections total
```
**Behavior:**
- Protects server resources
- Prevents memory exhaustion
- Global limit across all IPs
## Response Behavior
### HTTP Responses
When rate limited, HTTP clients receive:
```json
{
"error": "Rate limit exceeded",
"retry_after": "60"
}
```
With these headers:
- Status code: 429 (default) or configured value
- Content-Type: application/json
Configure custom responses:
```toml
[streams.httpserver.rate_limit]
response_code = 503 # Service Unavailable
response_message = "Server overloaded, please retry later"
```
### TCP Behavior
TCP connections are **silently dropped** when rate limited:
- No error message sent
- Connection immediately closed
- Prevents information leakage
## Configuration Examples
### Light Protection
For internal or trusted environments:
```toml
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 50.0
burst_size = 100
limit_by = "ip"
```
### Moderate Protection
For semi-public endpoints:
```toml
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 10.0
burst_size = 30
limit_by = "ip"
max_connections_per_ip = 5
max_total_connections = 200
```
### Strict Protection
For public or sensitive endpoints:
```toml
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 2.0
burst_size = 5
limit_by = "ip"
max_connections_per_ip = 2
max_total_connections = 50
response_code = 503
response_message = "Service temporarily unavailable"
```
### Debug/Development
Disable for testing:
```toml
[streams.httpserver.rate_limit]
enabled = false
```
## Use Case Scenarios
### Public Log Viewer
Prevent abuse while allowing legitimate use:
```toml
[[streams]]
name = "public-logs"
[streams.httpserver]
enabled = true
port = 8080
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 5.0 # 5 new connections per second
burst_size = 10 # Allow short bursts
limit_by = "ip"
max_connections_per_ip = 3 # Max 3 streams per user
max_total_connections = 100
```
### Internal Monitoring
Protect against accidental overload:
```toml
[[streams]]
name = "internal-metrics"
[streams.httpserver]
enabled = true
port = 8081
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 100.0 # High limit for internal use
burst_size = 200
limit_by = "global" # Total system limit
max_total_connections = 500
```
### High-Security Audit Logs
Very restrictive access:
```toml
[[streams]]
name = "audit"
[streams.httpserver]
enabled = true
port = 8443
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 0.5 # 1 request every 2 seconds
burst_size = 2
limit_by = "ip"
max_connections_per_ip = 1 # Single connection only
max_total_connections = 10
response_code = 403 # Forbidden (hide rate limit)
response_message = "Access denied"
```
### Multi-Tenant Service
Different limits per stream:
```toml
# Free tier
[[streams]]
name = "logs-free"
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 1.0
burst_size = 5
max_connections_per_ip = 1
# Premium tier
[[streams]]
name = "logs-premium"
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 50.0
burst_size = 100
max_connections_per_ip = 10
```
## Monitoring Rate Limits
### Status Endpoint
Check rate limit statistics:
```bash
curl http://localhost:8080/status | jq '.server.features.rate_limit'
```
Response includes:
```json
{
"enabled": true,
"total_requests": 15234,
"blocked_requests": 89,
"active_ips": 12,
"total_connections": 8,
"config": {
"requests_per_second": 10,
"burst_size": 20,
"limit_by": "ip"
}
}
```
### Debug Logging
Enable debug logs to see rate limit decisions:
```bash
logwisp --log-level debug
```
Look for messages:
```
Request rate limited ip=192.168.1.100
Connection limit exceeded ip=192.168.1.100 connections=5 limit=5
Created new IP limiter ip=192.168.1.100 total_ips=3
```
## Testing Rate Limits
### Test Script
```bash
#!/bin/bash
# Test rate limiting behavior
URL="http://localhost:8080/stream"
PARALLEL=10
DURATION=10
echo "Testing rate limits..."
echo "URL: $URL"
echo "Parallel connections: $PARALLEL"
echo "Duration: ${DURATION}s"
echo
# Function to connect and count lines
test_connection() {
local id=$1
local count=0
local start=$(date +%s)
while (( $(date +%s) - start < DURATION )); do
if curl -s -N --max-time 1 "$URL" >/dev/null 2>&1; then
((count++))
echo "[$id] Connected successfully (total: $count)"
else
echo "[$id] Rate limited!"
fi
sleep 0.1
done
}
# Run parallel connections
for i in $(seq 1 $PARALLEL); do
test_connection $i &
done
wait
echo "Test complete"
```
### Load Testing
Using Apache Bench (ab):
```bash
# Test burst handling
ab -n 100 -c 20 http://localhost:8080/status
# Test sustained load
ab -n 1000 -c 5 -r http://localhost:8080/status
```
Using curl:
```bash
# Test connection limit
for i in {1..10}; do
curl -N http://localhost:8080/stream &
done
```
## Tuning Guidelines
### Setting requests_per_second
Consider:
- Expected legitimate traffic
- Server capacity
- Client retry behavior
**Formula**: `requests_per_second = expected_clients × requests_per_client`
### Setting burst_size
General rule: `burst_size = 2-3 × requests_per_second`
Examples:
- `10 req/s → burst_size = 20-30`
- `1 req/s → burst_size = 3-5`
- `100 req/s → burst_size = 200-300`
### Connection Limits
Based on available memory:
- Each HTTP connection: ~1-2MB
- Each TCP connection: ~0.5-1MB
**Formula**: `max_connections = available_memory / memory_per_connection`
## Common Issues
### "All requests blocked"
Check if:
- Rate limits too strict
- Burst size too small
- Using global limiting with many clients
### "Memory growth"
Possible causes:
- No connection limits set
- Slow clients holding connections
- Too high burst_size
Solutions:
```toml
max_connections_per_ip = 5
max_total_connections = 100
```
### "Legitimate users blocked"
Consider:
- Increasing burst_size for short spikes
- Using per-IP instead of global limiting
- Different streams for different user tiers
## Security Considerations
### Information Disclosure
Rate limit responses can reveal information:
```toml
# Default - informative
response_code = 429
response_message = "Rate limit exceeded"
# Security-focused - generic
response_code = 503
response_message = "Service unavailable"
# High security - misleading
response_code = 403
response_message = "Forbidden"
```
### DDoS Protection
Rate limiting helps but isn't complete DDoS protection:
- Use with firewall rules
- Consider CDN/proxy rate limiting
- Monitor for distributed attacks
### Resource Exhaustion
Protect against:
- Connection exhaustion
- Memory exhaustion
- CPU exhaustion
```toml
# Comprehensive protection
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 10.0
burst_size = 20
max_connections_per_ip = 5
max_total_connections = 100
limit_by = "ip"
```
## Best Practices
1. **Start Conservative**: Begin with strict limits and relax as needed
2. **Monitor Statistics**: Use `/status` endpoint to track behavior
3. **Test Thoroughly**: Verify limits work as expected under load
4. **Document Limits**: Make rate limits clear to users
5. **Provide Retry Info**: Help clients implement proper retry logic
6. **Different Tiers**: Consider different limits for different user types
7. **Regular Review**: Adjust limits based on usage patterns
## See Also
- [Configuration Guide](configuration.md) - Complete configuration reference
- [Security Best Practices](security.md) - Security hardening
- [Performance Tuning](performance.md) - Optimization guidelines
- [Troubleshooting](troubleshooting.md) - Common issues

520
doc/router.md Normal file
View File

@ -0,0 +1,520 @@
# Router Mode Guide
Router mode allows multiple LogWisp streams to share HTTP ports through path-based routing, simplifying deployment and access control.
## Overview
In standard mode, each stream requires its own port:
- Stream 1: `http://localhost:8080/stream`
- Stream 2: `http://localhost:8081/stream`
- Stream 3: `http://localhost:8082/stream`
In router mode, streams share ports via paths:
- Stream 1: `http://localhost:8080/app/stream`
- Stream 2: `http://localhost:8080/database/stream`
- Stream 3: `http://localhost:8080/system/stream`
- Global status: `http://localhost:8080/status`
## Enabling Router Mode
Start LogWisp with the `--router` flag:
```bash
logwisp --router --config /etc/logwisp/multi-stream.toml
```
## Configuration
### Basic Router Configuration
```toml
# All streams can use the same port in router mode
[[streams]]
name = "app"
[streams.monitor]
targets = [{ path = "/var/log/app", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080 # Same port OK
[[streams]]
name = "database"
[streams.monitor]
targets = [{ path = "/var/log/postgresql", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080 # Shared port
[[streams]]
name = "nginx"
[streams.monitor]
targets = [{ path = "/var/log/nginx", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080 # Shared port
```
### Path Structure
In router mode, paths are automatically prefixed with the stream name:
| Stream Name | Configuration Path | Router Mode Path |
|------------|-------------------|------------------|
| `app` | `/stream` | `/app/stream` |
| `app` | `/status` | `/app/status` |
| `database` | `/stream` | `/database/stream` |
| `database` | `/status` | `/database/status` |
### Custom Paths
You can customize the paths in each stream:
```toml
[[streams]]
name = "api"
[streams.httpserver]
stream_path = "/logs" # Becomes /api/logs
status_path = "/health" # Becomes /api/health
```
## URL Endpoints
### Stream Endpoints
Access individual streams:
```bash
# SSE stream for 'app' logs
curl -N http://localhost:8080/app/stream
# Status for 'database' stream
curl http://localhost:8080/database/status
# Custom path example
curl -N http://localhost:8080/api/logs
```
### Global Status
Router mode provides a global status endpoint:
```bash
curl http://localhost:8080/status | jq .
```
Returns aggregated information:
```json
{
"service": "LogWisp Router",
"version": "1.0.0",
"port": 8080,
"total_streams": 3,
"streams": {
"app": { /* stream stats */ },
"database": { /* stream stats */ },
"nginx": { /* stream stats */ }
},
"router": {
"uptime_seconds": 3600,
"total_requests": 15234,
"routed_requests": 15220,
"failed_requests": 14
}
}
```
## Port Sharing
### How It Works
1. Router server listens on configured ports
2. Examines request path to determine target stream
3. Routes request to appropriate stream handler
4. Stream handles request as if standalone
### Port Assignment Rules
In router mode:
- Multiple streams can use the same port
- Router detects and consolidates shared ports
- Each unique port gets one router server
- TCP servers remain independent (no routing)
Example with multiple ports:
```toml
# Streams 1-3 share port 8080
[[streams]]
name = "app"
[streams.httpserver]
port = 8080
[[streams]]
name = "db"
[streams.httpserver]
port = 8080
[[streams]]
name = "web"
[streams.httpserver]
port = 8080
# Stream 4 uses different port
[[streams]]
name = "admin"
[streams.httpserver]
port = 9090
# Result: 2 router servers (8080 and 9090)
```
## Use Cases
### Microservices Architecture
Route logs from different services:
```toml
[[streams]]
name = "frontend"
[streams.monitor]
targets = [{ path = "/var/log/frontend", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080
[[streams]]
name = "backend"
[streams.monitor]
targets = [{ path = "/var/log/backend", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080
[[streams]]
name = "worker"
[streams.monitor]
targets = [{ path = "/var/log/worker", pattern = "*.log" }]
[streams.httpserver]
enabled = true
port = 8080
```
Access via:
- Frontend logs: `http://localhost:8080/frontend/stream`
- Backend logs: `http://localhost:8080/backend/stream`
- Worker logs: `http://localhost:8080/worker/stream`
### Environment-Based Routing
Different log levels per environment:
```toml
[[streams]]
name = "prod"
[streams.monitor]
targets = [{ path = "/logs/prod", pattern = "*.log" }]
[[streams.filters]]
type = "include"
patterns = ["ERROR", "WARN"]
[streams.httpserver]
port = 8080
[[streams]]
name = "staging"
[streams.monitor]
targets = [{ path = "/logs/staging", pattern = "*.log" }]
[[streams.filters]]
type = "include"
patterns = ["ERROR", "WARN", "INFO"]
[streams.httpserver]
port = 8080
[[streams]]
name = "dev"
[streams.monitor]
targets = [{ path = "/logs/dev", pattern = "*.log" }]
# No filters - all logs
[streams.httpserver]
port = 8080
```
### Department Access
Separate streams for different teams:
```toml
[[streams]]
name = "engineering"
[streams.monitor]
targets = [{ path = "/logs/apps", pattern = "*.log" }]
[streams.httpserver]
port = 8080
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 50.0
[[streams]]
name = "security"
[streams.monitor]
targets = [{ path = "/logs/audit", pattern = "*.log" }]
[streams.httpserver]
port = 8080
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 5.0
max_connections_per_ip = 1
[[streams]]
name = "support"
[streams.monitor]
targets = [{ path = "/logs/customer", pattern = "*.log" }]
[[streams.filters]]
type = "exclude"
patterns = ["password", "token", "secret"]
[streams.httpserver]
port = 8080
```
## Advanced Features
### Mixed Mode Deployment
Combine router and standalone modes:
```toml
# Public streams via router
[[streams]]
name = "public-api"
[streams.httpserver]
enabled = true
port = 8080 # Router mode
[[streams]]
name = "public-web"
[streams.httpserver]
enabled = true
port = 8080 # Router mode
# Internal stream standalone
[[streams]]
name = "internal"
[streams.httpserver]
enabled = true
port = 9999 # Different port, standalone
# High-performance TCP
[[streams]]
name = "metrics"
[streams.tcpserver]
enabled = true
port = 9090 # TCP not affected by router
```
### Load Balancer Integration
Router mode works well with load balancers:
```nginx
# Nginx configuration
upstream logwisp {
server logwisp1:8080;
server logwisp2:8080;
server logwisp3:8080;
}
location /logs/ {
proxy_pass http://logwisp/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
}
```
Access becomes:
- `https://example.com/logs/app/stream`
- `https://example.com/logs/database/stream`
- `https://example.com/logs/status`
### Path-Based Access Control
Use reverse proxy for authentication:
```nginx
# Require auth for security logs
location /logs/security/ {
auth_basic "Security Logs";
auth_basic_user_file /etc/nginx/security.htpasswd;
proxy_pass http://localhost:8080/security/;
}
# Public access for status
location /logs/app/ {
proxy_pass http://localhost:8080/app/;
}
```
## Limitations
### Router Mode Limitations
1. **HTTP Only**: Router mode only works for HTTP/SSE streams
2. **No TCP Routing**: TCP streams remain on separate ports
3. **Path Conflicts**: Stream names must be unique
4. **Same Config**: All streams on a port share SSL/auth settings
### When Not to Use Router Mode
- High-performance scenarios (use TCP)
- Streams need different SSL certificates
- Complex authentication per stream
- Network isolation requirements
## Troubleshooting
### "Path not found"
Check available routes:
```bash
curl http://localhost:8080/invalid-path
```
Response shows available routes:
```json
{
"error": "Not Found",
"requested_path": "/invalid-path",
"available_routes": [
"/status (global status)",
"/app/stream (stream: app)",
"/app/status (status: app)",
"/database/stream (stream: database)",
"/database/status (status: database)"
]
}
```
### "Port conflict"
If you see port conflicts:
1. Ensure `--router` flag is used
2. Check all streams have `httpserver.enabled = true`
3. Verify no other services use the port
### Debug Routing
Enable debug logging:
```bash
logwisp --router --log-level debug
```
Look for routing decisions:
```
Router request method=GET path=/app/stream remote_addr=127.0.0.1:54321
Routing request to stream stream=app original_path=/app/stream remaining_path=/stream
```
### Performance Impact
Router mode adds minimal overhead:
- ~100-200ns per request for path matching
- Negligible memory overhead
- No impact on streaming performance
## Best Practices
### Naming Conventions
Use clear, consistent stream names:
```toml
# Good: Clear purpose
name = "frontend-prod"
name = "backend-staging"
name = "worker-payments"
# Bad: Ambiguous
name = "logs1"
name = "stream2"
name = "test"
```
### Path Organization
Group related streams:
```
/prod/frontend/stream
/prod/backend/stream
/staging/frontend/stream
/staging/backend/stream
```
### Documentation
Document your routing structure:
```toml
# Stream for production API logs
# Access: https://logs.example.com/api-prod/stream
[[streams]]
name = "api-prod"
```
### Monitoring
Use global status for overview:
```bash
# Monitor all streams
watch -n 5 'curl -s localhost:8080/status | jq .streams'
# Check specific stream
curl -s localhost:8080/status | jq '.streams.app'
```
## Migration Guide
### From Standalone to Router
1. **Update configuration** - ensure consistent ports:
```toml
# Change from different ports
[streams.httpserver]
port = 8080 # Was 8081, 8082, etc.
```
2. **Start with router flag**:
```bash
logwisp --router --config existing.toml
```
3. **Update client URLs**:
```bash
# Old: http://localhost:8081/stream
# New: http://localhost:8080/streamname/stream
```
4. **Update monitoring**:
```bash
# Global status now available
curl http://localhost:8080/status
```
### Gradual Migration
Run both modes during transition:
```bash
# Week 1: Run standalone (current)
logwisp --config prod.toml
# Week 2: Run both
logwisp --config prod.toml & # Standalone
logwisp --router --config prod-router.toml & # Router
# Week 3: Router only
logwisp --router --config prod.toml
```
## See Also
- [Configuration Guide](configuration.md) - Stream configuration
- [HTTP Streaming](api.md#http-sse) - SSE protocol details
- [Load Balancing](integrations.md#load-balancers) - Integration patterns
- [Security Best Practices](security.md) - Securing router deployments

537
doc/troubleshooting.md Normal file
View File

@ -0,0 +1,537 @@
# Troubleshooting Guide
This guide helps diagnose and resolve common issues with LogWisp.
## Diagnostic Tools
### Enable Debug Logging
The first step in troubleshooting is enabling debug logs:
```bash
# Via command line
logwisp --log-level debug --log-output stderr
# Via environment
export LOGWISP_LOGGING_LEVEL=debug
logwisp
# Via config
[logging]
level = "debug"
output = "stderr"
```
### Check Status Endpoint
Verify LogWisp is running and processing:
```bash
# Basic check
curl http://localhost:8080/status
# Pretty print
curl -s http://localhost:8080/status | jq .
# Check specific metrics
curl -s http://localhost:8080/status | jq '.monitor'
```
### Test Log Streaming
Verify streams are working:
```bash
# Test SSE stream (should show heartbeats if enabled)
curl -N http://localhost:8080/stream
# Test with timeout
timeout 5 curl -N http://localhost:8080/stream
# Test TCP stream
nc localhost 9090
```
## Common Issues
### No Logs Appearing
**Symptoms:**
- Stream connects but no log entries appear
- Status shows `total_entries: 0`
**Diagnosis:**
1. Check monitor configuration:
```bash
curl -s http://localhost:8080/status | jq '.monitor'
```
2. Verify file paths exist:
```bash
# Check your configured paths
ls -la /var/log/myapp/
```
3. Check file permissions:
```bash
# LogWisp user must have read access
sudo -u logwisp ls /var/log/myapp/
```
4. Verify files match pattern:
```bash
# If pattern is "*.log"
ls /var/log/myapp/*.log
```
5. Check if files are being updated:
```bash
# Should show recent timestamps
ls -la /var/log/myapp/*.log
tail -f /var/log/myapp/app.log
```
**Solutions:**
- Fix file permissions:
```bash
sudo chmod 644 /var/log/myapp/*.log
sudo usermod -a -G adm logwisp # Add to log group
```
- Correct path configuration:
```toml
targets = [
{ path = "/correct/path/to/logs", pattern = "*.log" }
]
```
- Use absolute paths:
```toml
# Bad: Relative path
targets = [{ path = "./logs", pattern = "*.log" }]
# Good: Absolute path
targets = [{ path = "/var/log/app", pattern = "*.log" }]
```
### High CPU Usage
**Symptoms:**
- LogWisp process using excessive CPU
- System slowdown
**Diagnosis:**
1. Check process CPU:
```bash
top -p $(pgrep logwisp)
```
2. Review check intervals:
```bash
grep check_interval /etc/logwisp/logwisp.toml
```
3. Count active watchers:
```bash
curl -s http://localhost:8080/status | jq '.monitor.active_watchers'
```
4. Check filter complexity:
```bash
curl -s http://localhost:8080/status | jq '.filters'
```
**Solutions:**
- Increase check interval:
```toml
[streams.monitor]
check_interval_ms = 1000 # Was 50ms
```
- Reduce watched files:
```toml
# Instead of watching entire directory
targets = [
{ path = "/var/log/specific-app.log", is_file = true }
]
```
- Simplify filter patterns:
```toml
# Complex regex (slow)
patterns = ["^\\[\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\]\\s+\\[(ERROR|WARN)\\]"]
# Simple patterns (fast)
patterns = ["ERROR", "WARN"]
```
### Memory Growth
**Symptoms:**
- Increasing memory usage over time
- Eventually runs out of memory
**Diagnosis:**
1. Monitor memory usage:
```bash
watch -n 10 'ps aux | grep logwisp'
```
2. Check connection count:
```bash
curl -s http://localhost:8080/status | jq '.server.active_clients'
```
3. Check for dropped entries:
```bash
curl -s http://localhost:8080/status | jq '.monitor.dropped_entries'
```
**Solutions:**
- Limit connections:
```toml
[streams.httpserver.rate_limit]
enabled = true
max_connections_per_ip = 5
max_total_connections = 100
```
- Reduce buffer sizes:
```toml
[streams.httpserver]
buffer_size = 500 # Was 5000
```
- Enable rate limiting:
```toml
[streams.httpserver.rate_limit]
enabled = true
requests_per_second = 10.0
```
### Connection Refused
**Symptoms:**
- Cannot connect to LogWisp
- `curl: (7) Failed to connect`
**Diagnosis:**
1. Check if LogWisp is running:
```bash
ps aux | grep logwisp
systemctl status logwisp
```
2. Verify listening ports:
```bash
sudo netstat -tlnp | grep logwisp
# or
sudo ss -tlnp | grep logwisp
```
3. Check firewall:
```bash
sudo iptables -L -n | grep 8080
sudo ufw status
```
**Solutions:**
- Start the service:
```bash
sudo systemctl start logwisp
```
- Fix port configuration:
```toml
[streams.httpserver]
enabled = true # Must be true
port = 8080 # Correct port
```
- Open firewall:
```bash
sudo ufw allow 8080/tcp
```
### Rate Limit Errors
**Symptoms:**
- HTTP 429 responses
- "Rate limit exceeded" errors
**Diagnosis:**
1. Check rate limit stats:
```bash
curl -s http://localhost:8080/status | jq '.features.rate_limit'
```
2. Test rate limits:
```bash
# Rapid requests
for i in {1..20}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/status; done
```
**Solutions:**
- Increase rate limits:
```toml
[streams.httpserver.rate_limit]
requests_per_second = 50.0 # Was 10.0
burst_size = 100 # Was 20
```
- Use per-IP limiting:
```toml
limit_by = "ip" # Instead of "global"
```
- Disable for internal use:
```toml
enabled = false
```
### Filter Not Working
**Symptoms:**
- Unwanted logs still appearing
- Wanted logs being filtered out
**Diagnosis:**
1. Check filter configuration:
```bash
curl -s http://localhost:8080/status | jq '.filters'
```
2. Test patterns:
```bash
# Test regex pattern
echo "ERROR: test message" | grep -E "your-pattern"
```
3. Enable debug logging to see filter decisions:
```bash
logwisp --log-level debug 2>&1 | grep filter
```
**Solutions:**
- Fix pattern syntax:
```toml
# Word boundaries
patterns = ["\\bERROR\\b"] # Not "ERROR" which matches "TERROR"
# Case insensitive
patterns = ["(?i)error"]
```
- Check filter order:
```toml
# Include filters run first
[[streams.filters]]
type = "include"
patterns = ["ERROR", "WARN"]
# Then exclude filters
[[streams.filters]]
type = "exclude"
patterns = ["IGNORE_THIS"]
```
- Use correct logic:
```toml
logic = "or" # Match ANY pattern
# not
logic = "and" # Match ALL patterns
```
### Logs Dropping
**Symptoms:**
- `dropped_entries` counter increasing
- Missing log entries in stream
**Diagnosis:**
1. Check drop statistics:
```bash
curl -s http://localhost:8080/status | jq '{
dropped: .monitor.dropped_entries,
total: .monitor.total_entries,
percent: (.monitor.dropped_entries / .monitor.total_entries * 100)
}'
```
2. Monitor drop rate:
```bash
watch -n 5 'curl -s http://localhost:8080/status | jq .monitor.dropped_entries'
```
**Solutions:**
- Increase buffer sizes:
```toml
[streams.httpserver]
buffer_size = 5000 # Was 1000
```
- Add flow control:
```toml
[streams.monitor]
check_interval_ms = 500 # Slow down reading
```
- Reduce clients:
```toml
[streams.httpserver.rate_limit]
max_total_connections = 50
```
## Performance Issues
### Slow Response Times
**Diagnosis:**
```bash
# Measure response time
time curl -s http://localhost:8080/status > /dev/null
# Check system load
uptime
top
```
**Solutions:**
- Reduce concurrent operations
- Increase system resources
- Use TCP instead of HTTP for high volume
### Network Bandwidth
**Diagnosis:**
```bash
# Monitor network usage
iftop -i eth0 -f "port 8080"
# Check connection count
ss -tan | grep :8080 | wc -l
```
**Solutions:**
- Enable compression (future feature)
- Filter more aggressively
- Use TCP for local connections
## Debug Commands
### System Information
```bash
# LogWisp version
logwisp --version
# System resources
free -h
df -h
ulimit -a
# Network state
ss -tlnp
netstat -anp | grep logwisp
```
### Process Inspection
```bash
# Process details
ps aux | grep logwisp
# Open files
lsof -p $(pgrep logwisp)
# System calls (Linux)
strace -p $(pgrep logwisp) -e trace=open,read,write
# File system activity
inotifywait -m /var/log/myapp/
```
### Configuration Validation
```bash
# Test configuration
logwisp --config test.toml --log-level debug --log-output stderr
# Check file syntax
cat /etc/logwisp/logwisp.toml | grep -E "^\s*\["
# Validate TOML
python3 -m pip install toml
python3 -c "import toml; toml.load('/etc/logwisp/logwisp.toml'); print('Valid')"
```
## Getting Help
### Collect Diagnostic Information
Create a diagnostic bundle:
```bash
#!/bin/bash
# diagnostic.sh
DIAG_DIR="logwisp-diag-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$DIAG_DIR"
# Version
logwisp --version > "$DIAG_DIR/version.txt" 2>&1
# Configuration (sanitized)
grep -v "password\|secret\|token" /etc/logwisp/logwisp.toml > "$DIAG_DIR/config.toml"
# Status
curl -s http://localhost:8080/status > "$DIAG_DIR/status.json"
# System info
uname -a > "$DIAG_DIR/system.txt"
free -h >> "$DIAG_DIR/system.txt"
df -h >> "$DIAG_DIR/system.txt"
# Process info
ps aux | grep logwisp > "$DIAG_DIR/process.txt"
lsof -p $(pgrep logwisp) > "$DIAG_DIR/files.txt" 2>&1
# Recent logs
journalctl -u logwisp -n 1000 > "$DIAG_DIR/logs.txt" 2>&1
# Create archive
tar -czf "$DIAG_DIR.tar.gz" "$DIAG_DIR"
rm -rf "$DIAG_DIR"
echo "Diagnostic bundle created: $DIAG_DIR.tar.gz"
```
### Report Issues
When reporting issues, include:
1. LogWisp version
2. Configuration (sanitized)
3. Error messages
4. Steps to reproduce
5. Diagnostic bundle
## See Also
- [Monitoring Guide](monitoring.md) - Status and metrics
- [Performance Tuning](performance.md) - Optimization
- [Configuration Guide](configuration.md) - Settings reference
- [FAQ](faq.md) - Frequently asked questions

9
go.mod
View File

@ -1,11 +1,10 @@
module logwisp module logwisp
go 1.24.2 go 1.24.5
toolchain go1.24.4
require ( require (
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e
github.com/panjf2000/gnet/v2 v2.9.1 github.com/panjf2000/gnet/v2 v2.9.1
github.com/valyala/fasthttp v1.63.0 github.com/valyala/fasthttp v1.63.0
) )
@ -19,7 +18,7 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.34.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
) )

10
go.sum
View File

@ -8,6 +8,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 h1:qE4SpAJWFaLkdRyE0FjTPBBRYE7LOvcmRCB5p86W73Q= github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6 h1:qE4SpAJWFaLkdRyE0FjTPBBRYE7LOvcmRCB5p86W73Q=
github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8= github.com/lixenwraith/config v0.0.0-20250701170607-8515fa0543b6/go.mod h1:4wPJ3HnLrYrtUwTinngCsBgtdIXsnxkLa7q4KAIbwY8=
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e h1:WjYl/OIKxDCFA1In2W0bJbCGJ/Ub9X9DL+avZRNjXIQ=
github.com/lixenwraith/log v0.0.0-20250710012114-049926224b0e/go.mod h1:KFE7B7m2pu5kAl0olDCvywlOqFJhanogAhTlVvlp8JE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
@ -30,10 +32,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 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/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -0,0 +1,229 @@
// FILE: src/cmd/logwisp/bootstrap.go
package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"logwisp/src/internal/config"
"logwisp/src/internal/service"
"logwisp/src/internal/version"
"github.com/lixenwraith/log"
)
// bootstrapService creates and initializes the log transport service
func bootstrapService(ctx context.Context, cfg *config.Config) (*service.Service, *service.HTTPRouter, error) {
// Create log transport service
svc := service.New(ctx, logger)
// Create HTTP router if requested
var router *service.HTTPRouter
if *useRouter {
router = service.NewHTTPRouter(svc, logger)
logger.Info("msg", "HTTP router mode enabled")
}
// Initialize streams
successCount := 0
for _, streamCfg := range cfg.Streams {
logger.Info("msg", "Initializing transport", "transport", streamCfg.Name)
// Handle router mode configuration
if *useRouter && streamCfg.HTTPServer != nil && streamCfg.HTTPServer.Enabled {
if err := initializeRouterStream(svc, router, streamCfg); err != nil {
logger.Error("msg", "Failed to initialize router stream",
"transport", streamCfg.Name,
"error", err)
continue
}
} else {
// Standard standalone mode
if err := svc.CreateStream(streamCfg); err != nil {
logger.Error("msg", "Failed to create transport",
"transport", streamCfg.Name,
"error", err)
continue
}
}
successCount++
displayStreamEndpoints(streamCfg, *useRouter)
}
if successCount == 0 {
return nil, nil, fmt.Errorf("no streams successfully started (attempted %d)", len(cfg.Streams))
}
logger.Info("msg", "LogWisp started",
"version", version.Short(),
"transports", successCount)
return svc, router, nil
}
// initializeRouterStream sets up a stream for router mode
func initializeRouterStream(svc *service.Service, router *service.HTTPRouter, streamCfg config.StreamConfig) error {
// Temporarily disable standalone server startup
originalEnabled := streamCfg.HTTPServer.Enabled
streamCfg.HTTPServer.Enabled = false
if err := svc.CreateStream(streamCfg); err != nil {
return err
}
// Get the created transport and configure for router mode
stream, err := svc.GetStream(streamCfg.Name)
if err != nil {
return err
}
if stream.HTTPServer != nil {
stream.HTTPServer.SetRouterMode()
// Restore enabled state
stream.Config.HTTPServer.Enabled = originalEnabled
if err := router.RegisterStream(stream); err != nil {
return err
}
logger.Info("msg", "Stream registered with router", "stream", streamCfg.Name)
}
return nil
}
// initializeLogger sets up the logger based on configuration and CLI flags
func initializeLogger(cfg *config.Config) error {
logger = log.NewLogger()
var configArgs []string
// Determine output mode from CLI or config
outputMode := cfg.Logging.Output
if *logOutput != "" {
outputMode = *logOutput
}
// Determine log level
level := cfg.Logging.Level
if *logLevel != "" {
level = *logLevel
}
levelValue, err := parseLogLevel(level)
if err != nil {
return fmt.Errorf("invalid log level: %w", err)
}
configArgs = append(configArgs, fmt.Sprintf("level=%d", levelValue))
// Configure based on output mode
switch outputMode {
case "none":
// ⚠️ SECURITY: Disabling logs may hide security events
configArgs = append(configArgs, "disable_file=true", "enable_stdout=false")
case "stdout":
configArgs = append(configArgs,
"disable_file=true",
"enable_stdout=true",
"stdout_target=stdout")
case "stderr":
configArgs = append(configArgs,
"disable_file=true",
"enable_stdout=true",
"stdout_target=stderr")
case "file":
configArgs = append(configArgs, "enable_stdout=false")
configureFileLogging(&configArgs, cfg)
case "both":
configArgs = append(configArgs, "enable_stdout=true")
configureFileLogging(&configArgs, cfg)
configureConsoleTarget(&configArgs, cfg)
default:
return fmt.Errorf("invalid log output mode: %s", outputMode)
}
// Apply format if specified
if cfg.Logging.Console != nil && cfg.Logging.Console.Format != "" {
configArgs = append(configArgs, fmt.Sprintf("format=%s", cfg.Logging.Console.Format))
}
return logger.InitWithDefaults(configArgs...)
}
// configureFileLogging sets up file-based logging parameters
func configureFileLogging(configArgs *[]string, cfg *config.Config) {
// CLI overrides
if *logFile != "" {
dir := filepath.Dir(*logFile)
name := strings.TrimSuffix(filepath.Base(*logFile), filepath.Ext(*logFile))
*configArgs = append(*configArgs,
fmt.Sprintf("directory=%s", dir),
fmt.Sprintf("name=%s", name))
} else if *logDir != "" {
*configArgs = append(*configArgs,
fmt.Sprintf("directory=%s", *logDir),
fmt.Sprintf("name=%s", cfg.Logging.File.Name))
} else if cfg.Logging.File != nil {
// Use config file settings
*configArgs = append(*configArgs,
fmt.Sprintf("directory=%s", cfg.Logging.File.Directory),
fmt.Sprintf("name=%s", cfg.Logging.File.Name),
fmt.Sprintf("max_size_mb=%d", cfg.Logging.File.MaxSizeMB),
fmt.Sprintf("max_total_size_mb=%d", cfg.Logging.File.MaxTotalSizeMB))
if cfg.Logging.File.RetentionHours > 0 {
*configArgs = append(*configArgs,
fmt.Sprintf("retention_period_hrs=%.1f", cfg.Logging.File.RetentionHours))
}
}
}
// configureConsoleTarget sets up console output parameters
func configureConsoleTarget(configArgs *[]string, cfg *config.Config) {
target := "stderr" // default
if *logConsole != "" {
target = *logConsole
} else if cfg.Logging.Console != nil && cfg.Logging.Console.Target != "" {
target = cfg.Logging.Console.Target
}
// Handle "split" mode at application level since log package doesn't support it natively
if target == "split" {
// For now, default to stderr for all since log package doesn't support split
// TODO: Future enhancement - route ERROR/WARN to stderr, INFO/DEBUG to stdout
target = "stderr"
}
*configArgs = append(*configArgs, fmt.Sprintf("stdout_target=%s", target))
}
// isBackgroundProcess checks if we're already running in background
func isBackgroundProcess() bool {
return os.Getenv("LOGWISP_BACKGROUND") == "1"
}
// runInBackground starts the process in background
func runInBackground() error {
cmd := exec.Command(os.Args[0], os.Args[1:]...)
cmd.Env = append(os.Environ(), "LOGWISP_BACKGROUND=1")
cmd.Stdin = nil
cmd.Stdout = os.Stdout // Keep stdout for logging
cmd.Stderr = os.Stderr // Keep stderr for logging
if err := cmd.Start(); err != nil {
return err
}
fmt.Printf("Started LogWisp in background (PID: %d)\n", cmd.Process.Pid)
return nil
}

124
src/cmd/logwisp/flags.go Normal file
View File

@ -0,0 +1,124 @@
// FILE: src/cmd/logwisp/flags.go
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/lixenwraith/log"
)
// Command-line flags
var (
// General flags
configFile = flag.String("config", "", "Config file path")
useRouter = flag.Bool("router", false, "Use HTTP router for path-based routing")
showVersion = flag.Bool("version", false, "Show version information")
background = flag.Bool("background", false, "Run as background process")
// Logging flags
logOutput = flag.String("log-output", "", "Log output: file, stdout, stderr, both, none (overrides config)")
logLevel = flag.String("log-level", "", "Log level: debug, info, warn, error (overrides config)")
logFile = flag.String("log-file", "", "Log file path (when using file output)")
logDir = flag.String("log-dir", "", "Log directory (when using file output)")
logConsole = flag.String("log-console", "", "Console target: stdout, stderr, split (overrides config)")
)
func init() {
flag.Usage = customUsage
}
func customUsage() {
fmt.Fprintf(os.Stderr, "LogWisp - Multi-Stream Log Monitoring Service\n\n")
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Options:\n")
// General options
fmt.Fprintf(os.Stderr, "\nGeneral:\n")
fmt.Fprintf(os.Stderr, " -config string\n\tConfig file path\n")
fmt.Fprintf(os.Stderr, " -router\n\tUse HTTP router for path-based routing\n")
fmt.Fprintf(os.Stderr, " -version\n\tShow version information\n")
fmt.Fprintf(os.Stderr, " -background\n\tRun as background process\n")
// Logging options
fmt.Fprintf(os.Stderr, "\nLogging:\n")
fmt.Fprintf(os.Stderr, " -log-output string\n\tLog output: file, stdout, stderr, both, none (overrides config)\n")
fmt.Fprintf(os.Stderr, " -log-level string\n\tLog level: debug, info, warn, error (overrides config)\n")
fmt.Fprintf(os.Stderr, " -log-file string\n\tLog file path (when using file output)\n")
fmt.Fprintf(os.Stderr, " -log-dir string\n\tLog directory (when using file output)\n")
fmt.Fprintf(os.Stderr, " -log-console string\n\tConsole target: stdout, stderr, split (overrides config)\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " # Run with default config (logs to stderr)\n")
fmt.Fprintf(os.Stderr, " %s\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, " # Run with file logging\n")
fmt.Fprintf(os.Stderr, " %s --log-output file --log-dir /var/log/logwisp\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, " # Run with debug logging to both file and console\n")
fmt.Fprintf(os.Stderr, " %s --log-output both --log-level debug\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, " # Run with custom config and override log level\n")
fmt.Fprintf(os.Stderr, " %s --config /etc/logwisp.toml --log-level warn\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, " # Run in router mode with multiple streams\n")
fmt.Fprintf(os.Stderr, " %s --router --config /etc/logwisp/multi-stream.toml\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Environment Variables:\n")
fmt.Fprintf(os.Stderr, " LOGWISP_CONFIG_FILE Config file path\n")
fmt.Fprintf(os.Stderr, " LOGWISP_CONFIG_DIR Config directory\n")
fmt.Fprintf(os.Stderr, " LOGWISP_DISABLE_STATUS_REPORTER Disable periodic status reports (set to 1)\n")
fmt.Fprintf(os.Stderr, " LOGWISP_BACKGROUND Internal use - background process marker\n")
fmt.Fprintf(os.Stderr, "\nFor complete documentation, see: https://github.com/logwisp/logwisp/tree/main/doc\n")
}
func parseFlags() error {
flag.Parse()
// Validate log-output flag if provided
if *logOutput != "" {
validOutputs := map[string]bool{
"file": true, "stdout": true, "stderr": true,
"both": true, "none": true,
}
if !validOutputs[*logOutput] {
return fmt.Errorf("invalid log-output: %s (valid: file, stdout, stderr, both, none)", *logOutput)
}
}
// Validate log-level flag if provided
if *logLevel != "" {
if _, err := parseLogLevel(*logLevel); err != nil {
return fmt.Errorf("invalid log-level: %s (valid: debug, info, warn, error)", *logLevel)
}
}
// Validate log-console flag if provided
if *logConsole != "" {
validTargets := map[string]bool{
"stdout": true, "stderr": true, "split": true,
}
if !validTargets[*logConsole] {
return fmt.Errorf("invalid log-console: %s (valid: stdout, stderr, split)", *logConsole)
}
}
return nil
}
func parseLogLevel(level string) (int, error) {
switch strings.ToLower(level) {
case "debug":
return int(log.LevelDebug), nil
case "info":
return int(log.LevelInfo), nil
case "warn", "warning":
return int(log.LevelWarn), nil
case "error":
return int(log.LevelError), nil
default:
return 0, fmt.Errorf("unknown log level: %s", level)
}
}

View File

@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"flag"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
@ -11,25 +10,36 @@ import (
"time" "time"
"logwisp/src/internal/config" "logwisp/src/internal/config"
"logwisp/src/internal/service"
"logwisp/src/internal/version" "logwisp/src/internal/version"
"github.com/lixenwraith/log"
) )
func main() { var logger *log.Logger
// Parse CLI flags
var (
configFile = flag.String("config", "", "Config file path")
useRouter = flag.Bool("router", false, "Use HTTP router for path-based routing")
// routerPort = flag.Int("router-port", 0, "Override router port (default: first HTTP port)")
showVersion = flag.Bool("version", false, "Show version information")
)
flag.Parse()
func main() {
// Parse and validate flags
if err := parseFlags(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Handle version flag
if *showVersion { if *showVersion {
fmt.Println(version.String()) fmt.Println(version.String())
os.Exit(0) os.Exit(0)
} }
// Handle background mode
if *background && !isBackgroundProcess() {
if err := runInBackground(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to start background process: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
// Set config file environment if specified
if *configFile != "" { if *configFile != "" {
os.Setenv("LOGWISP_CONFIG_FILE", *configFile) os.Setenv("LOGWISP_CONFIG_FILE", *configFile)
} }
@ -41,6 +51,20 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// Initialize logger
if err := initializeLogger(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
os.Exit(1)
}
defer shutdownLogger()
// Log startup information
logger.Info("msg", "LogWisp starting",
"version", version.String(),
"config_file", *configFile,
"log_output", cfg.Logging.Output,
"router_mode", *useRouter)
// Create context for shutdown // Create context for shutdown
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -49,82 +73,29 @@ func main() {
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Create log transport service // Bootstrap the service
svc := service.New(ctx) svc, router, err := bootstrapService(ctx, cfg)
if err != nil {
// Create HTTP router if requested logger.Error("msg", "Failed to bootstrap service", "error", err)
var router *service.HTTPRouter
if *useRouter {
router = service.NewHTTPRouter(svc)
fmt.Println("HTTP router mode enabled")
}
// Initialize streams
successCount := 0
for _, streamCfg := range cfg.Streams {
fmt.Printf("Initializing transport '%s'...\n", streamCfg.Name)
// Set router mode BEFORE creating transport
if *useRouter && streamCfg.HTTPServer != nil && streamCfg.HTTPServer.Enabled {
// Temporarily disable standalone server startup
originalEnabled := streamCfg.HTTPServer.Enabled
streamCfg.HTTPServer.Enabled = false
if err := svc.CreateStream(streamCfg); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create transport '%s': %v\n", streamCfg.Name, err)
continue
}
// Get the created transport and configure for router mode
stream, _ := svc.GetStream(streamCfg.Name)
if stream.HTTPServer != nil {
stream.HTTPServer.SetRouterMode()
// Restore enabled state
stream.Config.HTTPServer.Enabled = originalEnabled
if err := router.RegisterStream(stream); err != nil {
fmt.Fprintf(os.Stderr, "Failed to register transport '%s' with router: %v\n",
streamCfg.Name, err)
} else {
fmt.Printf("Stream '%s' registered with router\n", streamCfg.Name)
}
}
} else {
// Standard standalone mode
if err := svc.CreateStream(streamCfg); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create transport '%s': %v\n", streamCfg.Name, err)
continue
}
}
successCount++
// Display endpoints
displayStreamEndpoints(streamCfg, *useRouter)
}
if successCount == 0 {
fmt.Fprintln(os.Stderr, "No streams successfully started")
os.Exit(1) os.Exit(1)
} }
fmt.Printf("LogWisp %s\n", version.Short()) // Start status reporter if enabled
fmt.Printf("\n%d transport(s) running. Press Ctrl+C to stop.\n", successCount) if shouldEnableStatusReporter() {
// Start periodic status display
go statusReporter(svc) go statusReporter(svc)
}
// Wait for shutdown // Wait for shutdown signal
<-sigChan <-sigChan
fmt.Println("\nShutting down...") logger.Info("msg", "Shutdown signal received, starting graceful shutdown...")
// Shutdown router first if using it // Shutdown router first if using it
if router != nil { if router != nil {
fmt.Println("Shutting down HTTP router...") logger.Info("msg", "Shutting down HTTP router...")
router.Shutdown() router.Shutdown()
} }
// Shutdown service (handles all streams) // Shutdown service with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel() defer shutdownCancel()
@ -136,68 +107,26 @@ func main() {
select { select {
case <-done: case <-done:
fmt.Println("Shutdown complete") logger.Info("msg", "Shutdown complete")
case <-shutdownCtx.Done(): case <-shutdownCtx.Done():
fmt.Println("Shutdown timeout - forcing exit") logger.Error("msg", "Shutdown timeout exceeded - forcing exit")
os.Exit(1) os.Exit(1)
} }
} }
func displayStreamEndpoints(cfg config.StreamConfig, routerMode bool) { func shutdownLogger() {
if cfg.TCPServer != nil && cfg.TCPServer.Enabled { if logger != nil {
fmt.Printf(" TCP: port %d\n", cfg.TCPServer.Port) if err := logger.Shutdown(2 * time.Second); err != nil {
} // Best effort - can't log the shutdown error
fmt.Fprintf(os.Stderr, "Logger shutdown error: %v\n", err)
if cfg.HTTPServer != nil && cfg.HTTPServer.Enabled {
if routerMode {
fmt.Printf(" HTTP: /%s%s (transport), /%s%s (status)\n",
cfg.Name, cfg.HTTPServer.StreamPath,
cfg.Name, cfg.HTTPServer.StatusPath)
} else {
fmt.Printf(" HTTP: http://localhost:%d%s (transport), http://localhost:%d%s (status)\n",
cfg.HTTPServer.Port, cfg.HTTPServer.StreamPath,
cfg.HTTPServer.Port, cfg.HTTPServer.StatusPath)
}
if cfg.Auth != nil && cfg.Auth.Type != "none" {
fmt.Printf(" Auth: %s\n", cfg.Auth.Type)
} }
} }
} }
func statusReporter(service *service.Service) { func shouldEnableStatusReporter() bool {
ticker := time.NewTicker(30 * time.Second) // Status reporter can be disabled via environment variable
defer ticker.Stop() if os.Getenv("LOGWISP_DISABLE_STATUS_REPORTER") == "1" {
return false
for range ticker.C {
stats := service.GetGlobalStats()
totalStreams := stats["total_streams"].(int)
if totalStreams == 0 {
return
}
fmt.Printf("\n[%s] Active streams: %d\n",
time.Now().Format("15:04:05"), totalStreams)
for name, streamStats := range stats["streams"].(map[string]interface{}) {
s := streamStats.(map[string]interface{})
fmt.Printf(" %s: ", name)
if monitor, ok := s["monitor"].(map[string]interface{}); ok {
fmt.Printf("watchers=%d entries=%d ",
monitor["active_watchers"],
monitor["total_entries"])
}
if tcp, ok := s["tcp"].(map[string]interface{}); ok && tcp["enabled"].(bool) {
fmt.Printf("tcp_conns=%d ", tcp["connections"])
}
if http, ok := s["http"].(map[string]interface{}); ok && http["enabled"].(bool) {
fmt.Printf("http_conns=%d ", http["connections"])
}
fmt.Println()
}
} }
return true
} }

119
src/cmd/logwisp/status.go Normal file
View File

@ -0,0 +1,119 @@
// FILE: src/cmd/logwisp/status.go
package main
import (
"fmt"
"time"
"logwisp/src/internal/config"
"logwisp/src/internal/service"
)
// statusReporter periodically logs service status
func statusReporter(service *service.Service) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
stats := service.GetGlobalStats()
totalStreams := stats["total_streams"].(int)
if totalStreams == 0 {
logger.Warn("msg", "No active streams in status report",
"component", "status_reporter")
return
}
// Log status at DEBUG level to avoid cluttering INFO logs
logger.Debug("msg", "Status report",
"component", "status_reporter",
"active_streams", totalStreams,
"time", time.Now().Format("15:04:05"))
// Log individual stream status
for name, streamStats := range stats["streams"].(map[string]interface{}) {
logStreamStatus(name, streamStats.(map[string]interface{}))
}
}
}
// logStreamStatus logs the status of an individual stream
func logStreamStatus(name string, stats map[string]interface{}) {
statusFields := []interface{}{
"msg", "Stream status",
"stream", name,
}
// Add monitor statistics
if monitor, ok := stats["monitor"].(map[string]interface{}); ok {
statusFields = append(statusFields,
"watchers", monitor["active_watchers"],
"entries", monitor["total_entries"])
}
// Add TCP server statistics
if tcp, ok := stats["tcp"].(map[string]interface{}); ok && tcp["enabled"].(bool) {
statusFields = append(statusFields, "tcp_conns", tcp["connections"])
}
// Add HTTP server statistics
if http, ok := stats["http"].(map[string]interface{}); ok && http["enabled"].(bool) {
statusFields = append(statusFields, "http_conns", http["connections"])
}
logger.Debug(statusFields...)
}
// displayStreamEndpoints logs the configured endpoints for a stream
func displayStreamEndpoints(cfg config.StreamConfig, routerMode bool) {
// Display TCP endpoints
if cfg.TCPServer != nil && cfg.TCPServer.Enabled {
logger.Info("msg", "TCP endpoint configured",
"component", "main",
"transport", cfg.Name,
"port", cfg.TCPServer.Port)
if cfg.TCPServer.RateLimit != nil && cfg.TCPServer.RateLimit.Enabled {
logger.Info("msg", "TCP rate limiting enabled",
"transport", cfg.Name,
"requests_per_second", cfg.TCPServer.RateLimit.RequestsPerSecond,
"burst_size", cfg.TCPServer.RateLimit.BurstSize)
}
}
// Display HTTP endpoints
if cfg.HTTPServer != nil && cfg.HTTPServer.Enabled {
if routerMode {
logger.Info("msg", "HTTP endpoints configured",
"transport", cfg.Name,
"stream_path", fmt.Sprintf("/%s%s", cfg.Name, cfg.HTTPServer.StreamPath),
"status_path", fmt.Sprintf("/%s%s", cfg.Name, cfg.HTTPServer.StatusPath))
} else {
logger.Info("msg", "HTTP endpoints configured",
"transport", cfg.Name,
"stream_url", fmt.Sprintf("http://localhost:%d%s", cfg.HTTPServer.Port, cfg.HTTPServer.StreamPath),
"status_url", fmt.Sprintf("http://localhost:%d%s", cfg.HTTPServer.Port, cfg.HTTPServer.StatusPath))
}
if cfg.HTTPServer.RateLimit != nil && cfg.HTTPServer.RateLimit.Enabled {
logger.Info("msg", "HTTP rate limiting enabled",
"transport", cfg.Name,
"requests_per_second", cfg.HTTPServer.RateLimit.RequestsPerSecond,
"burst_size", cfg.HTTPServer.RateLimit.BurstSize,
"limit_by", cfg.HTTPServer.RateLimit.LimitBy)
}
// Display authentication information
if cfg.Auth != nil && cfg.Auth.Type != "none" {
logger.Info("msg", "Authentication enabled",
"transport", cfg.Name,
"auth_type", cfg.Auth.Type)
}
}
// Display filter information
if len(cfg.Filters) > 0 {
logger.Info("msg", "Filters configured",
"transport", cfg.Name,
"filter_count", len(cfg.Filters))
}
}

View File

@ -2,6 +2,9 @@
package config package config
type Config struct { type Config struct {
// Logging configuration
Logging *LogConfig `toml:"logging"`
// Stream configurations // Stream configurations
Streams []StreamConfig `toml:"streams"` Streams []StreamConfig `toml:"streams"`
} }

View File

@ -12,6 +12,7 @@ import (
func defaults() *Config { func defaults() *Config {
return &Config{ return &Config{
Logging: DefaultLogConfig(),
Streams: []StreamConfig{ Streams: []StreamConfig{
{ {
Name: "default", Name: "default",

View File

@ -0,0 +1,62 @@
// FILE: src/internal/config/logging.go
package config
// LogConfig represents logging configuration for LogWisp
type LogConfig struct {
// Output mode: "file", "stdout", "stderr", "both", "none"
Output string `toml:"output"`
// Log level: "debug", "info", "warn", "error"
Level string `toml:"level"`
// File output settings (when Output includes "file" or "both")
File *LogFileConfig `toml:"file"`
// Console output settings
Console *LogConsoleConfig `toml:"console"`
}
type LogFileConfig struct {
// Directory for log files
Directory string `toml:"directory"`
// Base name for log files
Name string `toml:"name"`
// Maximum size per log file in MB
MaxSizeMB int64 `toml:"max_size_mb"`
// Maximum total size of all logs in MB
MaxTotalSizeMB int64 `toml:"max_total_size_mb"`
// Log retention in hours (0 = disabled)
RetentionHours float64 `toml:"retention_hours"`
}
type LogConsoleConfig struct {
// Target for console output: "stdout", "stderr", "split"
// "split" means info/debug to stdout, warn/error to stderr
Target string `toml:"target"`
// Format: "txt" or "json"
Format string `toml:"format"`
}
// DefaultLogConfig returns sensible logging defaults
func DefaultLogConfig() *LogConfig {
return &LogConfig{
Output: "stderr", // Default to stderr for containerized environments
Level: "info",
File: &LogFileConfig{
Directory: "./logs",
Name: "logwisp",
MaxSizeMB: 100,
MaxTotalSizeMB: 1000,
RetentionHours: 168, // 7 days
},
Console: &LogConsoleConfig{
Target: "stderr",
Format: "txt",
},
}
}

View File

@ -14,6 +14,10 @@ func (c *Config) validate() error {
return fmt.Errorf("no streams configured") return fmt.Errorf("no streams configured")
} }
if err := validateLogConfig(c.Logging); err != nil {
return fmt.Errorf("logging config: %w", err)
}
// Validate each transport // Validate each transport
streamNames := make(map[string]bool) streamNames := make(map[string]bool)
streamPorts := make(map[int]string) streamPorts := make(map[int]string)
@ -277,3 +281,31 @@ func validateFilter(streamName string, filterIndex int, cfg *filter.Config) erro
return nil return nil
} }
func validateLogConfig(cfg *LogConfig) error {
validOutputs := map[string]bool{
"file": true, "stdout": true, "stderr": true,
"both": true, "none": true,
}
if !validOutputs[cfg.Output] {
return fmt.Errorf("invalid log output mode: %s", cfg.Output)
}
validLevels := map[string]bool{
"debug": true, "info": true, "warn": true, "error": true,
}
if !validLevels[cfg.Level] {
return fmt.Errorf("invalid log level: %s", cfg.Level)
}
if cfg.Console != nil {
validTargets := map[string]bool{
"stdout": true, "stderr": true, "split": true,
}
if !validTargets[cfg.Console.Target] {
return fmt.Errorf("invalid console target: %s", cfg.Console.Target)
}
}
return nil
}

View File

@ -6,11 +6,14 @@ import (
"sync/atomic" "sync/atomic"
"logwisp/src/internal/monitor" "logwisp/src/internal/monitor"
"github.com/lixenwraith/log"
) )
// Chain manages multiple filters in sequence // Chain manages multiple filters in sequence
type Chain struct { type Chain struct {
filters []*Filter filters []*Filter
logger *log.Logger
// Statistics // Statistics
totalProcessed atomic.Uint64 totalProcessed atomic.Uint64
@ -18,19 +21,23 @@ type Chain struct {
} }
// NewChain creates a new filter chain from configurations // NewChain creates a new filter chain from configurations
func NewChain(configs []Config) (*Chain, error) { func NewChain(configs []Config, logger *log.Logger) (*Chain, error) {
chain := &Chain{ chain := &Chain{
filters: make([]*Filter, 0, len(configs)), filters: make([]*Filter, 0, len(configs)),
logger: logger,
} }
for i, cfg := range configs { for i, cfg := range configs {
filter, err := New(cfg) filter, err := New(cfg, logger)
if err != nil { if err != nil {
return nil, fmt.Errorf("filter[%d]: %w", i, err) return nil, fmt.Errorf("filter[%d]: %w", i, err)
} }
chain.filters = append(chain.filters, filter) chain.filters = append(chain.filters, filter)
} }
logger.Info("msg", "Filter chain created",
"component", "filter_chain",
"filter_count", len(configs))
return chain, nil return chain, nil
} }
@ -46,8 +53,12 @@ func (c *Chain) Apply(entry monitor.LogEntry) bool {
} }
// All filters must pass // All filters must pass
for _, filter := range c.filters { for i, filter := range c.filters {
if !filter.Apply(entry) { if !filter.Apply(entry) {
c.logger.Debug("msg", "Entry filtered out",
"component", "filter_chain",
"filter_index", i,
"filter_type", filter.config.Type)
return false return false
} }
} }

View File

@ -8,6 +8,8 @@ import (
"sync/atomic" "sync/atomic"
"logwisp/src/internal/monitor" "logwisp/src/internal/monitor"
"github.com/lixenwraith/log"
) )
// Type represents the filter type // Type represents the filter type
@ -38,6 +40,7 @@ type Filter struct {
config Config config Config
patterns []*regexp.Regexp patterns []*regexp.Regexp
mu sync.RWMutex mu sync.RWMutex
logger *log.Logger
// Statistics // Statistics
totalProcessed atomic.Uint64 totalProcessed atomic.Uint64
@ -46,7 +49,7 @@ type Filter struct {
} }
// New creates a new filter from configuration // New creates a new filter from configuration
func New(cfg Config) (*Filter, error) { func New(cfg Config, logger *log.Logger) (*Filter, error) {
// Set defaults // Set defaults
if cfg.Type == "" { if cfg.Type == "" {
cfg.Type = TypeInclude cfg.Type = TypeInclude
@ -58,6 +61,7 @@ func New(cfg Config) (*Filter, error) {
f := &Filter{ f := &Filter{
config: cfg, config: cfg,
patterns: make([]*regexp.Regexp, 0, len(cfg.Patterns)), patterns: make([]*regexp.Regexp, 0, len(cfg.Patterns)),
logger: logger,
} }
// Compile patterns // Compile patterns
@ -69,6 +73,12 @@ func New(cfg Config) (*Filter, error) {
f.patterns = append(f.patterns, re) f.patterns = append(f.patterns, re)
} }
logger.Debug("msg", "Filter created",
"component", "filter",
"type", cfg.Type,
"logic", cfg.Logic,
"pattern_count", len(cfg.Patterns))
return f, nil return f, nil
} }
@ -134,6 +144,9 @@ func (f *Filter) matches(text string) bool {
default: default:
// Shouldn't happen after validation // Shouldn't happen after validation
f.logger.Warn("msg", "Unknown filter logic",
"component", "filter",
"logic", f.config.Logic)
return false return false
} }
} }
@ -169,5 +182,8 @@ func (f *Filter) UpdatePatterns(patterns []string) error {
f.config.Patterns = patterns f.config.Patterns = patterns
f.mu.Unlock() f.mu.Unlock()
f.logger.Info("msg", "Filter patterns updated",
"component", "filter",
"pattern_count", len(patterns))
return nil return nil
} }

View File

@ -15,6 +15,8 @@ import (
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"time" "time"
"github.com/lixenwraith/log"
) )
type fileWatcher struct { type fileWatcher struct {
@ -29,13 +31,15 @@ type fileWatcher struct {
rotationSeq int rotationSeq int
entriesRead atomic.Uint64 entriesRead atomic.Uint64
lastReadTime atomic.Value // time.Time lastReadTime atomic.Value // time.Time
logger *log.Logger
} }
func newFileWatcher(path string, callback func(LogEntry)) *fileWatcher { func newFileWatcher(path string, callback func(LogEntry), logger *log.Logger) *fileWatcher {
w := &fileWatcher{ w := &fileWatcher{
path: path, path: path,
callback: callback, callback: callback,
position: -1, position: -1,
logger: logger,
} }
w.lastReadTime.Store(time.Time{}) w.lastReadTime.Store(time.Time{})
return w return w
@ -59,7 +63,7 @@ func (w *fileWatcher) watch(ctx context.Context) error {
} }
if err := w.checkFile(); err != nil { if err := w.checkFile(); err != nil {
// Log error but continue watching // Log error but continue watching
fmt.Printf("[WARN] checkFile error for %s: %v\n", w.path, err) w.logger.Warn("msg", "checkFile error", "error", err)
} }
} }
} }
@ -118,12 +122,20 @@ func (w *fileWatcher) checkFile() error {
// File doesn't exist yet, keep watching // File doesn't exist yet, keep watching
return nil return nil
} }
w.logger.Error("msg", "Failed to open file for checking",
"component", "file_watcher",
"path", w.path,
"error", err)
return err return err
} }
defer file.Close() defer file.Close()
info, err := file.Stat() info, err := file.Stat()
if err != nil { if err != nil {
w.logger.Error("msg", "Failed to stat file",
"component", "file_watcher",
"path", w.path,
"error", err)
return err return err
} }
@ -193,6 +205,12 @@ func (w *fileWatcher) checkFile() error {
Level: "INFO", Level: "INFO",
Message: fmt.Sprintf("Log rotation detected (#%d): %s", seq, rotationReason), Message: fmt.Sprintf("Log rotation detected (#%d): %s", seq, rotationReason),
}) })
w.logger.Info("msg", "Log rotation detected",
"component", "file_watcher",
"path", w.path,
"sequence", seq,
"reason", rotationReason)
} }
// Only read if there's new content // Only read if there's new content
@ -216,11 +234,20 @@ func (w *fileWatcher) checkFile() error {
w.lastReadTime.Store(time.Now()) w.lastReadTime.Store(time.Now())
} }
if err := scanner.Err(); err != nil {
w.logger.Error("msg", "Scanner error while reading file",
"component", "file_watcher",
"path", w.path,
"position", startPos,
"error", err)
return err
}
// Update position after successful read // Update position after successful read
currentPos, err := file.Seek(0, io.SeekCurrent) currentPos, err := file.Seek(0, io.SeekCurrent)
if err != nil { if err != nil {
// Log error but don't fail - position tracking is best effort // Log error but don't fail - position tracking is best effort
fmt.Printf("[WARN] Failed to get file position for %s: %v\n", w.path, err) w.logger.Warn("msg", "Failed to get file position", "error", err)
// Use size as fallback position // Use size as fallback position
currentPos = currentSize currentPos = currentSize
} }

View File

@ -4,6 +4,7 @@ package monitor
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -11,6 +12,8 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/lixenwraith/log"
) )
type LogEntry struct { type LogEntry struct {
@ -63,6 +66,7 @@ type monitor struct {
droppedEntries atomic.Uint64 droppedEntries atomic.Uint64
startTime time.Time startTime time.Time
lastEntryTime atomic.Value // time.Time lastEntryTime atomic.Value // time.Time
logger *log.Logger
} }
type target struct { type target struct {
@ -72,11 +76,12 @@ type target struct {
regex *regexp.Regexp regex *regexp.Regexp
} }
func New() Monitor { func New(logger *log.Logger) Monitor {
m := &monitor{ m := &monitor{
watchers: make(map[string]*fileWatcher), watchers: make(map[string]*fileWatcher),
checkInterval: 100 * time.Millisecond, checkInterval: 100 * time.Millisecond,
startTime: time.Now(), startTime: time.Now(),
logger: logger,
} }
m.lastEntryTime.Store(time.Time{}) m.lastEntryTime.Store(time.Time{})
return m return m
@ -103,6 +108,7 @@ func (m *monitor) publish(entry LogEntry) {
case ch <- entry: case ch <- entry:
default: default:
m.droppedEntries.Add(1) m.droppedEntries.Add(1)
m.logger.Debug("msg", "Dropped log entry - subscriber buffer full")
} }
} }
} }
@ -111,11 +117,17 @@ func (m *monitor) SetCheckInterval(interval time.Duration) {
m.mu.Lock() m.mu.Lock()
m.checkInterval = interval m.checkInterval = interval
m.mu.Unlock() m.mu.Unlock()
m.logger.Debug("msg", "Check interval updated", "interval_ms", interval.Milliseconds())
} }
func (m *monitor) AddTarget(path, pattern string, isFile bool) error { func (m *monitor) AddTarget(path, pattern string, isFile bool) error {
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
m.logger.Error("msg", "Failed to resolve absolute path",
"component", "monitor",
"path", path,
"error", err)
return fmt.Errorf("invalid path %s: %w", path, err) return fmt.Errorf("invalid path %s: %w", path, err)
} }
@ -124,6 +136,11 @@ func (m *monitor) AddTarget(path, pattern string, isFile bool) error {
regexPattern := globToRegex(pattern) regexPattern := globToRegex(pattern)
compiledRegex, err = regexp.Compile(regexPattern) compiledRegex, err = regexp.Compile(regexPattern)
if err != nil { if err != nil {
m.logger.Error("msg", "Failed to compile pattern regex",
"component", "monitor",
"pattern", pattern,
"regex", regexPattern,
"error", err)
return fmt.Errorf("invalid pattern %s: %w", pattern, err) return fmt.Errorf("invalid pattern %s: %w", pattern, err)
} }
} }
@ -137,6 +154,12 @@ func (m *monitor) AddTarget(path, pattern string, isFile bool) error {
}) })
m.mu.Unlock() m.mu.Unlock()
m.logger.Info("msg", "Added monitor target",
"component", "monitor",
"path", absPath,
"pattern", pattern,
"is_file", isFile)
return nil return nil
} }
@ -162,6 +185,9 @@ func (m *monitor) RemoveTarget(path string) error {
if w, exists := m.watchers[absPath]; exists { if w, exists := m.watchers[absPath]; exists {
w.stop() w.stop()
delete(m.watchers, absPath) delete(m.watchers, absPath)
m.logger.Info("msg", "Monitor started",
"component", "monitor",
"check_interval_ms", m.checkInterval.Milliseconds())
} }
return nil return nil
@ -171,6 +197,8 @@ func (m *monitor) Start(ctx context.Context) error {
m.ctx, m.cancel = context.WithCancel(ctx) m.ctx, m.cancel = context.WithCancel(ctx)
m.wg.Add(1) m.wg.Add(1)
go m.monitorLoop() go m.monitorLoop()
m.logger.Info("msg", "Monitor started", "check_interval_ms", m.checkInterval.Milliseconds())
return nil return nil
} }
@ -188,6 +216,8 @@ func (m *monitor) Stop() {
close(ch) close(ch)
} }
m.mu.Unlock() m.mu.Unlock()
m.logger.Info("msg", "Monitor stopped")
} }
func (m *monitor) GetStats() Stats { func (m *monitor) GetStats() Stats {
@ -262,7 +292,11 @@ func (m *monitor) checkTargets() {
// Directory scanning for pattern matching // Directory scanning for pattern matching
files, err := m.scanDirectory(t.path, t.regex) files, err := m.scanDirectory(t.path, t.regex)
if err != nil { if err != nil {
fmt.Printf("[DEBUG] Error scanning directory %s: %v\n", t.path, err) m.logger.Warn("msg", "Failed to scan directory",
"component", "monitor",
"path", t.path,
"pattern", t.pattern,
"error", err)
continue continue
} }
@ -304,16 +338,26 @@ func (m *monitor) ensureWatcher(path string) {
return return
} }
w := newFileWatcher(path, m.publish) w := newFileWatcher(path, m.publish, m.logger)
m.watchers[path] = w m.watchers[path] = w
fmt.Printf("[DEBUG] Created watcher for: %s\n", path) m.logger.Debug("msg", "Created watcher", "path", path)
m.wg.Add(1) m.wg.Add(1)
go func() { go func() {
defer m.wg.Done() defer m.wg.Done()
if err := w.watch(m.ctx); err != nil { if err := w.watch(m.ctx); err != nil {
fmt.Printf("[ERROR] Watcher for %s failed: %v\n", path, err) // Log based on error type
if errors.Is(err, context.Canceled) {
m.logger.Debug("msg", "Watcher cancelled",
"component", "monitor",
"path", path)
} else {
m.logger.Error("msg", "Watcher failed",
"component", "monitor",
"path", path,
"error", err)
}
} }
m.mu.Lock() m.mu.Lock()
@ -330,6 +374,7 @@ func (m *monitor) cleanupWatchers() {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
w.stop() w.stop()
delete(m.watchers, path) delete(m.watchers, path)
m.logger.Debug("msg", "Cleaned up watcher for non-existent file", "path", path)
} }
} }
} }

View File

@ -4,16 +4,20 @@ package ratelimit
import ( import (
"fmt" "fmt"
"net" "net"
"os"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"logwisp/src/internal/config" "logwisp/src/internal/config"
"github.com/lixenwraith/log"
) )
// Manages rate limiting for a transport // Manages rate limiting for a transport
type Limiter struct { type Limiter struct {
config config.RateLimitConfig config config.RateLimitConfig
logger *log.Logger
// Per-IP limiters // Per-IP limiters
ipLimiters map[string]*ipLimiter ipLimiters map[string]*ipLimiter
@ -53,6 +57,13 @@ func New(cfg config.RateLimitConfig) *Limiter {
ipLimiters: make(map[string]*ipLimiter), ipLimiters: make(map[string]*ipLimiter),
ipConnections: make(map[string]*atomic.Int32), ipConnections: make(map[string]*atomic.Int32),
lastCleanup: time.Now(), lastCleanup: time.Now(),
logger: log.NewLogger(),
}
// Initialize the logger with defaults
if err := l.logger.InitWithDefaults(); err != nil {
// Fall back to stderr logging if logger init fails
fmt.Fprintf(os.Stderr, "ratelimit: failed to initialize logger: %v\n", err)
} }
// Create global limiter if not using per-IP limiting // Create global limiter if not using per-IP limiting
@ -66,6 +77,12 @@ func New(cfg config.RateLimitConfig) *Limiter {
// Start cleanup goroutine // Start cleanup goroutine
go l.cleanupLoop() go l.cleanupLoop()
l.logger.Info("msg", "Rate limiter initialized",
"component", "ratelimit",
"requests_per_second", cfg.RequestsPerSecond,
"burst_size", cfg.BurstSize,
"limit_by", cfg.LimitBy)
return l return l
} }
@ -80,7 +97,10 @@ func (l *Limiter) CheckHTTP(remoteAddr string) (allowed bool, statusCode int, me
ip, _, err := net.SplitHostPort(remoteAddr) ip, _, err := net.SplitHostPort(remoteAddr)
if err != nil { if err != nil {
// If we can't parse the IP, allow the request but log // If we can't parse the IP, allow the request but log
fmt.Printf("[RATELIMIT] Failed to parse remote addr %s: %v\n", remoteAddr, err) l.logger.Warn("msg", "Failed to parse remote addr",
"component", "ratelimit",
"remote_addr", remoteAddr,
"error", err)
return true, 0, "" return true, 0, ""
} }
@ -97,6 +117,13 @@ func (l *Limiter) CheckHTTP(remoteAddr string) (allowed bool, statusCode int, me
statusCode = 429 statusCode = 429
} }
message = "Connection limit exceeded" message = "Connection limit exceeded"
l.logger.Warn("msg", "Connection limit exceeded",
"component", "ratelimit",
"ip", ip,
"connections", counter.Load(),
"limit", l.config.MaxConnectionsPerIP)
return false, statusCode, message return false, statusCode, message
} }
} }
@ -113,6 +140,7 @@ func (l *Limiter) CheckHTTP(remoteAddr string) (allowed bool, statusCode int, me
if message == "" { if message == "" {
message = "Rate limit exceeded" message = "Rate limit exceeded"
} }
l.logger.Debug("msg", "Request rate limited", "ip", ip)
} }
return allowed, statusCode, message return allowed, statusCode, message
@ -136,6 +164,7 @@ func (l *Limiter) CheckTCP(remoteAddr net.Addr) bool {
allowed := l.checkLimit(ip) allowed := l.checkLimit(ip)
if !allowed { if !allowed {
l.blockedRequests.Add(1) l.blockedRequests.Add(1)
l.logger.Debug("msg", "TCP connection rate limited", "ip", ip)
} }
return allowed return allowed
@ -160,7 +189,10 @@ func (l *Limiter) AddConnection(remoteAddr string) {
} }
l.connMu.Unlock() l.connMu.Unlock()
counter.Add(1) newCount := counter.Add(1)
l.logger.Debug("msg", "Connection added",
"ip", ip,
"connections", newCount)
} }
// Removes a connection for an IP // Removes a connection for an IP
@ -180,6 +212,10 @@ func (l *Limiter) RemoveConnection(remoteAddr string) {
if exists { if exists {
newCount := counter.Add(-1) newCount := counter.Add(-1)
l.logger.Debug("msg", "Connection removed",
"ip", ip,
"connections", newCount)
if newCount <= 0 { if newCount <= 0 {
// Clean up if no more connections // Clean up if no more connections
l.connMu.Lock() l.connMu.Lock()
@ -248,6 +284,10 @@ func (l *Limiter) checkLimit(ip string) bool {
} }
l.ipLimiters[ip] = limiter l.ipLimiters[ip] = limiter
l.uniqueIPs.Add(1) l.uniqueIPs.Add(1)
l.logger.Debug("msg", "Created new IP limiter",
"ip", ip,
"total_ips", l.uniqueIPs.Load())
} else { } else {
limiter.lastSeen = time.Now() limiter.lastSeen = time.Now()
} }
@ -268,6 +308,8 @@ func (l *Limiter) checkLimit(ip string) bool {
default: default:
// Unknown limit_by value, allow by default // Unknown limit_by value, allow by default
l.logger.Warn("msg", "Unknown limit_by value",
"limit_by", l.config.LimitBy)
return true return true
} }
} }
@ -293,11 +335,19 @@ func (l *Limiter) cleanup() {
l.ipMu.Lock() l.ipMu.Lock()
defer l.ipMu.Unlock() defer l.ipMu.Unlock()
cleaned := 0
for ip, limiter := range l.ipLimiters { for ip, limiter := range l.ipLimiters {
if now.Sub(limiter.lastSeen) > staleTimeout { if now.Sub(limiter.lastSeen) > staleTimeout {
delete(l.ipLimiters, ip) delete(l.ipLimiters, ip)
cleaned++
} }
} }
if cleaned > 0 {
l.logger.Debug("msg", "Cleaned up stale IP limiters",
"cleaned", cleaned,
"remaining", len(l.ipLimiters))
}
} }
// Runs periodic cleanup // Runs periodic cleanup

View File

@ -8,6 +8,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/lixenwraith/log"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@ -15,6 +16,7 @@ type HTTPRouter struct {
service *Service service *Service
servers map[int]*routerServer // port -> server servers map[int]*routerServer // port -> server
mu sync.RWMutex mu sync.RWMutex
logger *log.Logger
// Statistics // Statistics
startTime time.Time startTime time.Time
@ -23,11 +25,12 @@ type HTTPRouter struct {
failedRequests atomic.Uint64 failedRequests atomic.Uint64
} }
func NewHTTPRouter(service *Service) *HTTPRouter { func NewHTTPRouter(service *Service, logger *log.Logger) *HTTPRouter {
return &HTTPRouter{ return &HTTPRouter{
service: service, service: service,
servers: make(map[int]*routerServer), servers: make(map[int]*routerServer),
startTime: time.Now(), startTime: time.Now(),
logger: logger,
} }
} }
@ -47,6 +50,7 @@ func (r *HTTPRouter) RegisterStream(stream *LogStream) error {
routes: make(map[string]*LogStream), routes: make(map[string]*LogStream),
router: r, router: r,
startTime: time.Now(), startTime: time.Now(),
logger: r.logger,
} }
rs.server = &fasthttp.Server{ rs.server = &fasthttp.Server{
Handler: rs.requestHandler, Handler: rs.requestHandler,
@ -59,10 +63,14 @@ func (r *HTTPRouter) RegisterStream(stream *LogStream) error {
// Start server in background // Start server in background
go func() { go func() {
addr := fmt.Sprintf(":%d", port) addr := fmt.Sprintf(":%d", port)
fmt.Printf("[ROUTER] Starting server on port %d\n", port) r.logger.Info("msg", "Starting router server",
"component", "http_router",
"port", port)
if err := rs.server.ListenAndServe(addr); err != nil { if err := rs.server.ListenAndServe(addr); err != nil {
// Log error but don't crash r.logger.Error("msg", "Router server failed",
fmt.Printf("[ROUTER] Server on port %d failed: %v\n", port, err) "component", "http_router",
"port", port,
"error", err)
} }
}() }()
@ -87,7 +95,11 @@ func (r *HTTPRouter) RegisterStream(stream *LogStream) error {
} }
rs.routes[pathPrefix] = stream rs.routes[pathPrefix] = stream
fmt.Printf("[ROUTER] Registered transport '%s' at path '%s' on port %d\n", stream.Name, pathPrefix, port) r.logger.Info("msg", "Registered transport route",
"component", "http_router",
"transport", stream.Name,
"path", pathPrefix,
"port", port)
return nil return nil
} }

View File

@ -12,6 +12,8 @@ import (
"logwisp/src/internal/filter" "logwisp/src/internal/filter"
"logwisp/src/internal/monitor" "logwisp/src/internal/monitor"
"logwisp/src/internal/transport" "logwisp/src/internal/transport"
"github.com/lixenwraith/log"
) )
type LogStream struct { type LogStream struct {
@ -22,6 +24,7 @@ type LogStream struct {
TCPServer *transport.TCPStreamer TCPServer *transport.TCPStreamer
HTTPServer *transport.HTTPStreamer HTTPServer *transport.HTTPStreamer
Stats *StreamStats Stats *StreamStats
logger *log.Logger
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@ -38,6 +41,10 @@ type StreamStats struct {
} }
func (ls *LogStream) Shutdown() { func (ls *LogStream) Shutdown() {
ls.logger.Info("msg", "Shutting down stream",
"component", "logstream",
"stream", ls.Name)
// Stop servers first // Stop servers first
var wg sync.WaitGroup var wg sync.WaitGroup
@ -65,6 +72,10 @@ func (ls *LogStream) Shutdown() {
// Stop monitor // Stop monitor
ls.Monitor.Stop() ls.Monitor.Stop()
ls.logger.Info("msg", "Stream shutdown complete",
"component", "logstream",
"stream", ls.Name)
} }
func (ls *LogStream) GetStats() map[string]any { func (ls *LogStream) GetStats() map[string]any {
@ -112,6 +123,11 @@ func (ls *LogStream) UpdateTargets(targets []config.MonitorTarget) error {
// Basic validation // Basic validation
absPath, err := filepath.Abs(target.Path) absPath, err := filepath.Abs(target.Path)
if err != nil { if err != nil {
ls.logger.Error("msg", "Invalid target path",
"component", "logstream",
"stream", ls.Name,
"path", target.Path,
"error", err)
return fmt.Errorf("invalid target path %s: %w", target.Path, err) return fmt.Errorf("invalid target path %s: %w", target.Path, err)
} }
target.Path = absPath target.Path = absPath
@ -124,6 +140,12 @@ func (ls *LogStream) UpdateTargets(targets []config.MonitorTarget) error {
// Add new targets // Add new targets
for _, target := range validatedTargets { for _, target := range validatedTargets {
if err := ls.Monitor.AddTarget(target.Path, target.Pattern, target.IsFile); err != nil { if err := ls.Monitor.AddTarget(target.Path, target.Pattern, target.IsFile); err != nil {
ls.logger.Error("msg", "Failed to add monitor target - rolling back",
"component", "logstream",
"stream", ls.Name,
"target", target.Path,
"pattern", target.Pattern,
"error", err)
// Rollback: restore old watchers // Rollback: restore old watchers
for _, watcher := range oldWatchers { for _, watcher := range oldWatchers {
// Best effort restoration // Best effort restoration
@ -138,6 +160,12 @@ func (ls *LogStream) UpdateTargets(targets []config.MonitorTarget) error {
ls.Monitor.RemoveTarget(watcher.Path) ls.Monitor.RemoveTarget(watcher.Path)
} }
ls.logger.Info("msg", "Updated monitor targets",
"component", "logstream",
"stream", ls.Name,
"old_count", len(oldWatchers),
"new_count", len(validatedTargets))
return nil return nil
} }
@ -157,8 +185,11 @@ func (ls *LogStream) startStatsUpdater(ctx context.Context) {
ls.Stats.TCPConnections = ls.TCPServer.GetActiveConnections() ls.Stats.TCPConnections = ls.TCPServer.GetActiveConnections()
if oldTCP != ls.Stats.TCPConnections { if oldTCP != ls.Stats.TCPConnections {
// This debug should now show changes // This debug should now show changes
fmt.Printf("[STATS DEBUG] %s TCP: %d -> %d\n", ls.logger.Debug("msg", "TCP connection count changed",
ls.Name, oldTCP, ls.Stats.TCPConnections) "component", "logstream",
"stream", ls.Name,
"old", oldTCP,
"new", ls.Stats.TCPConnections)
} }
} }
if ls.HTTPServer != nil { if ls.HTTPServer != nil {
@ -166,8 +197,11 @@ func (ls *LogStream) startStatsUpdater(ctx context.Context) {
ls.Stats.HTTPConnections = ls.HTTPServer.GetActiveConnections() ls.Stats.HTTPConnections = ls.HTTPServer.GetActiveConnections()
if oldHTTP != ls.Stats.HTTPConnections { if oldHTTP != ls.Stats.HTTPConnections {
// This debug should now show changes // This debug should now show changes
fmt.Printf("[STATS DEBUG] %s HTTP: %d -> %d\n", ls.logger.Debug("msg", "HTTP connection count changed",
ls.Name, oldHTTP, ls.Stats.HTTPConnections) "component", "logstream",
"stream", ls.Name,
"old", oldHTTP,
"new", ls.Stats.HTTPConnections)
} }
} }
} }

View File

@ -9,13 +9,16 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/valyala/fasthttp"
"logwisp/src/internal/version" "logwisp/src/internal/version"
"github.com/lixenwraith/log"
"github.com/valyala/fasthttp"
) )
type routerServer struct { type routerServer struct {
port int port int
server *fasthttp.Server server *fasthttp.Server
logger *log.Logger
routes map[string]*LogStream // path prefix -> transport routes map[string]*LogStream // path prefix -> transport
routeMu sync.RWMutex routeMu sync.RWMutex
router *HTTPRouter router *HTTPRouter
@ -28,9 +31,14 @@ func (rs *routerServer) requestHandler(ctx *fasthttp.RequestCtx) {
rs.router.totalRequests.Add(1) rs.router.totalRequests.Add(1)
path := string(ctx.Path()) path := string(ctx.Path())
remoteAddr := ctx.RemoteAddr().String()
// Log request for debugging // Log request for debugging
fmt.Printf("[ROUTER] Request: %s %s from %s\n", ctx.Method(), path, ctx.RemoteAddr()) rs.logger.Debug("msg", "Router request",
"component", "router_server",
"method", ctx.Method(),
"path", path,
"remote_addr", remoteAddr)
// Special case: global status at /status // Special case: global status at /status
if path == "/status" { if path == "/status" {
@ -79,8 +87,11 @@ func (rs *routerServer) requestHandler(ctx *fasthttp.RequestCtx) {
remainingPath = matchedStream.Config.HTTPServer.StreamPath remainingPath = matchedStream.Config.HTTPServer.StreamPath
} }
fmt.Printf("[ROUTER] Routing to transport '%s': %s -> %s\n", rs.logger.Debug("msg", "Routing request to transport",
matchedStream.Name, originalPath, remainingPath) "component", "router_server",
"transport", matchedStream.Name,
"original_path", originalPath,
"remaining_path", remainingPath)
ctx.URI().SetPath(remainingPath) ctx.URI().SetPath(remainingPath)
matchedStream.HTTPServer.RouteRequest(ctx) matchedStream.HTTPServer.RouteRequest(ctx)

View File

@ -11,6 +11,8 @@ import (
"logwisp/src/internal/filter" "logwisp/src/internal/filter"
"logwisp/src/internal/monitor" "logwisp/src/internal/monitor"
"logwisp/src/internal/transport" "logwisp/src/internal/transport"
"github.com/lixenwraith/log"
) )
type Service struct { type Service struct {
@ -19,14 +21,16 @@ type Service struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
wg sync.WaitGroup wg sync.WaitGroup
logger *log.Logger
} }
func New(ctx context.Context) *Service { func New(ctx context.Context, logger *log.Logger) *Service {
serviceCtx, cancel := context.WithCancel(ctx) serviceCtx, cancel := context.WithCancel(ctx)
return &Service{ return &Service{
streams: make(map[string]*LogStream), streams: make(map[string]*LogStream),
ctx: serviceCtx, ctx: serviceCtx,
cancel: cancel, cancel: cancel,
logger: logger,
} }
} }
@ -35,14 +39,21 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
defer s.mu.Unlock() defer s.mu.Unlock()
if _, exists := s.streams[cfg.Name]; exists { if _, exists := s.streams[cfg.Name]; exists {
return fmt.Errorf("transport '%s' already exists", cfg.Name) err := fmt.Errorf("transport '%s' already exists", cfg.Name)
s.logger.Error("msg", "Failed to create stream - duplicate name",
"component", "service",
"stream", cfg.Name,
"error", err)
return err
} }
s.logger.Debug("msg", "Creating stream", "stream", cfg.Name)
// Create transport context // Create transport context
streamCtx, streamCancel := context.WithCancel(s.ctx) streamCtx, streamCancel := context.WithCancel(s.ctx)
// Create monitor // Create monitor - pass the service logger directly
mon := monitor.New() mon := monitor.New(s.logger)
mon.SetCheckInterval(time.Duration(cfg.GetCheckInterval(100)) * time.Millisecond) mon.SetCheckInterval(time.Duration(cfg.GetCheckInterval(100)) * time.Millisecond)
// Add targets // Add targets
@ -56,15 +67,24 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
// Start monitor // Start monitor
if err := mon.Start(streamCtx); err != nil { if err := mon.Start(streamCtx); err != nil {
streamCancel() streamCancel()
s.logger.Error("msg", "Failed to start monitor",
"component", "service",
"stream", cfg.Name,
"error", err)
return fmt.Errorf("failed to start monitor: %w", err) return fmt.Errorf("failed to start monitor: %w", err)
} }
// Create filter chain // Create filter chain
var filterChain *filter.Chain var filterChain *filter.Chain
if len(cfg.Filters) > 0 { if len(cfg.Filters) > 0 {
chain, err := filter.NewChain(cfg.Filters) chain, err := filter.NewChain(cfg.Filters, s.logger)
if err != nil { if err != nil {
streamCancel() streamCancel()
s.logger.Error("msg", "Failed to create filter chain",
"component", "service",
"stream", cfg.Name,
"filter_count", len(cfg.Filters),
"error", err)
return fmt.Errorf("failed to create filter chain: %w", err) return fmt.Errorf("failed to create filter chain: %w", err)
} }
filterChain = chain filterChain = chain
@ -81,6 +101,7 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
}, },
ctx: streamCtx, ctx: streamCtx,
cancel: streamCancel, cancel: streamCancel,
logger: s.logger, // Use parent logger
} }
// Start TCP server if configured // Start TCP server if configured
@ -97,10 +118,18 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
s.filterLoop(streamCtx, rawChan, tcpChan, filterChain) s.filterLoop(streamCtx, rawChan, tcpChan, filterChain)
}() }()
ls.TCPServer = transport.NewTCPStreamer(tcpChan, *cfg.TCPServer) ls.TCPServer = transport.NewTCPStreamer(
tcpChan,
*cfg.TCPServer,
s.logger) // Pass parent logger
if err := s.startTCPServer(ls); err != nil { if err := s.startTCPServer(ls); err != nil {
ls.Shutdown() ls.Shutdown()
s.logger.Error("msg", "Failed to start TCP server",
"component", "service",
"stream", cfg.Name,
"port", cfg.TCPServer.Port,
"error", err)
return fmt.Errorf("TCP server failed: %w", err) return fmt.Errorf("TCP server failed: %w", err)
} }
} }
@ -119,10 +148,18 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
s.filterLoop(streamCtx, rawChan, httpChan, filterChain) s.filterLoop(streamCtx, rawChan, httpChan, filterChain)
}() }()
ls.HTTPServer = transport.NewHTTPStreamer(httpChan, *cfg.HTTPServer) ls.HTTPServer = transport.NewHTTPStreamer(
httpChan,
*cfg.HTTPServer,
s.logger) // Pass parent logger
if err := s.startHTTPServer(ls); err != nil { if err := s.startHTTPServer(ls); err != nil {
ls.Shutdown() ls.Shutdown()
s.logger.Error("msg", "Failed to start HTTP server",
"component", "service",
"stream", cfg.Name,
"port", cfg.HTTPServer.Port,
"error", err)
return fmt.Errorf("HTTP server failed: %w", err) return fmt.Errorf("HTTP server failed: %w", err)
} }
} }
@ -130,6 +167,7 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
ls.startStatsUpdater(streamCtx) ls.startStatsUpdater(streamCtx)
s.streams[cfg.Name] = ls s.streams[cfg.Name] = ls
s.logger.Info("msg", "Stream created successfully", "stream", cfg.Name)
return nil return nil
} }
@ -152,6 +190,7 @@ func (s *Service) filterLoop(ctx context.Context, in <-chan monitor.LogEntry, ou
return return
default: default:
// Drop if output buffer is full // Drop if output buffer is full
s.logger.Debug("msg", "Dropped log entry - buffer full")
} }
} }
} }
@ -186,15 +225,23 @@ func (s *Service) RemoveStream(name string) error {
stream, exists := s.streams[name] stream, exists := s.streams[name]
if !exists { if !exists {
return fmt.Errorf("transport '%s' not found", name) err := fmt.Errorf("transport '%s' not found", name)
s.logger.Warn("msg", "Cannot remove non-existent stream",
"component", "service",
"stream", name,
"error", err)
return err
} }
s.logger.Info("msg", "Removing stream", "stream", name)
stream.Shutdown() stream.Shutdown()
delete(s.streams, name) delete(s.streams, name)
return nil return nil
} }
func (s *Service) Shutdown() { func (s *Service) Shutdown() {
s.logger.Info("msg", "Service shutdown initiated")
s.mu.Lock() s.mu.Lock()
streams := make([]*LogStream, 0, len(s.streams)) streams := make([]*LogStream, 0, len(s.streams))
for _, stream := range s.streams { for _, stream := range s.streams {
@ -215,6 +262,8 @@ func (s *Service) Shutdown() {
s.cancel() s.cancel()
s.wg.Wait() s.wg.Wait()
s.logger.Info("msg", "Service shutdown complete")
} }
func (s *Service) GetGlobalStats() map[string]any { func (s *Service) GetGlobalStats() map[string]any {
@ -247,8 +296,13 @@ func (s *Service) startTCPServer(ls *LogStream) error {
// Check startup // Check startup
select { select {
case err := <-errChan: case err := <-errChan:
s.logger.Error("msg", "TCP server startup failed immediately",
"component", "service",
"stream", ls.Name,
"error", err)
return err return err
case <-time.After(time.Second): case <-time.After(time.Second):
s.logger.Debug("msg", "TCP server started", "stream", ls.Name)
return nil return nil
} }
} }
@ -267,8 +321,13 @@ func (s *Service) startHTTPServer(ls *LogStream) error {
// Check startup // Check startup
select { select {
case err := <-errChan: case err := <-errChan:
s.logger.Error("msg", "HTTP server startup failed immediately",
"component", "service",
"stream", ls.Name,
"error", err)
return err return err
case <-time.After(time.Second): case <-time.After(time.Second):
s.logger.Debug("msg", "HTTP server started", "stream", ls.Name)
return nil return nil
} }
} }

View File

@ -11,11 +11,14 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/valyala/fasthttp"
"logwisp/src/internal/config" "logwisp/src/internal/config"
"logwisp/src/internal/monitor" "logwisp/src/internal/monitor"
"logwisp/src/internal/ratelimit" "logwisp/src/internal/ratelimit"
"logwisp/src/internal/version" "logwisp/src/internal/version"
"github.com/lixenwraith/log"
"github.com/lixenwraith/log/compat"
"github.com/valyala/fasthttp"
) )
type HTTPStreamer struct { type HTTPStreamer struct {
@ -27,6 +30,7 @@ type HTTPStreamer struct {
startTime time.Time startTime time.Time
done chan struct{} done chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
logger *log.Logger
// Path configuration // Path configuration
streamPath string streamPath string
@ -39,7 +43,7 @@ type HTTPStreamer struct {
rateLimiter *ratelimit.Limiter rateLimiter *ratelimit.Limiter
} }
func NewHTTPStreamer(logChan chan monitor.LogEntry, cfg config.HTTPConfig) *HTTPStreamer { func NewHTTPStreamer(logChan chan monitor.LogEntry, cfg config.HTTPConfig, logger *log.Logger) *HTTPStreamer {
// Set default paths if not configured // Set default paths if not configured
streamPath := cfg.StreamPath streamPath := cfg.StreamPath
if streamPath == "" { if streamPath == "" {
@ -58,6 +62,7 @@ func NewHTTPStreamer(logChan chan monitor.LogEntry, cfg config.HTTPConfig) *HTTP
streamPath: streamPath, streamPath: streamPath,
statusPath: statusPath, statusPath: statusPath,
standalone: true, // Default to standalone mode standalone: true, // Default to standalone mode
logger: logger,
} }
// Initialize rate limiter if configured // Initialize rate limiter if configured
@ -71,19 +76,26 @@ func NewHTTPStreamer(logChan chan monitor.LogEntry, cfg config.HTTPConfig) *HTTP
// Configures the streamer for use with a router // Configures the streamer for use with a router
func (h *HTTPStreamer) SetRouterMode() { func (h *HTTPStreamer) SetRouterMode() {
h.standalone = false h.standalone = false
h.logger.Debug("msg", "HTTP streamer set to router mode",
"component", "http_streamer")
} }
func (h *HTTPStreamer) Start() error { func (h *HTTPStreamer) Start() error {
if !h.standalone { if !h.standalone {
// In router mode, don't start our own server // In router mode, don't start our own server
h.logger.Debug("msg", "HTTP streamer in router mode, skipping server start",
"component", "http_streamer")
return nil return nil
} }
// Create fasthttp adapter for logging
fasthttpLogger := compat.NewFastHTTPAdapter(h.logger)
h.server = &fasthttp.Server{ h.server = &fasthttp.Server{
Handler: h.requestHandler, Handler: h.requestHandler,
DisableKeepalive: false, DisableKeepalive: false,
StreamRequestBody: true, StreamRequestBody: true,
Logger: nil, Logger: fasthttpLogger,
} }
addr := fmt.Sprintf(":%d", h.config.Port) addr := fmt.Sprintf(":%d", h.config.Port)
@ -91,6 +103,11 @@ func (h *HTTPStreamer) Start() error {
// Run server in separate goroutine to avoid blocking // Run server in separate goroutine to avoid blocking
errChan := make(chan error, 1) errChan := make(chan error, 1)
go func() { go func() {
h.logger.Info("msg", "HTTP server started",
"component", "http_streamer",
"port", h.config.Port,
"stream_path", h.streamPath,
"status_path", h.statusPath)
err := h.server.ListenAndServe(addr) err := h.server.ListenAndServe(addr)
if err != nil { if err != nil {
errChan <- err errChan <- err
@ -103,11 +120,17 @@ func (h *HTTPStreamer) Start() error {
return err return err
case <-time.After(100 * time.Millisecond): case <-time.After(100 * time.Millisecond):
// Server started successfully // Server started successfully
h.logger.Info("msg", "HTTP server started",
"port", h.config.Port,
"stream_path", h.streamPath,
"status_path", h.statusPath)
return nil return nil
} }
} }
func (h *HTTPStreamer) Stop() { func (h *HTTPStreamer) Stop() {
h.logger.Info("msg", "Stopping HTTP server")
// Signal all client handlers to stop // Signal all client handlers to stop
close(h.done) close(h.done)
@ -120,6 +143,8 @@ func (h *HTTPStreamer) Stop() {
// Wait for all active client handlers to finish // Wait for all active client handlers to finish
h.wg.Wait() h.wg.Wait()
h.logger.Info("msg", "HTTP server stopped")
} }
func (h *HTTPStreamer) RouteRequest(ctx *fasthttp.RequestCtx) { func (h *HTTPStreamer) RouteRequest(ctx *fasthttp.RequestCtx) {
@ -193,6 +218,9 @@ func (h *HTTPStreamer) handleStream(ctx *fasthttp.RequestCtx) {
return return
default: default:
// Drop if client buffer full // Drop if client buffer full
h.logger.Debug("msg", "Dropped entry for slow client",
"component", "http_streamer",
"remote_addr", remoteAddr)
} }
case <-clientDone: case <-clientDone:
return return
@ -205,14 +233,16 @@ func (h *HTTPStreamer) handleStream(ctx *fasthttp.RequestCtx) {
// Define the transport writer function // Define the transport writer function
streamFunc := func(w *bufio.Writer) { streamFunc := func(w *bufio.Writer) {
newCount := h.activeClients.Add(1) newCount := h.activeClients.Add(1)
fmt.Printf("[HTTP DEBUG] Client connected on port %d. Count now: %d\n", h.logger.Debug("msg", "HTTP client connected",
h.config.Port, newCount) "remote_addr", remoteAddr,
"active_clients", newCount)
h.wg.Add(1) h.wg.Add(1)
defer func() { defer func() {
newCount := h.activeClients.Add(-1) newCount := h.activeClients.Add(-1)
fmt.Printf("[HTTP DEBUG] Client disconnected on port %d. Count now: %d\n", h.logger.Debug("msg", "HTTP client disconnected",
h.config.Port, newCount) "remote_addr", remoteAddr,
"active_clients", newCount)
h.wg.Done() h.wg.Done()
}() }()
@ -246,6 +276,10 @@ func (h *HTTPStreamer) handleStream(ctx *fasthttp.RequestCtx) {
data, err := json.Marshal(entry) data, err := json.Marshal(entry)
if err != nil { if err != nil {
h.logger.Error("msg", "Failed to marshal log entry",
"component", "http_streamer",
"error", err,
"entry_source", entry.Source)
continue continue
} }

View File

@ -26,8 +26,8 @@ func (s *tcpServer) OnBoot(eng gnet.Engine) gnet.Action {
} }
func (s *tcpServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) { func (s *tcpServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
// Debug: Log all connection attempts remoteAddr := c.RemoteAddr().String()
fmt.Printf("[TCP DEBUG] Connection attempt from %s\n", c.RemoteAddr()) s.streamer.logger.Debug("msg", "TCP connection attempt", "remote_addr", remoteAddr)
// Check rate limit // Check rate limit
if s.streamer.rateLimiter != nil { if s.streamer.rateLimiter != nil {
@ -35,12 +35,15 @@ func (s *tcpServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
remoteStr := c.RemoteAddr().String() remoteStr := c.RemoteAddr().String()
tcpAddr, err := net.ResolveTCPAddr("tcp", remoteStr) tcpAddr, err := net.ResolveTCPAddr("tcp", remoteStr)
if err != nil { if err != nil {
fmt.Printf("[TCP DEBUG] Failed to parse address %s: %v\n", remoteStr, err) s.streamer.logger.Warn("msg", "Failed to parse TCP address",
"remote_addr", remoteAddr,
"error", err)
return nil, gnet.Close return nil, gnet.Close
} }
if !s.streamer.rateLimiter.CheckTCP(tcpAddr) { if !s.streamer.rateLimiter.CheckTCP(tcpAddr) {
fmt.Printf("[TCP DEBUG] Rate limited connection from %s\n", remoteStr) s.streamer.logger.Warn("msg", "TCP connection rate limited",
"remote_addr", remoteAddr)
// Silently close connection when rate limited // Silently close connection when rate limited
return nil, gnet.Close return nil, gnet.Close
} }
@ -51,27 +54,29 @@ func (s *tcpServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
s.connections.Store(c, struct{}{}) s.connections.Store(c, struct{}{})
oldCount := s.streamer.activeConns.Load()
newCount := s.streamer.activeConns.Add(1) newCount := s.streamer.activeConns.Add(1)
fmt.Printf("[TCP ATOMIC] OnOpen: %d -> %d (expected: %d)\n", oldCount, newCount, oldCount+1) s.streamer.logger.Debug("msg", "TCP connection opened",
"remote_addr", remoteAddr,
"active_connections", newCount)
fmt.Printf("[TCP DEBUG] Connection opened. Count now: %d\n", newCount)
return nil, gnet.None return nil, gnet.None
} }
func (s *tcpServer) OnClose(c gnet.Conn, err error) gnet.Action { func (s *tcpServer) OnClose(c gnet.Conn, err error) gnet.Action {
s.connections.Delete(c) s.connections.Delete(c)
remoteAddr := c.RemoteAddr().String()
// Remove connection tracking // Remove connection tracking
if s.streamer.rateLimiter != nil { if s.streamer.rateLimiter != nil {
s.streamer.rateLimiter.RemoveConnection(c.RemoteAddr().String()) s.streamer.rateLimiter.RemoveConnection(c.RemoteAddr().String())
} }
oldCount := s.streamer.activeConns.Load()
newCount := s.streamer.activeConns.Add(-1) newCount := s.streamer.activeConns.Add(-1)
fmt.Printf("[TCP ATOMIC] OnClose: %d -> %d (expected: %d)\n", oldCount, newCount, oldCount-1) s.streamer.logger.Debug("msg", "TCP connection closed",
"remote_addr", remoteAddr,
fmt.Printf("[TCP DEBUG] Connection closed. Count now: %d (err: %v)\n", newCount, err) "active_connections", newCount,
"error", err)
return gnet.None return gnet.None
} }
@ -80,7 +85,3 @@ func (s *tcpServer) OnTraffic(c gnet.Conn) gnet.Action {
c.Discard(-1) c.Discard(-1)
return gnet.None return gnet.None
} }
func (t *TCPStreamer) GetActiveConnections() int32 {
return t.activeConns.Load()
}

View File

@ -9,10 +9,12 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/panjf2000/gnet/v2"
"logwisp/src/internal/config" "logwisp/src/internal/config"
"logwisp/src/internal/monitor" "logwisp/src/internal/monitor"
"logwisp/src/internal/ratelimit" "logwisp/src/internal/ratelimit"
"github.com/lixenwraith/log"
"github.com/panjf2000/gnet/v2"
) )
type TCPStreamer struct { type TCPStreamer struct {
@ -26,14 +28,16 @@ type TCPStreamer struct {
engineMu sync.Mutex engineMu sync.Mutex
wg sync.WaitGroup wg sync.WaitGroup
rateLimiter *ratelimit.Limiter rateLimiter *ratelimit.Limiter
logger *log.Logger
} }
func NewTCPStreamer(logChan chan monitor.LogEntry, cfg config.TCPConfig) *TCPStreamer { func NewTCPStreamer(logChan chan monitor.LogEntry, cfg config.TCPConfig, logger *log.Logger) *TCPStreamer {
t := &TCPStreamer{ t := &TCPStreamer{
logChan: logChan, logChan: logChan,
config: cfg, config: cfg,
done: make(chan struct{}), done: make(chan struct{}),
startTime: time.Now(), startTime: time.Now(),
logger: logger,
} }
if cfg.RateLimit != nil && cfg.RateLimit.Enabled { if cfg.RateLimit != nil && cfg.RateLimit.Enabled {
@ -59,11 +63,21 @@ func (t *TCPStreamer) Start() error {
// Run gnet in separate goroutine to avoid blocking // Run gnet in separate goroutine to avoid blocking
errChan := make(chan error, 1) errChan := make(chan error, 1)
go func() { go func() {
t.logger.Info("msg", "Starting TCP server",
"component", "tcp_streamer",
"port", t.config.Port)
err := gnet.Run(t.server, addr, err := gnet.Run(t.server, addr,
gnet.WithLogger(noopLogger{}), gnet.WithLogger(noopLogger{}),
gnet.WithMulticore(true), gnet.WithMulticore(true),
gnet.WithReusePort(true), gnet.WithReusePort(true),
) )
if err != nil {
t.logger.Error("msg", "TCP server failed",
"component", "tcp_streamer",
"port", t.config.Port,
"error", err)
}
errChan <- err errChan <- err
}() }()
@ -76,11 +90,13 @@ func (t *TCPStreamer) Start() error {
return err return err
case <-time.After(100 * time.Millisecond): case <-time.After(100 * time.Millisecond):
// Server started successfully // Server started successfully
t.logger.Info("msg", "TCP server started", "port", t.config.Port)
return nil return nil
} }
} }
func (t *TCPStreamer) Stop() { func (t *TCPStreamer) Stop() {
t.logger.Info("msg", "Stopping TCP server")
// Signal broadcast loop to stop // Signal broadcast loop to stop
close(t.done) close(t.done)
@ -97,6 +113,8 @@ func (t *TCPStreamer) Stop() {
// Wait for broadcast loop to finish // Wait for broadcast loop to finish
t.wg.Wait() t.wg.Wait()
t.logger.Info("msg", "TCP server stopped")
} }
func (t *TCPStreamer) broadcastLoop() { func (t *TCPStreamer) broadcastLoop() {
@ -117,6 +135,10 @@ func (t *TCPStreamer) broadcastLoop() {
} }
data, err := json.Marshal(entry) data, err := json.Marshal(entry)
if err != nil { if err != nil {
t.logger.Error("msg", "Failed to marshal log entry",
"component", "tcp_streamer",
"error", err,
"entry_source", entry.Source)
continue continue
} }
data = append(data, '\n') data = append(data, '\n')
@ -163,3 +185,7 @@ func (t *TCPStreamer) formatHeartbeat() []byte {
jsonData, _ := json.Marshal(data) jsonData, _ := json.Marshal(data)
return append(jsonData, '\n') return append(jsonData, '\n')
} }
func (t *TCPStreamer) GetActiveConnections() int32 {
return t.activeConns.Load()
}