A production-ready Go service that acts as a transparent Elasticsearch proxy with intelligent bulk request aggregation. Designed to optimize Elasticsearch performance by batching small bulk requests while transparently proxying all other operations.
Built following Go best practices with the Standard Go Project Layout.
- Smart Bulk Aggregation: Automatically aggregates
/_bulkrequests in memory with per-index buffers - Transparent Proxying: All non-bulk requests pass through unchanged
- Intelligent Request Classification: Distinguishes between bulk writes, searches, reads, maintenance, and other operations
- Time-based Flushing: Configurable flush intervals (default: 3s)
- Size-based Flushing: Automatic flush on size threshold (default: 5MB)
- Backpressure Handling: Returns HTTP 429 when buffer is full
- Retry Logic: Exponential backoff for failed bulk sends
- Rich Prometheus Metrics: Detailed metrics with operation type and HTTP method labels
- Structured Logging: JSON logs using zerolog
- Configuration Management: Flexible config using Viper
- Production Ready: Health checks, graceful shutdown, resource limits
- High Performance: <5ms overhead for non-bulk requests
Following the standard Go project layout:
es-bulk-proxy/
βββ cmd/
β βββ es-bulk-proxy/ # Main application entry point
β βββ main.go
βββ internal/ # Private application code
β βββ buffer/ # Bulk buffer aggregation logic
β β βββ buffer.go
β βββ config/ # Configuration with Viper
β β βββ config.go
β βββ handler/ # HTTP handlers and routing
β β βββ handler.go
β βββ logger/ # Structured logging with zerolog
β β βββ logger.go
β βββ metrics/ # Prometheus metrics
β βββ metrics.go
βββ configs/ # Configuration files
β βββ config.yaml # Example configuration
βββ deployments/ # Deployment configurations
β βββ docker-compose.yml
β βββ kubernetes.yaml
β βββ prometheus.yml
βββ Dockerfile # Multi-stage Docker build
βββ Makefile # Build and deployment commands
βββ go.mod # Go module dependencies
βββ README.md
βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ
β β β β β β
β Zenarmor βββββββββΆ β ES Proxy βββββββββΆ β Elasticsearch β
β β β β β β
βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ
β
β Per-Index
β Buffering
β
βββββββΌβββββββ
β Buffer Mgr β
ββββββββββββββ€
βindex1/_bulkβ
βindex2/_bulkβ
βindex3/_bulkβ
ββββββββββββββ
Key Components:
- Request Router: Classifies requests by operation type (bulk, search, read, maintenance, write, delete)
- Buffer Manager: Maintains separate buffers for each index-specific bulk endpoint
- Per-Index Buffers: Each buffer aggregates requests for its specific index path
- Flush Logic: Time-based and size-based flushing per buffer
- Metrics Collector: Tracks requests by type and method for detailed visibility
- Go 1.25 or higher
- Docker & Docker Compose (for containerized deployment)
- Elasticsearch instance (for testing)
The fastest way to get started with a complete stack:
cd deployments
docker-compose up -dThis starts:
- Elasticsearch on port 9200
- ES Proxy on port 8080
- Prometheus on port 9090
- Grafana on port 3001 (admin/admin) with pre-configured dashboard
Access the Dashboard:
- Grafana Dashboard: http://localhost:3001/d/es-bulk-proxy-dashboard
- Login with
admin/admin
# Clone and navigate
cd es-bulk-proxy
# Download dependencies
go mod download
# Build
make build
# Run with environment variables
ES_URL=http://localhost:9200 ./es-bulk-proxy
# Or using go run
make run# Build image
docker build -t es-bulk-proxy:latest .
# Run container
docker run -d \
-p 8080:8080 \
-e ES_URL=http://elasticsearch:9200 \
--name es-bulk-proxy \
es-bulk-proxy:latestES Proxy supports configuration through:
- Config file (YAML) -
configs/config.yaml - Environment variables - Override config file values
- Defaults - Built-in sensible defaults
Create a configs/config.yaml:
server:
port: "8080"
elasticsearch:
url: "http://elasticsearch:9200"
buffer:
flushinterval: "30s"
maxbatchsize: 5242880 # 5MB
maxbuffersize: 52428800 # 50MB
retry:
attempts: 3
backoffmin: "100ms"All config values can be overridden with environment variables:
| Variable | Description | Default |
|---|---|---|
PORT |
HTTP server port | 8080 |
ES_URL |
Elasticsearch endpoint URL | http://localhost:9200 |
FLUSH_INTERVAL |
Time-based flush interval | 30s |
MAX_BATCH_SIZE |
Size threshold for flushing (bytes) | 5242880 (5MB) |
MAX_BUFFER_SIZE |
Maximum buffer size (bytes) | 52428800 (50MB) |
RETRY_ATTEMPTS |
Number of retry attempts | 3 |
RETRY_BACKOFF_MIN |
Minimum backoff duration | 100ms |
ENVIRONMENT |
Set to development for debug logs with pretty console output |
(production - INFO level JSON logs) |
# Production with custom settings
export ES_URL=https://elasticsearch:9200
export FLUSH_INTERVAL=30s
export MAX_BATCH_SIZE=10485760
export ENVIRONMENT=production
./es-bulk-proxy
# Development mode with pretty console logs
export ENVIRONMENT=development
./es-bulk-proxyBulk write operations (aggregated)
curl -X POST http://localhost:8080/_bulk \
-H "Content-Type: application/x-ndjson" \
-d '{"index":{"_index":"myindex"}}
{"field1":"value1"}
'Response:
{"errors":false}All other Elasticsearch APIs are transparently proxied:
# Search
curl http://localhost:8080/_search
# Cluster health
curl http://localhost:8080/_cluster/health
# Index operations
curl -X PUT http://localhost:8080/myindexHealth check endpoint
curl http://localhost:8080/healthReadiness check endpoint
curl http://localhost:8080/readyPrometheus metrics endpoint
curl http://localhost:8080/metricsAvailable Metrics:
es_proxy_requests_total{type, method}- Total requests by operation type and HTTP methodtype="bulk"- Bulk write operations (/_bulk endpoints)type="search"- Search queries (POST to /_search, /_count)type="read"- Read operations (GET, HEAD requests)type="maintenance"- Index maintenance (/_refresh, /_flush, /_forcemerge)type="write"- Single document writestype="delete"- Delete operations
es_proxy_bulk_batches_total- Number of bulk batches sent to Elasticsearches_proxy_bulk_failures_total- Number of failed bulk sendses_proxy_buffer_size_bytes- Current buffer size in byteses_proxy_latency_seconds{type, method}- Request latency histogram by operation type and method
To use this proxy with Zenarmor:
-
Deploy ES Proxy alongside your Elasticsearch cluster
-
Configure Zenarmor to use the proxy URL:
Instead of: http://elasticsearch:9200 Use: http://es-bulk-proxy:8080 -
Monitor Performance via
/metricsendpoint -
Tune Settings based on your traffic patterns
- Reduced Load: 80-90% fewer requests to Elasticsearch
- Better Throughput: Larger batches = better compression & indexing
- Lower Latency: Fewer round trips to ES cluster
- Index-Aware Buffering: Separate buffers per index maintain context and prevent cross-index conflicts
- Cost Savings: Reduced CPU/memory on ES nodes
/cmd- Main applications for this project/internal- Private application and library code (not importable by external projects)/configs- Configuration file templates or default configs/deployments- Deployment configurations (Docker, Kubernetes, etc.)
# Show all available commands
make help
# Download dependencies
make deps
# Build binary
make build
# Run locally
make run
# Run tests
make test
# Run linters
make lint
# Build Docker image
make docker-build
# Start full dev environment
make dev# Run unit tests
go test -v ./...
# Run integration tests
make integration-test
# Run with coverage
make testcd deployments
docker-compose up -d
# View logs
docker-compose logs -f es-bulk-proxy
# Stop
docker-compose down# Deploy
kubectl apply -f deployments/kubernetes.yaml
# Check status
kubectl get pods -l app=es-bulk-proxy
kubectl get svc es-bulk-proxy
# View logs
kubectl logs -l app=es-bulk-proxy -f
# Port forward for testing
kubectl port-forward svc/es-bulk-proxy 8080:8080
# Scale
kubectl scale deployment es-bulk-proxy --replicas=5
# Delete
kubectl delete -f deployments/kubernetes.yamlThe Kubernetes deployment includes:
- Deployment with 2 replicas
- ClusterIP Service
- Horizontal Pod Autoscaler (2-10 pods)
- ConfigMap for configuration
- ServiceMonitor for Prometheus Operator
- PodDisruptionBudget for high availability
A pre-configured Grafana dashboard is included for comprehensive monitoring:
Access: http://localhost:3001/d/es-bulk-proxy-dashboard (admin/admin)
Dashboard Features:
- π Real-time request rate by type and HTTP method (bulk, search, read, maintenance)
- π Buffer size gauge with thresholds
- π― Success rate and failure tracking
- β±οΈ Latency percentiles (p50, p95, p99) per operation type
- π Bulk batch rate and trends
- π₯§ Request type distribution with method breakdown
Quick Setup:
# Dashboard is auto-provisioned with docker-compose
cd deployments && docker-compose up -d
# Generate test traffic
chmod +x generate-test-traffic.sh
./generate-test-traffic.shSee GRAFANA_DASHBOARD.md for detailed documentation.
The service exposes Prometheus metrics at /metrics. Key metrics to monitor:
# Request rate by type
rate(es_proxy_requests_total[5m])
# Bulk write rate specifically
rate(es_proxy_requests_total{type="bulk"}[5m])
# Search query rate
rate(es_proxy_requests_total{type="search"}[5m])
# Bulk batch rate
rate(es_proxy_bulk_batches_total[5m])
# Error rate
rate(es_proxy_bulk_failures_total[5m])
# Buffer size
es_proxy_buffer_size_bytes
# Latency by operation type
histogram_quantile(0.95, rate(es_proxy_latency_seconds_bucket[5m]))
Import the provided dashboard or create custom dashboards using the metrics above.
Pre-configured Dashboard:
- Located at:
deployments/grafana-dashboard.json - Auto-provisioned when using docker-compose
- Access: http://localhost:3001/d/es-bulk-proxy-dashboard
- Includes 11 panels covering all key metrics
Manual Import:
- Login to Grafana (admin/admin)
- Go to Dashboards β Import
- Upload
deployments/grafana-dashboard.json - Select Prometheus datasource
Cause: Buffer is full (exceeded MAX_BUFFER_SIZE)
Solutions:
# Increase buffer size
export MAX_BUFFER_SIZE=104857600 # 100MB
# Decrease flush interval
export FLUSH_INTERVAL=1s
# Scale horizontally
kubectl scale deployment es-bulk-proxy --replicas=5Cause: Large batches or slow Elasticsearch
Solutions:
# Decrease batch size
export MAX_BATCH_SIZE=2621440 # 2.5MB
# Decrease flush interval
export FLUSH_INTERVAL=1sCause: Elasticsearch unreachable or rejecting requests
Solutions:
# Check metrics
curl http://localhost:8080/metrics | grep bulk_failures
# View logs
docker logs es-bulk-proxy 2>&1 | grep "failed to send bulk"
# Test ES connectivity
curl http://localhost:9200/_cluster/healthTested on 4-core CPU, 8GB RAM:
- Throughput: 1000+ bulk requests/sec
- Latency: <5ms overhead for proxy requests
- Memory: ~100MB under normal load
- CPU: <200m under normal load
- Flush Interval: Lower for faster writes, higher for better batching
- Batch Size: Larger batches = better compression, but higher latency
- Scale Horizontally: Use HPA for high-traffic scenarios
- Monitor Metrics: Watch
buffer_size_bytesfor tuning
- Language: Go 1.25+
- Logging: zerolog - High-performance structured logging
- Configuration: Viper - Flexible configuration management
- Metrics: Prometheus - Production monitoring
- HTTP: Go standard library - Reverse proxy and HTTP server
See LICENSE file in repository root.
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions:
- GitHub Issues: github.com/codifierr/go-scratchpad/issues
Built with β€οΈ for optimizing Elasticsearch bulk operations