v0.1.10 internal package rename, minor fixes and refactoring
This commit is contained in:
14
README.md
14
README.md
@ -64,17 +64,17 @@ LogStream Service
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration file location: `~/.config/logwisp.toml`
|
Default configuration file location: `~/.config/logwisp.toml`
|
||||||
|
|
||||||
### Basic Multi-Stream Configuration
|
### Basic Multi-Stream Configuration
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Application logs stream
|
# Application logs transport
|
||||||
[[streams]]
|
[[streams]]
|
||||||
name = "app"
|
name = "app"
|
||||||
|
|
||||||
[streams.monitor]
|
[streams.monitor]
|
||||||
# Per-stream check interval in milliseconds
|
# Per-transport check interval in milliseconds
|
||||||
check_interval_ms = 100
|
check_interval_ms = 100
|
||||||
targets = [
|
targets = [
|
||||||
{ path = "/var/log/myapp", pattern = "*.log", is_file = false },
|
{ path = "/var/log/myapp", pattern = "*.log", is_file = false },
|
||||||
@ -116,7 +116,7 @@ response_code = 429
|
|||||||
response_message = "Rate limit exceeded"
|
response_message = "Rate limit exceeded"
|
||||||
max_connections_per_ip = 5
|
max_connections_per_ip = 5
|
||||||
|
|
||||||
# System logs stream with slower check interval
|
# System logs transport with slower check interval
|
||||||
[[streams]]
|
[[streams]]
|
||||||
name = "system"
|
name = "system"
|
||||||
|
|
||||||
@ -288,10 +288,10 @@ All HTTP streams share ports with path-based routing:
|
|||||||
### HTTP/SSE Stream
|
### HTTP/SSE Stream
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Connect to a stream
|
# Connect to a transport
|
||||||
curl -N http://localhost:8080/stream
|
curl -N http://localhost:8080/stream
|
||||||
|
|
||||||
# Check stream status (includes filter and rate limit stats)
|
# Check transport status (includes filter and rate limit stats)
|
||||||
curl http://localhost:8080/status
|
curl http://localhost:8080/status
|
||||||
|
|
||||||
# With authentication (when implemented)
|
# With authentication (when implemented)
|
||||||
@ -742,6 +742,8 @@ Contributions are welcome! Please read our contributing guidelines and submit pu
|
|||||||
- [x] Regex-based log filtering
|
- [x] Regex-based log filtering
|
||||||
- [ ] Log transformation (field extraction, formatting)
|
- [ ] Log transformation (field extraction, formatting)
|
||||||
- [ ] Configurable logging/stdout support
|
- [ ] Configurable logging/stdout support
|
||||||
|
- [ ] Service/non-interactive setup
|
||||||
|
- [ ] Live config change support
|
||||||
- [ ] Authentication (Basic, JWT, mTLS)
|
- [ ] Authentication (Basic, JWT, mTLS)
|
||||||
- [ ] TLS/SSL support
|
- [ ] TLS/SSL support
|
||||||
- [ ] Prometheus metrics export
|
- [ ] Prometheus metrics export
|
||||||
|
|||||||
@ -30,18 +30,18 @@ logwisp/
|
|||||||
├── filter/
|
├── filter/
|
||||||
│ ├── filter.go # Regex-based log filtering implementation
|
│ ├── filter.go # Regex-based log filtering implementation
|
||||||
│ └── chain.go # Sequential filter chain management
|
│ └── chain.go # Sequential filter chain management
|
||||||
├── logstream/
|
|
||||||
│ ├── httprouter.go # HTTP router for path-based routing
|
|
||||||
│ ├── logstream.go # Stream lifecycle management
|
|
||||||
│ ├── routerserver.go # Router server implementation
|
|
||||||
│ └── service.go # Multi-stream service orchestration
|
|
||||||
├── monitor/
|
├── monitor/
|
||||||
│ ├── file_watcher.go # File watching and rotation detection
|
│ ├── file_watcher.go # File watching and rotation detection
|
||||||
│ └── monitor.go # Log monitoring interface and implementation
|
│ └── monitor.go # Log monitoring interface and implementation
|
||||||
├── ratelimit/
|
├── ratelimit/
|
||||||
│ ├── ratelimit.go # Token bucket algorithm implementation
|
│ ├── ratelimit.go # Token bucket algorithm implementation
|
||||||
│ └── limiter.go # Per-stream rate limiter with IP tracking
|
│ └── limiter.go # Per-stream rate limiter with IP tracking
|
||||||
├── stream/
|
├── 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
|
│ ├── httpstreamer.go # HTTP/SSE streaming with rate limiting
|
||||||
│ ├── noop_logger.go # Silent logger for gnet
|
│ ├── noop_logger.go # Silent logger for gnet
|
||||||
│ ├── tcpserver.go # TCP server with rate limiting (gnet)
|
│ ├── tcpserver.go # TCP server with rate limiting (gnet)
|
||||||
@ -206,7 +206,7 @@ Client Request → Rate Limiter → Token Bucket Check → Allow/Deny
|
|||||||
name = "stream-name"
|
name = "stream-name"
|
||||||
|
|
||||||
[streams.monitor]
|
[streams.monitor]
|
||||||
check_interval_ms = 100 # Per-stream check interval
|
check_interval_ms = 100 # Per-transport check interval
|
||||||
targets = [
|
targets = [
|
||||||
{ path = "/path/to/logs", pattern = "*.log", is_file = false },
|
{ path = "/path/to/logs", pattern = "*.log", is_file = false },
|
||||||
{ path = "/path/to/file.log", is_file = true }
|
{ path = "/path/to/file.log", is_file = true }
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
"logwisp/src/internal/logstream"
|
"logwisp/src/internal/service"
|
||||||
"logwisp/src/internal/version"
|
"logwisp/src/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,41 +49,41 @@ 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 stream service
|
// Create log transport service
|
||||||
service := logstream.New(ctx)
|
svc := service.New(ctx)
|
||||||
|
|
||||||
// Create HTTP router if requested
|
// Create HTTP router if requested
|
||||||
var router *logstream.HTTPRouter
|
var router *service.HTTPRouter
|
||||||
if *useRouter {
|
if *useRouter {
|
||||||
router = logstream.NewHTTPRouter(service)
|
router = service.NewHTTPRouter(svc)
|
||||||
fmt.Println("HTTP router mode enabled")
|
fmt.Println("HTTP router mode enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize streams
|
// Initialize streams
|
||||||
successCount := 0
|
successCount := 0
|
||||||
for _, streamCfg := range cfg.Streams {
|
for _, streamCfg := range cfg.Streams {
|
||||||
fmt.Printf("Initializing stream '%s'...\n", streamCfg.Name)
|
fmt.Printf("Initializing transport '%s'...\n", streamCfg.Name)
|
||||||
|
|
||||||
// Set router mode BEFORE creating stream
|
// Set router mode BEFORE creating transport
|
||||||
if *useRouter && streamCfg.HTTPServer != nil && streamCfg.HTTPServer.Enabled {
|
if *useRouter && streamCfg.HTTPServer != nil && streamCfg.HTTPServer.Enabled {
|
||||||
// Temporarily disable standalone server startup
|
// Temporarily disable standalone server startup
|
||||||
originalEnabled := streamCfg.HTTPServer.Enabled
|
originalEnabled := streamCfg.HTTPServer.Enabled
|
||||||
streamCfg.HTTPServer.Enabled = false
|
streamCfg.HTTPServer.Enabled = false
|
||||||
|
|
||||||
if err := service.CreateStream(streamCfg); err != nil {
|
if err := svc.CreateStream(streamCfg); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create stream '%s': %v\n", streamCfg.Name, err)
|
fmt.Fprintf(os.Stderr, "Failed to create transport '%s': %v\n", streamCfg.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the created stream and configure for router mode
|
// Get the created transport and configure for router mode
|
||||||
stream, _ := service.GetStream(streamCfg.Name)
|
stream, _ := svc.GetStream(streamCfg.Name)
|
||||||
if stream.HTTPServer != nil {
|
if stream.HTTPServer != nil {
|
||||||
stream.HTTPServer.SetRouterMode()
|
stream.HTTPServer.SetRouterMode()
|
||||||
// Restore enabled state
|
// Restore enabled state
|
||||||
stream.Config.HTTPServer.Enabled = originalEnabled
|
stream.Config.HTTPServer.Enabled = originalEnabled
|
||||||
|
|
||||||
if err := router.RegisterStream(stream); err != nil {
|
if err := router.RegisterStream(stream); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to register stream '%s' with router: %v\n",
|
fmt.Fprintf(os.Stderr, "Failed to register transport '%s' with router: %v\n",
|
||||||
streamCfg.Name, err)
|
streamCfg.Name, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Stream '%s' registered with router\n", streamCfg.Name)
|
fmt.Printf("Stream '%s' registered with router\n", streamCfg.Name)
|
||||||
@ -91,8 +91,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Standard standalone mode
|
// Standard standalone mode
|
||||||
if err := service.CreateStream(streamCfg); err != nil {
|
if err := svc.CreateStream(streamCfg); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create stream '%s': %v\n", streamCfg.Name, err)
|
fmt.Fprintf(os.Stderr, "Failed to create transport '%s': %v\n", streamCfg.Name, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,10 +109,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("LogWisp %s\n", version.Short())
|
fmt.Printf("LogWisp %s\n", version.Short())
|
||||||
fmt.Printf("\n%d stream(s) running. Press Ctrl+C to stop.\n", successCount)
|
fmt.Printf("\n%d transport(s) running. Press Ctrl+C to stop.\n", successCount)
|
||||||
|
|
||||||
// Start periodic status display
|
// Start periodic status display
|
||||||
go statusReporter(service)
|
go statusReporter(svc)
|
||||||
|
|
||||||
// Wait for shutdown
|
// Wait for shutdown
|
||||||
<-sigChan
|
<-sigChan
|
||||||
@ -130,7 +130,7 @@ func main() {
|
|||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
service.Shutdown()
|
svc.Shutdown()
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -150,11 +150,11 @@ func displayStreamEndpoints(cfg config.StreamConfig, routerMode bool) {
|
|||||||
|
|
||||||
if cfg.HTTPServer != nil && cfg.HTTPServer.Enabled {
|
if cfg.HTTPServer != nil && cfg.HTTPServer.Enabled {
|
||||||
if routerMode {
|
if routerMode {
|
||||||
fmt.Printf(" HTTP: /%s%s (stream), /%s%s (status)\n",
|
fmt.Printf(" HTTP: /%s%s (transport), /%s%s (status)\n",
|
||||||
cfg.Name, cfg.HTTPServer.StreamPath,
|
cfg.Name, cfg.HTTPServer.StreamPath,
|
||||||
cfg.Name, cfg.HTTPServer.StatusPath)
|
cfg.Name, cfg.HTTPServer.StatusPath)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" HTTP: http://localhost:%d%s (stream), http://localhost:%d%s (status)\n",
|
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.StreamPath,
|
||||||
cfg.HTTPServer.Port, cfg.HTTPServer.StatusPath)
|
cfg.HTTPServer.Port, cfg.HTTPServer.StatusPath)
|
||||||
}
|
}
|
||||||
@ -165,7 +165,7 @@ func displayStreamEndpoints(cfg config.StreamConfig, routerMode bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusReporter(service *logstream.Service) {
|
func statusReporter(service *service.Service) {
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ func defaults() *Config {
|
|||||||
Enabled: true,
|
Enabled: true,
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
BufferSize: 1000,
|
BufferSize: 1000,
|
||||||
StreamPath: "/stream",
|
StreamPath: "/transport",
|
||||||
StatusPath: "/status",
|
StatusPath: "/status",
|
||||||
Heartbeat: HeartbeatConfig{
|
Heartbeat: HeartbeatConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
// FILE: src/internal/config/stream.go
|
// FILE: src/internal/config/transport.go
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "logwisp/src/internal/filter"
|
import (
|
||||||
|
"logwisp/src/internal/filter"
|
||||||
|
)
|
||||||
|
|
||||||
type StreamConfig struct {
|
type StreamConfig struct {
|
||||||
// Stream identifier (used in logs and metrics)
|
// Stream identifier (used in logs and metrics)
|
||||||
Name string `toml:"name"`
|
Name string `toml:"name"`
|
||||||
|
|
||||||
// Monitor configuration for this stream
|
// Monitor configuration for this transport
|
||||||
Monitor *StreamMonitorConfig `toml:"monitor"`
|
Monitor *StreamMonitorConfig `toml:"monitor"`
|
||||||
|
|
||||||
// Filter configuration
|
// Filter configuration
|
||||||
|
|||||||
@ -3,9 +3,10 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"logwisp/src/internal/filter"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"logwisp/src/internal/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Config) validate() error {
|
func (c *Config) validate() error {
|
||||||
@ -13,38 +14,38 @@ func (c *Config) validate() error {
|
|||||||
return fmt.Errorf("no streams configured")
|
return fmt.Errorf("no streams configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate each stream
|
// Validate each transport
|
||||||
streamNames := make(map[string]bool)
|
streamNames := make(map[string]bool)
|
||||||
streamPorts := make(map[int]string)
|
streamPorts := make(map[int]string)
|
||||||
|
|
||||||
for i, stream := range c.Streams {
|
for i, stream := range c.Streams {
|
||||||
if stream.Name == "" {
|
if stream.Name == "" {
|
||||||
return fmt.Errorf("stream %d: missing name", i)
|
return fmt.Errorf("transport %d: missing name", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if streamNames[stream.Name] {
|
if streamNames[stream.Name] {
|
||||||
return fmt.Errorf("stream %d: duplicate name '%s'", i, stream.Name)
|
return fmt.Errorf("transport %d: duplicate name '%s'", i, stream.Name)
|
||||||
}
|
}
|
||||||
streamNames[stream.Name] = true
|
streamNames[stream.Name] = true
|
||||||
|
|
||||||
// Stream must have monitor config with targets
|
// Stream must have monitor config with targets
|
||||||
if stream.Monitor == nil || len(stream.Monitor.Targets) == 0 {
|
if stream.Monitor == nil || len(stream.Monitor.Targets) == 0 {
|
||||||
return fmt.Errorf("stream '%s': no monitor targets specified", stream.Name)
|
return fmt.Errorf("transport '%s': no monitor targets specified", stream.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate check interval
|
// Validate check interval
|
||||||
if stream.Monitor.CheckIntervalMs < 10 {
|
if stream.Monitor.CheckIntervalMs < 10 {
|
||||||
return fmt.Errorf("stream '%s': check interval too small: %d ms (min: 10ms)",
|
return fmt.Errorf("transport '%s': check interval too small: %d ms (min: 10ms)",
|
||||||
stream.Name, stream.Monitor.CheckIntervalMs)
|
stream.Name, stream.Monitor.CheckIntervalMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate targets
|
// Validate targets
|
||||||
for j, target := range stream.Monitor.Targets {
|
for j, target := range stream.Monitor.Targets {
|
||||||
if target.Path == "" {
|
if target.Path == "" {
|
||||||
return fmt.Errorf("stream '%s' target %d: empty path", stream.Name, j)
|
return fmt.Errorf("transport '%s' target %d: empty path", stream.Name, j)
|
||||||
}
|
}
|
||||||
if strings.Contains(target.Path, "..") {
|
if strings.Contains(target.Path, "..") {
|
||||||
return fmt.Errorf("stream '%s' target %d: path contains directory traversal", stream.Name, j)
|
return fmt.Errorf("transport '%s' target %d: path contains directory traversal", stream.Name, j)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,16 +59,16 @@ func (c *Config) validate() error {
|
|||||||
// Validate TCP server
|
// Validate TCP server
|
||||||
if stream.TCPServer != nil && stream.TCPServer.Enabled {
|
if stream.TCPServer != nil && stream.TCPServer.Enabled {
|
||||||
if stream.TCPServer.Port < 1 || stream.TCPServer.Port > 65535 {
|
if stream.TCPServer.Port < 1 || stream.TCPServer.Port > 65535 {
|
||||||
return fmt.Errorf("stream '%s': invalid TCP port: %d", stream.Name, stream.TCPServer.Port)
|
return fmt.Errorf("transport '%s': invalid TCP port: %d", stream.Name, stream.TCPServer.Port)
|
||||||
}
|
}
|
||||||
if existing, exists := streamPorts[stream.TCPServer.Port]; exists {
|
if existing, exists := streamPorts[stream.TCPServer.Port]; exists {
|
||||||
return fmt.Errorf("stream '%s': TCP port %d already used by stream '%s'",
|
return fmt.Errorf("transport '%s': TCP port %d already used by transport '%s'",
|
||||||
stream.Name, stream.TCPServer.Port, existing)
|
stream.Name, stream.TCPServer.Port, existing)
|
||||||
}
|
}
|
||||||
streamPorts[stream.TCPServer.Port] = stream.Name + "-tcp"
|
streamPorts[stream.TCPServer.Port] = stream.Name + "-tcp"
|
||||||
|
|
||||||
if stream.TCPServer.BufferSize < 1 {
|
if stream.TCPServer.BufferSize < 1 {
|
||||||
return fmt.Errorf("stream '%s': TCP buffer size must be positive: %d",
|
return fmt.Errorf("transport '%s': TCP buffer size must be positive: %d",
|
||||||
stream.Name, stream.TCPServer.BufferSize)
|
stream.Name, stream.TCPServer.BufferSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,32 +88,32 @@ func (c *Config) validate() error {
|
|||||||
// Validate HTTP server
|
// Validate HTTP server
|
||||||
if stream.HTTPServer != nil && stream.HTTPServer.Enabled {
|
if stream.HTTPServer != nil && stream.HTTPServer.Enabled {
|
||||||
if stream.HTTPServer.Port < 1 || stream.HTTPServer.Port > 65535 {
|
if stream.HTTPServer.Port < 1 || stream.HTTPServer.Port > 65535 {
|
||||||
return fmt.Errorf("stream '%s': invalid HTTP port: %d", stream.Name, stream.HTTPServer.Port)
|
return fmt.Errorf("transport '%s': invalid HTTP port: %d", stream.Name, stream.HTTPServer.Port)
|
||||||
}
|
}
|
||||||
if existing, exists := streamPorts[stream.HTTPServer.Port]; exists {
|
if existing, exists := streamPorts[stream.HTTPServer.Port]; exists {
|
||||||
return fmt.Errorf("stream '%s': HTTP port %d already used by stream '%s'",
|
return fmt.Errorf("transport '%s': HTTP port %d already used by transport '%s'",
|
||||||
stream.Name, stream.HTTPServer.Port, existing)
|
stream.Name, stream.HTTPServer.Port, existing)
|
||||||
}
|
}
|
||||||
streamPorts[stream.HTTPServer.Port] = stream.Name + "-http"
|
streamPorts[stream.HTTPServer.Port] = stream.Name + "-http"
|
||||||
|
|
||||||
if stream.HTTPServer.BufferSize < 1 {
|
if stream.HTTPServer.BufferSize < 1 {
|
||||||
return fmt.Errorf("stream '%s': HTTP buffer size must be positive: %d",
|
return fmt.Errorf("transport '%s': HTTP buffer size must be positive: %d",
|
||||||
stream.Name, stream.HTTPServer.BufferSize)
|
stream.Name, stream.HTTPServer.BufferSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate paths
|
// Validate paths
|
||||||
if stream.HTTPServer.StreamPath == "" {
|
if stream.HTTPServer.StreamPath == "" {
|
||||||
stream.HTTPServer.StreamPath = "/stream"
|
stream.HTTPServer.StreamPath = "/transport"
|
||||||
}
|
}
|
||||||
if stream.HTTPServer.StatusPath == "" {
|
if stream.HTTPServer.StatusPath == "" {
|
||||||
stream.HTTPServer.StatusPath = "/status"
|
stream.HTTPServer.StatusPath = "/status"
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(stream.HTTPServer.StreamPath, "/") {
|
if !strings.HasPrefix(stream.HTTPServer.StreamPath, "/") {
|
||||||
return fmt.Errorf("stream '%s': stream path must start with /: %s",
|
return fmt.Errorf("transport '%s': transport path must start with /: %s",
|
||||||
stream.Name, stream.HTTPServer.StreamPath)
|
stream.Name, stream.HTTPServer.StreamPath)
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(stream.HTTPServer.StatusPath, "/") {
|
if !strings.HasPrefix(stream.HTTPServer.StatusPath, "/") {
|
||||||
return fmt.Errorf("stream '%s': status path must start with /: %s",
|
return fmt.Errorf("transport '%s': status path must start with /: %s",
|
||||||
stream.Name, stream.HTTPServer.StatusPath)
|
stream.Name, stream.HTTPServer.StatusPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +134,7 @@ func (c *Config) validate() error {
|
|||||||
tcpEnabled := stream.TCPServer != nil && stream.TCPServer.Enabled
|
tcpEnabled := stream.TCPServer != nil && stream.TCPServer.Enabled
|
||||||
httpEnabled := stream.HTTPServer != nil && stream.HTTPServer.Enabled
|
httpEnabled := stream.HTTPServer != nil && stream.HTTPServer.Enabled
|
||||||
if !tcpEnabled && !httpEnabled {
|
if !tcpEnabled && !httpEnabled {
|
||||||
return fmt.Errorf("stream '%s': no servers enabled", stream.Name)
|
return fmt.Errorf("transport '%s': no servers enabled", stream.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate auth if present
|
// Validate auth if present
|
||||||
@ -148,11 +149,11 @@ func (c *Config) validate() error {
|
|||||||
func validateHeartbeat(serverType, streamName string, hb *HeartbeatConfig) error {
|
func validateHeartbeat(serverType, streamName string, hb *HeartbeatConfig) error {
|
||||||
if hb.Enabled {
|
if hb.Enabled {
|
||||||
if hb.IntervalSeconds < 1 {
|
if hb.IntervalSeconds < 1 {
|
||||||
return fmt.Errorf("stream '%s' %s: heartbeat interval must be positive: %d",
|
return fmt.Errorf("transport '%s' %s: heartbeat interval must be positive: %d",
|
||||||
streamName, serverType, hb.IntervalSeconds)
|
streamName, serverType, hb.IntervalSeconds)
|
||||||
}
|
}
|
||||||
if hb.Format != "json" && hb.Format != "comment" {
|
if hb.Format != "json" && hb.Format != "comment" {
|
||||||
return fmt.Errorf("stream '%s' %s: heartbeat format must be 'json' or 'comment': %s",
|
return fmt.Errorf("transport '%s' %s: heartbeat format must be 'json' or 'comment': %s",
|
||||||
streamName, serverType, hb.Format)
|
streamName, serverType, hb.Format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,23 +163,23 @@ func validateHeartbeat(serverType, streamName string, hb *HeartbeatConfig) error
|
|||||||
func validateSSL(serverType, streamName string, ssl *SSLConfig) error {
|
func validateSSL(serverType, streamName string, ssl *SSLConfig) error {
|
||||||
if ssl != nil && ssl.Enabled {
|
if ssl != nil && ssl.Enabled {
|
||||||
if ssl.CertFile == "" || ssl.KeyFile == "" {
|
if ssl.CertFile == "" || ssl.KeyFile == "" {
|
||||||
return fmt.Errorf("stream '%s' %s: SSL enabled but cert/key files not specified",
|
return fmt.Errorf("transport '%s' %s: SSL enabled but cert/key files not specified",
|
||||||
streamName, serverType)
|
streamName, serverType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ssl.ClientAuth && ssl.ClientCAFile == "" {
|
if ssl.ClientAuth && ssl.ClientCAFile == "" {
|
||||||
return fmt.Errorf("stream '%s' %s: client auth enabled but CA file not specified",
|
return fmt.Errorf("transport '%s' %s: client auth enabled but CA file not specified",
|
||||||
streamName, serverType)
|
streamName, serverType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate TLS versions
|
// Validate TLS versions
|
||||||
validVersions := map[string]bool{"TLS1.0": true, "TLS1.1": true, "TLS1.2": true, "TLS1.3": true}
|
validVersions := map[string]bool{"TLS1.0": true, "TLS1.1": true, "TLS1.2": true, "TLS1.3": true}
|
||||||
if ssl.MinVersion != "" && !validVersions[ssl.MinVersion] {
|
if ssl.MinVersion != "" && !validVersions[ssl.MinVersion] {
|
||||||
return fmt.Errorf("stream '%s' %s: invalid min TLS version: %s",
|
return fmt.Errorf("transport '%s' %s: invalid min TLS version: %s",
|
||||||
streamName, serverType, ssl.MinVersion)
|
streamName, serverType, ssl.MinVersion)
|
||||||
}
|
}
|
||||||
if ssl.MaxVersion != "" && !validVersions[ssl.MaxVersion] {
|
if ssl.MaxVersion != "" && !validVersions[ssl.MaxVersion] {
|
||||||
return fmt.Errorf("stream '%s' %s: invalid max TLS version: %s",
|
return fmt.Errorf("transport '%s' %s: invalid max TLS version: %s",
|
||||||
streamName, serverType, ssl.MaxVersion)
|
streamName, serverType, ssl.MaxVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,15 +193,15 @@ func validateAuth(streamName string, auth *AuthConfig) error {
|
|||||||
|
|
||||||
validTypes := map[string]bool{"none": true, "basic": true, "bearer": true, "mtls": true}
|
validTypes := map[string]bool{"none": true, "basic": true, "bearer": true, "mtls": true}
|
||||||
if !validTypes[auth.Type] {
|
if !validTypes[auth.Type] {
|
||||||
return fmt.Errorf("stream '%s': invalid auth type: %s", streamName, auth.Type)
|
return fmt.Errorf("transport '%s': invalid auth type: %s", streamName, auth.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.Type == "basic" && auth.BasicAuth == nil {
|
if auth.Type == "basic" && auth.BasicAuth == nil {
|
||||||
return fmt.Errorf("stream '%s': basic auth type specified but config missing", streamName)
|
return fmt.Errorf("transport '%s': basic auth type specified but config missing", streamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if auth.Type == "bearer" && auth.BearerAuth == nil {
|
if auth.Type == "bearer" && auth.BearerAuth == nil {
|
||||||
return fmt.Errorf("stream '%s': bearer auth type specified but config missing", streamName)
|
return fmt.Errorf("transport '%s': bearer auth type specified but config missing", streamName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -212,26 +213,33 @@ func validateRateLimit(serverType, streamName string, rl *RateLimitConfig) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rl.RequestsPerSecond <= 0 {
|
if rl.RequestsPerSecond <= 0 {
|
||||||
return fmt.Errorf("stream '%s' %s: requests_per_second must be positive: %f",
|
return fmt.Errorf("transport '%s' %s: requests_per_second must be positive: %f",
|
||||||
streamName, serverType, rl.RequestsPerSecond)
|
streamName, serverType, rl.RequestsPerSecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rl.BurstSize < 1 {
|
if rl.BurstSize < 1 {
|
||||||
return fmt.Errorf("stream '%s' %s: burst_size must be at least 1: %d",
|
return fmt.Errorf("transport '%s' %s: burst_size must be at least 1: %d",
|
||||||
streamName, serverType, rl.BurstSize)
|
streamName, serverType, rl.BurstSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
validLimitBy := map[string]bool{"ip": true, "global": true, "": true}
|
validLimitBy := map[string]bool{"ip": true, "global": true, "": true}
|
||||||
if !validLimitBy[rl.LimitBy] {
|
if !validLimitBy[rl.LimitBy] {
|
||||||
return fmt.Errorf("stream '%s' %s: invalid limit_by value: %s (must be 'ip' or 'global')",
|
return fmt.Errorf("transport '%s' %s: invalid limit_by value: %s (must be 'ip' or 'global')",
|
||||||
streamName, serverType, rl.LimitBy)
|
streamName, serverType, rl.LimitBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rl.ResponseCode > 0 && (rl.ResponseCode < 400 || rl.ResponseCode >= 600) {
|
if rl.ResponseCode > 0 && (rl.ResponseCode < 400 || rl.ResponseCode >= 600) {
|
||||||
return fmt.Errorf("stream '%s' %s: response_code must be 4xx or 5xx: %d",
|
return fmt.Errorf("transport '%s' %s: response_code must be 4xx or 5xx: %d",
|
||||||
streamName, serverType, rl.ResponseCode)
|
streamName, serverType, rl.ResponseCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rl.MaxConnectionsPerIP > 0 && rl.MaxTotalConnections > 0 {
|
||||||
|
if rl.MaxConnectionsPerIP > rl.MaxTotalConnections {
|
||||||
|
return fmt.Errorf("stream '%s' %s: max_connections_per_ip (%d) cannot exceed max_total_connections (%d)",
|
||||||
|
streamName, serverType, rl.MaxConnectionsPerIP, rl.MaxTotalConnections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,7 +249,7 @@ func validateFilter(streamName string, filterIndex int, cfg *filter.Config) erro
|
|||||||
case filter.TypeInclude, filter.TypeExclude, "":
|
case filter.TypeInclude, filter.TypeExclude, "":
|
||||||
// Valid types
|
// Valid types
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("stream '%s' filter[%d]: invalid type '%s' (must be 'include' or 'exclude')",
|
return fmt.Errorf("transport '%s' filter[%d]: invalid type '%s' (must be 'include' or 'exclude')",
|
||||||
streamName, filterIndex, cfg.Type)
|
streamName, filterIndex, cfg.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +258,7 @@ func validateFilter(streamName string, filterIndex int, cfg *filter.Config) erro
|
|||||||
case filter.LogicOr, filter.LogicAnd, "":
|
case filter.LogicOr, filter.LogicAnd, "":
|
||||||
// Valid logic
|
// Valid logic
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("stream '%s' filter[%d]: invalid logic '%s' (must be 'or' or 'and')",
|
return fmt.Errorf("transport '%s' filter[%d]: invalid logic '%s' (must be 'or' or 'and')",
|
||||||
streamName, filterIndex, cfg.Logic)
|
streamName, filterIndex, cfg.Logic)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,7 +270,7 @@ func validateFilter(streamName string, filterIndex int, cfg *filter.Config) erro
|
|||||||
// Validate regex patterns
|
// Validate regex patterns
|
||||||
for i, pattern := range cfg.Patterns {
|
for i, pattern := range cfg.Patterns {
|
||||||
if _, err := regexp.Compile(pattern); err != nil {
|
if _, err := regexp.Compile(pattern); err != nil {
|
||||||
return fmt.Errorf("stream '%s' filter[%d] pattern[%d] '%s': invalid regex: %w",
|
return fmt.Errorf("transport '%s' filter[%d] pattern[%d] '%s': invalid regex: %w",
|
||||||
streamName, filterIndex, i, pattern, err)
|
streamName, filterIndex, i, pattern, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -217,7 +217,14 @@ func (w *fileWatcher) checkFile() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update position after successful read
|
// Update position after successful read
|
||||||
if currentPos, err := file.Seek(0, io.SeekCurrent); err == nil {
|
currentPos, err := file.Seek(0, io.SeekCurrent)
|
||||||
|
if err != nil {
|
||||||
|
// 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)
|
||||||
|
// Use size as fallback position
|
||||||
|
currentPos = currentSize
|
||||||
|
}
|
||||||
|
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
w.position = currentPos
|
w.position = currentPos
|
||||||
w.size = currentSize
|
w.size = currentSize
|
||||||
@ -226,7 +233,6 @@ func (w *fileWatcher) checkFile() error {
|
|||||||
w.inode = currentInode
|
w.inode = currentInode
|
||||||
}
|
}
|
||||||
w.mu.Unlock()
|
w.mu.Unlock()
|
||||||
}
|
|
||||||
|
|
||||||
return scanner.Err()
|
return scanner.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manages rate limiting for a stream
|
// Manages rate limiting for a transport
|
||||||
type Limiter struct {
|
type Limiter struct {
|
||||||
config config.RateLimitConfig
|
config config.RateLimitConfig
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ type Limiter struct {
|
|||||||
ipLimiters map[string]*ipLimiter
|
ipLimiters map[string]*ipLimiter
|
||||||
ipMu sync.RWMutex
|
ipMu sync.RWMutex
|
||||||
|
|
||||||
// Global limiter for the stream
|
// Global limiter for the transport
|
||||||
globalLimiter *TokenBucket
|
globalLimiter *TokenBucket
|
||||||
|
|
||||||
// Connection tracking
|
// Connection tracking
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// FILE: src/internal/logstream/httprouter.go
|
// FILE: src/internal/service/httprouter.go
|
||||||
package logstream
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -71,23 +71,23 @@ func (r *HTTPRouter) RegisterStream(stream *LogStream) error {
|
|||||||
}
|
}
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
|
|
||||||
// Register routes for this stream
|
// Register routes for this transport
|
||||||
rs.routeMu.Lock()
|
rs.routeMu.Lock()
|
||||||
defer rs.routeMu.Unlock()
|
defer rs.routeMu.Unlock()
|
||||||
|
|
||||||
// Use stream name as path prefix
|
// Use transport name as path prefix
|
||||||
pathPrefix := "/" + stream.Name
|
pathPrefix := "/" + stream.Name
|
||||||
|
|
||||||
// Check for conflicts
|
// Check for conflicts
|
||||||
for existingPath, existingStream := range rs.routes {
|
for existingPath, existingStream := range rs.routes {
|
||||||
if strings.HasPrefix(pathPrefix, existingPath) || strings.HasPrefix(existingPath, pathPrefix) {
|
if strings.HasPrefix(pathPrefix, existingPath) || strings.HasPrefix(existingPath, pathPrefix) {
|
||||||
return fmt.Errorf("path conflict: '%s' conflicts with existing stream '%s' at '%s'",
|
return fmt.Errorf("path conflict: '%s' conflicts with existing transport '%s' at '%s'",
|
||||||
pathPrefix, existingStream.Name, existingPath)
|
pathPrefix, existingStream.Name, existingPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.routes[pathPrefix] = stream
|
rs.routes[pathPrefix] = stream
|
||||||
fmt.Printf("[ROUTER] Registered stream '%s' at path '%s' on port %d\n", stream.Name, pathPrefix, port)
|
fmt.Printf("[ROUTER] Registered transport '%s' at path '%s' on port %d\n", stream.Name, pathPrefix, port)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ func (r *HTTPRouter) UnregisterStream(streamName string) {
|
|||||||
for path, stream := range rs.routes {
|
for path, stream := range rs.routes {
|
||||||
if stream.Name == streamName {
|
if stream.Name == streamName {
|
||||||
delete(rs.routes, path)
|
delete(rs.routes, path)
|
||||||
fmt.Printf("[ROUTER] Unregistered stream '%s' from path '%s' on port %d\n",
|
fmt.Printf("[ROUTER] Unregistered transport '%s' from path '%s' on port %d\n",
|
||||||
streamName, path, port)
|
streamName, path, port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,15 +1,42 @@
|
|||||||
// FILE: src/internal/logstream/logstream.go
|
// FILE: src/internal/service/logstream.go
|
||||||
package logstream
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
|
"logwisp/src/internal/filter"
|
||||||
|
"logwisp/src/internal/monitor"
|
||||||
|
"logwisp/src/internal/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type LogStream struct {
|
||||||
|
Name string
|
||||||
|
Config config.StreamConfig
|
||||||
|
Monitor monitor.Monitor
|
||||||
|
FilterChain *filter.Chain
|
||||||
|
TCPServer *transport.TCPStreamer
|
||||||
|
HTTPServer *transport.HTTPStreamer
|
||||||
|
Stats *StreamStats
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type StreamStats struct {
|
||||||
|
StartTime time.Time
|
||||||
|
MonitorStats monitor.Stats
|
||||||
|
TCPConnections int32
|
||||||
|
HTTPConnections int32
|
||||||
|
TotalBytesServed uint64
|
||||||
|
TotalEntriesServed uint64
|
||||||
|
FilterStats map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
func (ls *LogStream) Shutdown() {
|
func (ls *LogStream) Shutdown() {
|
||||||
// Stop servers first
|
// Stop servers first
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -79,16 +106,36 @@ func (ls *LogStream) GetStats() map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LogStream) UpdateTargets(targets []config.MonitorTarget) error {
|
func (ls *LogStream) UpdateTargets(targets []config.MonitorTarget) error {
|
||||||
// Clear existing targets
|
// Validate new targets first
|
||||||
for _, watcher := range ls.Monitor.GetActiveWatchers() {
|
validatedTargets := make([]config.MonitorTarget, 0, len(targets))
|
||||||
ls.Monitor.RemoveTarget(watcher.Path)
|
for _, target := range targets {
|
||||||
|
// Basic validation
|
||||||
|
absPath, err := filepath.Abs(target.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid target path %s: %w", target.Path, err)
|
||||||
|
}
|
||||||
|
target.Path = absPath
|
||||||
|
validatedTargets = append(validatedTargets, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current watchers
|
||||||
|
oldWatchers := ls.Monitor.GetActiveWatchers()
|
||||||
|
|
||||||
// Add new targets
|
// Add new targets
|
||||||
for _, target := range targets {
|
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 {
|
||||||
return err
|
// Rollback: restore old watchers
|
||||||
|
for _, watcher := range oldWatchers {
|
||||||
|
// Best effort restoration
|
||||||
|
ls.Monitor.AddTarget(watcher.Path, "", false)
|
||||||
}
|
}
|
||||||
|
return fmt.Errorf("failed to add target %s: %w", target.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only remove old targets after new ones are successfully added
|
||||||
|
for _, watcher := range oldWatchers {
|
||||||
|
ls.Monitor.RemoveTarget(watcher.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// FILE: src/internal/config/routerserver.go
|
// FILE: src/internal/service/routerserver.go
|
||||||
package logstream
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -16,7 +16,7 @@ import (
|
|||||||
type routerServer struct {
|
type routerServer struct {
|
||||||
port int
|
port int
|
||||||
server *fasthttp.Server
|
server *fasthttp.Server
|
||||||
routes map[string]*LogStream // path prefix -> stream
|
routes map[string]*LogStream // path prefix -> transport
|
||||||
routeMu sync.RWMutex
|
routeMu sync.RWMutex
|
||||||
router *HTTPRouter
|
router *HTTPRouter
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
@ -38,7 +38,7 @@ func (rs *routerServer) requestHandler(ctx *fasthttp.RequestCtx) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find matching stream
|
// Find matching transport
|
||||||
rs.routeMu.RLock()
|
rs.routeMu.RLock()
|
||||||
var matchedStream *LogStream
|
var matchedStream *LogStream
|
||||||
var matchedPrefix string
|
var matchedPrefix string
|
||||||
@ -68,18 +68,18 @@ func (rs *routerServer) requestHandler(ctx *fasthttp.RequestCtx) {
|
|||||||
|
|
||||||
rs.router.routedRequests.Add(1)
|
rs.router.routedRequests.Add(1)
|
||||||
|
|
||||||
// Route to stream's handler
|
// Route to transport's handler
|
||||||
if matchedStream.HTTPServer != nil {
|
if matchedStream.HTTPServer != nil {
|
||||||
// Save original path
|
// Save original path
|
||||||
originalPath := string(ctx.URI().Path())
|
originalPath := string(ctx.URI().Path())
|
||||||
|
|
||||||
// Rewrite path to remove stream prefix
|
// Rewrite path to remove transport prefix
|
||||||
if remainingPath == "" {
|
if remainingPath == "" {
|
||||||
// Default to stream path if no remaining path
|
// Default to transport path if no remaining path
|
||||||
remainingPath = matchedStream.Config.HTTPServer.StreamPath
|
remainingPath = matchedStream.Config.HTTPServer.StreamPath
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[ROUTER] Routing to stream '%s': %s -> %s\n",
|
fmt.Printf("[ROUTER] Routing to transport '%s': %s -> %s\n",
|
||||||
matchedStream.Name, originalPath, remainingPath)
|
matchedStream.Name, originalPath, remainingPath)
|
||||||
|
|
||||||
ctx.URI().SetPath(remainingPath)
|
ctx.URI().SetPath(remainingPath)
|
||||||
@ -92,7 +92,7 @@ func (rs *routerServer) requestHandler(ctx *fasthttp.RequestCtx) {
|
|||||||
ctx.SetContentType("application/json")
|
ctx.SetContentType("application/json")
|
||||||
json.NewEncoder(ctx).Encode(map[string]string{
|
json.NewEncoder(ctx).Encode(map[string]string{
|
||||||
"error": "Stream HTTP server not available",
|
"error": "Stream HTTP server not available",
|
||||||
"stream": matchedStream.Name,
|
"transport": matchedStream.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ func (rs *routerServer) handleGlobalStatus(ctx *fasthttp.RequestCtx) {
|
|||||||
streamStats["routing"] = map[string]any{
|
streamStats["routing"] = map[string]any{
|
||||||
"path_prefix": prefix,
|
"path_prefix": prefix,
|
||||||
"endpoints": map[string]string{
|
"endpoints": map[string]string{
|
||||||
"stream": prefix + stream.Config.HTTPServer.StreamPath,
|
"transport": prefix + stream.Config.HTTPServer.StreamPath,
|
||||||
"status": prefix + stream.Config.HTTPServer.StatusPath,
|
"status": prefix + stream.Config.HTTPServer.StatusPath,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -148,7 +148,7 @@ func (rs *routerServer) handleNotFound(ctx *fasthttp.RequestCtx) {
|
|||||||
for prefix, stream := range rs.routes {
|
for prefix, stream := range rs.routes {
|
||||||
if stream.Config.HTTPServer != nil {
|
if stream.Config.HTTPServer != nil {
|
||||||
availableRoutes = append(availableRoutes,
|
availableRoutes = append(availableRoutes,
|
||||||
fmt.Sprintf("%s%s (stream: %s)", prefix, stream.Config.HTTPServer.StreamPath, stream.Name),
|
fmt.Sprintf("%s%s (transport: %s)", prefix, stream.Config.HTTPServer.StreamPath, stream.Name),
|
||||||
fmt.Sprintf("%s%s (status: %s)", prefix, stream.Config.HTTPServer.StatusPath, stream.Name),
|
fmt.Sprintf("%s%s (status: %s)", prefix, stream.Config.HTTPServer.StatusPath, stream.Name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
// FILE: src/internal/logstream/service.go
|
// FILE: src/internal/service/service.go
|
||||||
package logstream
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"logwisp/src/internal/filter"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"logwisp/src/internal/config"
|
"logwisp/src/internal/config"
|
||||||
|
"logwisp/src/internal/filter"
|
||||||
"logwisp/src/internal/monitor"
|
"logwisp/src/internal/monitor"
|
||||||
"logwisp/src/internal/stream"
|
"logwisp/src/internal/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@ -21,29 +21,6 @@ type Service struct {
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogStream struct {
|
|
||||||
Name string
|
|
||||||
Config config.StreamConfig
|
|
||||||
Monitor monitor.Monitor
|
|
||||||
FilterChain *filter.Chain
|
|
||||||
TCPServer *stream.TCPStreamer
|
|
||||||
HTTPServer *stream.HTTPStreamer
|
|
||||||
Stats *StreamStats
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamStats struct {
|
|
||||||
StartTime time.Time
|
|
||||||
MonitorStats monitor.Stats
|
|
||||||
TCPConnections int32
|
|
||||||
HTTPConnections int32
|
|
||||||
TotalBytesServed uint64
|
|
||||||
TotalEntriesServed uint64
|
|
||||||
FilterStats map[string]any
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(ctx context.Context) *Service {
|
func New(ctx context.Context) *Service {
|
||||||
serviceCtx, cancel := context.WithCancel(ctx)
|
serviceCtx, cancel := context.WithCancel(ctx)
|
||||||
return &Service{
|
return &Service{
|
||||||
@ -58,10 +35,10 @@ 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("stream '%s' already exists", cfg.Name)
|
return fmt.Errorf("transport '%s' already exists", cfg.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create stream context
|
// Create transport context
|
||||||
streamCtx, streamCancel := context.WithCancel(s.ctx)
|
streamCtx, streamCancel := context.WithCancel(s.ctx)
|
||||||
|
|
||||||
// Create monitor
|
// Create monitor
|
||||||
@ -93,7 +70,7 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
|
|||||||
filterChain = chain
|
filterChain = chain
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create log stream
|
// Create log transport
|
||||||
ls := &LogStream{
|
ls := &LogStream{
|
||||||
Name: cfg.Name,
|
Name: cfg.Name,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
@ -120,7 +97,7 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
|
|||||||
s.filterLoop(streamCtx, rawChan, tcpChan, filterChain)
|
s.filterLoop(streamCtx, rawChan, tcpChan, filterChain)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ls.TCPServer = stream.NewTCPStreamer(tcpChan, *cfg.TCPServer)
|
ls.TCPServer = transport.NewTCPStreamer(tcpChan, *cfg.TCPServer)
|
||||||
|
|
||||||
if err := s.startTCPServer(ls); err != nil {
|
if err := s.startTCPServer(ls); err != nil {
|
||||||
ls.Shutdown()
|
ls.Shutdown()
|
||||||
@ -142,7 +119,7 @@ func (s *Service) CreateStream(cfg config.StreamConfig) error {
|
|||||||
s.filterLoop(streamCtx, rawChan, httpChan, filterChain)
|
s.filterLoop(streamCtx, rawChan, httpChan, filterChain)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ls.HTTPServer = stream.NewHTTPStreamer(httpChan, *cfg.HTTPServer)
|
ls.HTTPServer = transport.NewHTTPStreamer(httpChan, *cfg.HTTPServer)
|
||||||
|
|
||||||
if err := s.startHTTPServer(ls); err != nil {
|
if err := s.startHTTPServer(ls); err != nil {
|
||||||
ls.Shutdown()
|
ls.Shutdown()
|
||||||
@ -187,7 +164,7 @@ func (s *Service) GetStream(name string) (*LogStream, error) {
|
|||||||
|
|
||||||
stream, exists := s.streams[name]
|
stream, exists := s.streams[name]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("stream '%s' not found", name)
|
return nil, fmt.Errorf("transport '%s' not found", name)
|
||||||
}
|
}
|
||||||
return stream, nil
|
return stream, nil
|
||||||
}
|
}
|
||||||
@ -209,7 +186,7 @@ func (s *Service) RemoveStream(name string) error {
|
|||||||
|
|
||||||
stream, exists := s.streams[name]
|
stream, exists := s.streams[name]
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("stream '%s' not found", name)
|
return fmt.Errorf("transport '%s' not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.Shutdown()
|
stream.Shutdown()
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// FILE: src/internal/stream/httpstreamer.go
|
// FILE: src/internal/transport/httpstreamer.go
|
||||||
package stream
|
package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -43,7 +43,7 @@ func NewHTTPStreamer(logChan chan monitor.LogEntry, cfg config.HTTPConfig) *HTTP
|
|||||||
// Set default paths if not configured
|
// Set default paths if not configured
|
||||||
streamPath := cfg.StreamPath
|
streamPath := cfg.StreamPath
|
||||||
if streamPath == "" {
|
if streamPath == "" {
|
||||||
streamPath = "/stream"
|
streamPath = "/transport"
|
||||||
}
|
}
|
||||||
statusPath := cfg.StatusPath
|
statusPath := cfg.StatusPath
|
||||||
if statusPath == "" {
|
if statusPath == "" {
|
||||||
@ -151,7 +151,7 @@ func (h *HTTPStreamer) requestHandler(ctx *fasthttp.RequestCtx) {
|
|||||||
ctx.SetContentType("application/json")
|
ctx.SetContentType("application/json")
|
||||||
json.NewEncoder(ctx).Encode(map[string]any{
|
json.NewEncoder(ctx).Encode(map[string]any{
|
||||||
"error": "Not Found",
|
"error": "Not Found",
|
||||||
"message": fmt.Sprintf("Available endpoints: %s (SSE stream), %s (status)",
|
"message": fmt.Sprintf("Available endpoints: %s (SSE transport), %s (status)",
|
||||||
h.streamPath, h.statusPath),
|
h.streamPath, h.statusPath),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -166,7 +166,7 @@ func (h *HTTPStreamer) handleStream(ctx *fasthttp.RequestCtx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set SSE headers
|
// Set SSE headers
|
||||||
ctx.Response.Header.Set("Content-Type", "text/event-stream")
|
ctx.Response.Header.Set("Content-Type", "text/event-transport")
|
||||||
ctx.Response.Header.Set("Cache-Control", "no-cache")
|
ctx.Response.Header.Set("Cache-Control", "no-cache")
|
||||||
ctx.Response.Header.Set("Connection", "keep-alive")
|
ctx.Response.Header.Set("Connection", "keep-alive")
|
||||||
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
ctx.Response.Header.Set("Access-Control-Allow-Origin", "*")
|
||||||
@ -202,7 +202,7 @@ func (h *HTTPStreamer) handleStream(ctx *fasthttp.RequestCtx) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Define the stream 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",
|
fmt.Printf("[HTTP DEBUG] Client connected on port %d. Count now: %d\n",
|
||||||
@ -336,7 +336,7 @@ func (h *HTTPStreamer) handleStatus(ctx *fasthttp.RequestCtx) {
|
|||||||
"mode": map[string]bool{"standalone": h.standalone, "router": !h.standalone},
|
"mode": map[string]bool{"standalone": h.standalone, "router": !h.standalone},
|
||||||
},
|
},
|
||||||
"endpoints": map[string]string{
|
"endpoints": map[string]string{
|
||||||
"stream": h.streamPath,
|
"transport": h.streamPath,
|
||||||
"status": h.statusPath,
|
"status": h.statusPath,
|
||||||
},
|
},
|
||||||
"features": map[string]any{
|
"features": map[string]any{
|
||||||
@ -361,7 +361,7 @@ func (h *HTTPStreamer) GetActiveConnections() int32 {
|
|||||||
return h.activeClients.Load()
|
return h.activeClients.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the configured stream endpoint path
|
// Returns the configured transport endpoint path
|
||||||
func (h *HTTPStreamer) GetStreamPath() string {
|
func (h *HTTPStreamer) GetStreamPath() string {
|
||||||
return h.streamPath
|
return h.streamPath
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// FILE: src/internal/stream/noop_logger.go
|
// FILE: src/internal/transport/noop_logger.go
|
||||||
package stream
|
package transport
|
||||||
|
|
||||||
// noopLogger implements gnet's Logger interface but discards everything
|
// noopLogger implements gnet's Logger interface but discards everything
|
||||||
type noopLogger struct{}
|
type noopLogger struct{}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// FILE: src/internal/monitor/tcpserver.go
|
// FILE: src/internal/monitor/tcpserver.go
|
||||||
package stream
|
package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -17,7 +17,10 @@ type tcpServer struct {
|
|||||||
|
|
||||||
func (s *tcpServer) OnBoot(eng gnet.Engine) gnet.Action {
|
func (s *tcpServer) OnBoot(eng gnet.Engine) gnet.Action {
|
||||||
// Store engine reference for shutdown
|
// Store engine reference for shutdown
|
||||||
|
s.streamer.engineMu.Lock()
|
||||||
s.streamer.engine = &eng
|
s.streamer.engine = &eng
|
||||||
|
s.streamer.engineMu.Unlock()
|
||||||
|
|
||||||
fmt.Printf("[TCP DEBUG] Server booted on port %d\n", s.streamer.config.Port)
|
fmt.Printf("[TCP DEBUG] Server booted on port %d\n", s.streamer.config.Port)
|
||||||
return gnet.None
|
return gnet.None
|
||||||
}
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
// FILE: src/internal/stream/tcpstreamer.go
|
// FILE: src/internal/transport/tcpstreamer.go
|
||||||
package stream
|
package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -23,6 +23,7 @@ type TCPStreamer struct {
|
|||||||
activeConns atomic.Int32
|
activeConns atomic.Int32
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
engine *gnet.Engine
|
engine *gnet.Engine
|
||||||
|
engineMu sync.Mutex
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
rateLimiter *ratelimit.Limiter
|
rateLimiter *ratelimit.Limiter
|
||||||
}
|
}
|
||||||
@ -84,10 +85,14 @@ func (t *TCPStreamer) Stop() {
|
|||||||
close(t.done)
|
close(t.done)
|
||||||
|
|
||||||
// Stop gnet engine if running
|
// Stop gnet engine if running
|
||||||
if t.engine != nil {
|
t.engineMu.Lock()
|
||||||
|
engine := t.engine
|
||||||
|
t.engineMu.Unlock()
|
||||||
|
|
||||||
|
if engine != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
t.engine.Stop(ctx)
|
(*engine).Stop(ctx) // Dereference the pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for broadcast loop to finish
|
// Wait for broadcast loop to finish
|
||||||
Reference in New Issue
Block a user