Files

331 lines
8.1 KiB
Markdown

# In-Memory Logger
Provides a thread-safe in-memory circular buffer logger with WebSocket-based real-time streaming for log data.
## Overview
The memlogger package implements a thread-safe in-memory log buffer with WebSocket streaming capabilities. It stores log data in memory and pushes new entries to connected WebSocket clients and event subscribers.
### Primary Consumers
- `internal/api/v1/cert/renew` - Provides WebSocket endpoint for certificate renewal logs
- Diagnostic and debugging interfaces
### Non-goals
- Does not persist logs to disk
- Does not provide log rotation or retention policies
- Does not support structured/log levels
- Does not provide authentication for WebSocket connections
### Stability
Internal package. Public interfaces are stable.
## Public API
### Exported Types
#### MemLogger Interface
```go
type MemLogger io.Writer
```
The `MemLogger` is an `io.Writer` interface. Any data written to it is stored in the circular buffer and broadcast to subscribers.
### Exported Functions
#### GetMemLogger
```go
func GetMemLogger() MemLogger
```
Returns the global singleton `MemLogger` instance.
**Example:**
```go
logger := memlogger.GetMemLogger()
logger.Write([]byte("log message"))
```
#### HandlerFunc
```go
func HandlerFunc() gin.HandlerFunc
```
Returns a Gin middleware handler that upgrades HTTP connections to WebSocket and streams log data.
**Example:**
```go
router.GET("/logs/ws", memlogger.HandlerFunc())
```
#### Events
```go
func Events() (<-chan []byte, func())
```
Returns a channel for receiving log events and a cancel function to unsubscribe.
**Returns:**
- `<-chan []byte` - Channel receiving log entry slices
- `func()` - Cleanup function that unsubscribes and closes the channel
**Example:**
```go
ch, cancel := memlogger.Events()
defer cancel()
for event := range ch {
fmt.Println(string(event))
}
```
## Architecture
### Core Components
```mermaid
flowchart LR
subgraph In-Memory Buffer
LB[bytes.Buffer] -->|Stores| Logs[Log Entries 16KB cap]
end
subgraph Notification System
Notify[notifyWS] -->|Notifies| WS[WebSocket Clients]
Notify -->|Notifies| Ch[Event Channels]
end
subgraph External Clients
HTTP[HTTP Request] -->|Upgrades to| WS
API[Events API] -->|Subscribes to| Ch
end
```
| Component | Responsibility |
| -------------- | ------------------------------------------------ |
| `memLogger` | Main struct holding buffer and subscription maps |
| `bytes.Buffer` | Circular buffer for log storage (16KB max) |
| `connChans` | xsync.Map of WebSocket channels |
| `listeners` | xsync.Map of event channels |
### Write Flow
```mermaid
sequenceDiagram
participant Writer
participant MemLogger
participant Buffer
participant Subscribers
Writer->>MemLogger: Write(p)
MemLogger->>Buffer: truncateIfNeeded(n)
Buffer->>Buffer: Truncate to 8KB if needed
Buffer->>Buffer: Write(p)
MemLogger->>MemLogger: writeBuf returns position
MemLogger->>Subscribers: notifyWS(pos, n)
Subscribers->>Subscribers: Send to WebSocket/Listeners
```
### Buffer Behavior
The circular buffer has fixed maximum size:
| Property | Value |
| ------------------ | ---------- |
| Maximum Size | 16 KB |
| Truncate Threshold | 8 KB (50%) |
| Write Chunk Size | 4 KB |
| Write Timeout | 10 seconds |
**Truncation Logic:**
When the buffer exceeds the maximum size:
1. The buffer is truncated to 8 KB (half the maximum)
1. Oldest entries are removed first
1. Recent logs are always preserved
### Thread Safety
Multiple synchronization mechanisms ensure thread safety:
| Field | Mutex Type | Purpose |
| ------------ | -------------- | ------------------------------------- |
| `Buffer` | `sync.RWMutex` | Protecting buffer operations |
| `notifyLock` | `sync.RWMutex` | Protecting notification maps |
| `connChans` | `xsync.Map` | Thread-safe WebSocket channel storage |
| `listeners` | `xsync.Map` | Thread-safe event listener storage |
## Configuration Surface
No explicit configuration. The singleton instance uses fixed constants:
```go
const (
maxMemLogSize = 16 * 1024 // 16KB buffer
truncateSize = maxMemLogSize / 2 // 8KB
initialWriteChunkSize = 4 * 1024
writeTimeout = 10 * time.Second
)
```
## Dependency and Integration Map
### Internal Dependencies
| Dependency | Purpose |
| ------------------------------------------ | -------------------- |
| `github.com/yusing/goutils/http/websocket` | WebSocket management |
### External Dependencies
| Dependency | Purpose |
| ------------------------------- | ------------------------- |
| `github.com/gin-gonic/gin` | HTTP/WebSocket handling |
| `github.com/puzpuzpuz/xsync/v4` | Concurrent map operations |
## Observability
### Logs
No logging in this package. Errors are returned via WebSocket write failures.
### Metrics
None exposed.
## Failure Modes and Recovery
| Failure | Detection | Recovery |
| ----------------------- | ------------------------ | ------------------------- |
| WebSocket write timeout | 3-second timer | Skip subscriber, continue |
| Buffer write error | `writeBuf` returns error | Logged but not returned |
| Subscriber channel full | Channel send timeout | Skip subscriber, continue |
| Buffer exceeds max size | `truncateIfNeeded` | Truncate to 8KB |
### Concurrency Guarantees
- Multiple goroutines can write concurrently
- Multiple WebSocket connections supported
- Subscriptions can be added/removed during operation
- Buffer truncation is atomic
## Usage Examples
### Basic Log Writing
```go
import "github.com/yusing/godoxy/internal/logging/memlogger"
logger := memlogger.GetMemLogger()
// Write a simple message
logger.Write([]byte("Application started\n"))
// Write formatted logs
logger.Write([]byte(fmt.Sprintf("[INFO] Request received: %s\n", path)))
```
### WebSocket Endpoint
```go
import (
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/logging/memlogger"
)
func setupRouter(r *gin.Engine) {
// Real-time log streaming via WebSocket
r.GET("/api/logs/stream", memlogger.HandlerFunc())
}
```
### Subscribing to Log Events
```go
func monitorLogs(ctx context.Context) {
ch, cancel := memlogger.Events()
defer cancel()
for {
select {
case <-ctx.Done():
return
case event := <-ch:
processLogEvent(event)
}
}
}
func processLogEvent(event []byte) {
// Handle the log event
fmt.Printf("Log: %s", string(event))
}
```
### WebSocket Client
```javascript
// Client-side JavaScript
const ws = new WebSocket("ws://localhost:8080/api/logs/stream");
ws.onmessage = (event) => {
console.log("New log entry:", event.data);
};
ws.onclose = () => {
console.log("Log stream disconnected");
};
ws.onerror = (error) => {
console.error("Log stream error:", error);
};
```
### Complete Integration
```go
func setupLogging(r *gin.Engine) *memlogger.MemLogger {
logger := memlogger.GetMemLogger()
// WebSocket endpoint for real-time logs
r.GET("/ws/logs", memlogger.HandlerFunc())
return logger
}
// Elsewhere in the application
func recordRequest(logger memlogger.MemLogger, path string, status int) {
logger.Write([]byte(fmt.Sprintf("[%s] %s - %d\n",
time.Now().Format(time.RFC3339), path, status)))
}
```
## Performance Characteristics
- O(1) write operations (amortized)
- O(n) for truncation where n is buffer size
- WebSocket notifications are non-blocking (3-second timeout)
- Memory usage is bounded at 16KB
## Testing Notes
- Mock by providing a custom `io.Writer` implementation
- Test concurrent writes with goroutines
- Verify truncation behavior
- Test WebSocket upgrade failures
## Related Packages
- `internal/api` - HTTP API endpoints
- `github.com/gin-gonic/gin` - HTTP framework
- `github.com/yusing/goutils/http/websocket` - WebSocket utilities