Add a simple, read-only web interface to Filebeat for debugging the bbolt registry, allowing inspection of all keys and their values with pagination.
- Configuration - Add
registry.debug_portconfig option - DB Access - Expose bbolt.DB from store for read-only access
- HTTP Server - Lightweight web server in Filebeat beater
- HTML UI - Single-page interface with pagination
- Data Decoder - Parse filestream state structures
File: filebeat/config/config.go:27-49
Add DebugPort to Registry struct:
type Registry struct {
// ... existing fields ...
DebugPort int `config:"debug_port"`
}Update DefaultConfig with default port 8000:
var DefaultConfig = Config{
Registry: Registry{
// ... existing fields ...
DebugPort: 8000,
},
// ...
}Validation: Add check in ValidateConfig() to ensure port is 0 (disabled) or 1024-65535.
File: libbeat/statestore/backend/bbolt/store.go:60
Add public getter method to store struct:
// DB returns the underlying bbolt database for read-only access.
// Returns nil if store is closed.
func (s *store) DB() *bbolt.DB {
s.mu.RLock()
defer s.mu.RUnlock()
if s.closed {
return nil
}
return s.db
}Note: The method uses existing RLock to ensure thread safety. No additional synchronization needed since bbolt handles concurrent readers.
File: libbeat/statestore/backend/bbolt/registry.go
Add method to retrieve DB from named store:
// GetDB returns the underlying bbolt database for a named store.
// Returns nil if store doesn't exist or is closed.
// This is intended for debugging/inspection tools only.
func (r *Registry) GetDB(name string) *bbolt.DB {
r.mu.Lock()
defer r.mu.Unlock()
if !r.active {
return nil
}
s := r.stores[name]
if s == nil {
return nil
}
return s.DB()
}Create new package: filebeat/beater/debug/
Main server implementation with HTTP handlers.
Key structures:
type Server struct {
logger *logp.Logger
port int
registry *bbolt.Registry
server *http.Server
storeName string
}
type PageRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Bucket string `json:"bucket"` // "data" or "metadata"
}
type PageResponse struct {
Keys []KeyValue `json:"keys"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
Bucket string `json:"bucket"`
}
type KeyValue struct {
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Error string `json:"error,omitempty"`
}HTTP Endpoints:
GET /- Serve HTML UIGET /api/keys- Paginated key listing (query params: page, page_size).GET /api/buckets- List available buckets
Implementation notes:
- Use bbolt read-only transactions for all operations
- Default page_size=50, max=1000
- Support both "data" and "metadata" buckets
- Handle pagination by iterating cursor and skipping to offset
- Return raw JSON for values (no decoding beyond JSON parsing)
Embedded HTML template for the UI.
UI Features:
- Bucket selector (data/metadata dropdown)
- Key list with pagination controls (Previous/Next, page input)
- JSON viewer for selected key (expandable/collapsible)
- Auto-refresh option (enabled by default, every 10s)
- Search/filter by key prefix (client-side)
Tech Stack:
- Pure HTML/CSS/JavaScript (no external dependencies)
- JSON syntax highlighting using
<pre>with CSS - Responsive design for mobile/desktop
- Dark mode support (media query)
File: filebeat/beater/filebeat.go
Modify Filebeat struct to include debug server:
type Filebeat struct {
// ... existing fields ...
debugServer *debug.Server
}In New() function, after opening state store:
// Start debug server if enabled
if config.Registry.DebugPort > 0 {
// Extract bbolt registry from filebeatStore
if bboltReg := extractBBoltRegistry(fb.store); bboltReg != nil {
debugServer, err := debug.NewServer(
fb.logger,
config.Registry.DebugPort,
bboltReg,
info.Beat, // store name
)
if err != nil {
fb.logger.Warnf("Failed to start debug server: %v", err)
} else {
fb.debugServer = debugServer
}
}
}Add helper function to extract registry:
func extractBBoltRegistry(store *filebeatStore) *bbolt.Registry {
// Access the underlying backend.Registry
// This requires either:
// 1. Adding a getter to statestore.Registry, or
// 2. Storing bbolt.Registry reference in filebeatStore during creation
// Option 2 is cleaner for this use case
}Note: Requires modifying openStateStore() in filebeat/beater/store.go to return/store the bbolt.Registry reference.
In Stop() function:
if fb.debugServer != nil {
fb.debugServer.Stop()
}File: filebeat/beater/store.go
Modify filebeatStore struct:
type filebeatStore struct {
registry *statestore.Registry
esRegistry *statestore.Registry
storeName string
cleanInterval time.Duration
notifier *es.Notifier
// For debug access
bboltRegistry *bbolt.Registry
}In openStateStore(), store the bbolt registry:
switch cfg.NormalizedType() {
case "bbolt":
reg, err = bbolt.New(logger, bbolt.Settings{...})
if err == nil {
store.bboltRegistry = reg.(*bbolt.Registry) // type assertion
}
// ...
}Add getter:
func (s *filebeatStore) BBoltRegistry() *bbolt.Registry {
return s.bboltRegistry
}File: filebeat/beater/debug/decoder.go
Parse filestream-specific state structures for better display.
// FileStreamState matches filebeat/input/filestream/internal/input-logfile/store.go:493
type FileStreamState struct {
ID string `json:"id"`
Offset int64 `json:"offset"`
TTL time.Duration `json:"ttl"`
Source string `json:"source"`
IdentifierName string `json:"identifier_name"`
}
func DecodeFileStreamState(raw json.RawMessage) (*FileStreamState, error) {
var state FileStreamState
if err := json.Unmarshal(raw, &state); err != nil {
return nil, err
}
return &state, nil
}Enhance KeyValue response to include parsed state when possible:
type KeyValue struct {
Key string `json:"key"`
Value json.RawMessage `json:"value"`
Parsed *FileStreamState `json:"parsed,omitempty"`
Error string `json:"error,omitempty"`
}filebeat/
├── config/
│ └── config.go # Add DebugPort field
├── beater/
│ ├── filebeat.go # Integrate debug server
│ ├── store.go # Store bbolt.Registry reference
│ └── debug/ # New package
│ ├── server.go # HTTP server and handlers
│ ├── html.go # Embedded HTML template
│ └── decoder.go # State structure parsing
libbeat/statestore/backend/bbolt/
├── store.go # Add DB() getter
└── registry.go # Add GetDB(name) method
Use data/registry/filebeat.db as test database:
- Copy to test environment
- Configure
registry.debug_port: 8000 - Start Filebeat:
./filebeat -e - Open browser:
http://localhost:8000 - Verify key listing, pagination, JSON display
- Test bucket switching (data/metadata)
- Verify graceful shutdown on Ctrl+C
# filebeat.yml
filebeat:
registry:
path: registry
debug_port: 8000 # Enable debug UI on port 8000
# Set to 0 to disable (default: 8000)Security Note: Debug server is localhost-only (bind to 127.0.0.1). Document that it should NEVER be exposed to untrusted networks.
- ✓ Configuration option
registry.debug_portworks (default 8000) - ✓ Web UI accessible at
http://localhost:<port> - ✓ Lists all keys from both "data" and "metadata" buckets
- ✓ Pagination works (default 50 items/page)
- ✓ JSON values displayed correctly with syntax highlighting
- ✓ Filestream states parsed and displayed in human-readable format
- ✓ No performance impact when debug server disabled (port=0)
- ✓ Read-only access only (no write/delete operations)
- ✓ Graceful shutdown on Filebeat stop
- ✓ Thread-safe concurrent access
-
statestore.Registry access: Should we add a public
Backend()method tolibbeat/statestore/registry.goto get the underlyingbackend.Registry? Or store it separately infilebeatStore?- Answer: Store separately in
filebeatStorefor cleaner separation and type safety.
- Answer: Store separately in
-
Port binding: Should debug server bind to
127.0.0.1only or allow configuration?- Recommendation: Hardcode to
0.0.0.0for now. Can adddebug_addressconfig later if needed.
- Recommendation: Hardcode to
-
Multiple stores: Filebeat uses
info.Beatas store name (typically "filebeat"). Should UI support multiple stores?- Answer: No, single store is sufficient for initial implementation. Can extend later.
-
Metadata bucket display: Should we decode the metadata (last_access, last_change) or show raw JSON?
- Recommendation: Parse and show human-readable RFC3339
-
UI framework: Pure HTML/JS or use template engine?
- Answer: Pure HTML/JS embedded as string constant. No external dependencies. Simpler for OSS project.
- Store reference cheaply stored in
filebeatStorestruct (8 bytes pointer) - DB access method uses existing locks, no additional overhead
- HTTP server runs only when
debug_port > 0 - No goroutines spawned unless server enabled
- Read-only transactions have minimal lock contention with writers
- Pagination prevents memory spikes from large key sets
- Add configuration field and validation
- Expose DB from bbolt store/registry
- Implement debug server package (server, handlers, HTML)
- Integrate with filebeat beater
- Add filestream decoder
- Write tests
- Manual testing with real registry
- Documentation
Estimated time: 4-6 hours of focused development.