summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-07 16:32:10 +0200
committerPaul Buetow <paul@buetow.org>2026-02-07 16:32:10 +0200
commit3fd46f3977fb650974e5e936cba362c787c00637 (patch)
treeb49111ddd0b7af4a007bca6a304dba10efcd88ff
reimport this PoC
-rw-r--r--AGENT.md63
-rw-r--r--CLAUDE.md1
-rw-r--r--Magefile.go253
-rw-r--r--README.md1000
-rwxr-xr-xbackfill-historic-data.sh60
-rwxr-xr-xbenchmark-100mb.sh223
-rwxr-xr-xbenchmark-1gb.sh223
-rwxr-xr-xcleanup-benchmark-data.sh88
-rwxr-xr-xcleanup-benchmark-metrics.sh83
-rwxr-xr-xgenerate-test-data.sh46
-rw-r--r--go.mod26
-rw-r--r--go.sum83
-rw-r--r--internal/config/config.go57
-rw-r--r--internal/config/config_test.go52
-rw-r--r--internal/ingester/auto.go145
-rw-r--r--internal/ingester/auto_test.go164
-rw-r--r--internal/ingester/clickhouse.go191
-rw-r--r--internal/ingester/pushgateway.go51
-rw-r--r--internal/ingester/pushgateway_test.go28
-rw-r--r--internal/ingester/remotewrite.go455
-rw-r--r--internal/ingester/remotewrite_test.go210
-rw-r--r--internal/metrics/generator.go85
-rw-r--r--internal/metrics/generator_test.go53
-rw-r--r--internal/metrics/sample.go34
-rw-r--r--internal/metrics/sample_test.go160
-rw-r--r--internal/parser/csv.go101
-rw-r--r--internal/parser/csv_test.go175
-rw-r--r--internal/parser/json.go62
-rw-r--r--internal/parser/json_test.go177
-rw-r--r--internal/parser/parser.go56
-rw-r--r--internal/parser/parser_test.go99
-rw-r--r--internal/parser/tabular_csv.go256
-rw-r--r--internal/parser/tabular_csv_test.go469
-rw-r--r--internal/resolver/dns_resolver.go274
-rw-r--r--internal/resolver/dns_resolver_test.go232
-rw-r--r--internal/version/version.go4
-rw-r--r--internal/watcher/file_watcher.go86
-rw-r--r--logo.pngbin0 -> 145949 bytes
-rwxr-xr-xrun.sh31
-rw-r--r--test-data/watch-clickhouse-test.csv12
-rwxr-xr-xverify-clickhouse.sh52
41 files changed, 5920 insertions, 0 deletions
diff --git a/AGENT.md b/AGENT.md
new file mode 100644
index 0000000..d5c22e7
--- /dev/null
+++ b/AGENT.md
@@ -0,0 +1,63 @@
+Follow ~/git/conf/snippets/go/go-projects.md
+
+## Grafana Dashboard Guidelines
+
+When creating or updating Grafana dashboards:
+
+### Sorting Requirements
+**ALWAYS ensure ALL panels are sorted by value (descending):**
+
+1. **Time Series Panels:**
+ - Add to `options.legend`: `"sortBy": "Last", "sortDesc": true`
+
+2. **Bar Gauge Panels:**
+ - Use `sort_desc()` in PromQL queries
+ - Example: `sort_desc(topk(10, sum by (label) (metric)))`
+
+3. **Pie/Donut Chart Panels:**
+ - Add to `options.legend`: `"sortBy": "Value", "sortDesc": true`
+
+4. **Table Panels:**
+ - Add to `options`: `"sortBy": [{"displayName": "ColumnName", "desc": true}]`
+
+### Example Panel Configuration
+
+**Time Series:**
+```json
+{
+ "type": "timeseries",
+ "options": {
+ "legend": {
+ "displayMode": "table",
+ "placement": "right",
+ "sortBy": "Last",
+ "sortDesc": true,
+ "calcs": ["lastNotNull", "mean", "max"]
+ }
+ }
+}
+```
+
+**Bar Gauge:**
+```json
+{
+ "type": "bargauge",
+ "targets": [{
+ "expr": "sort_desc(topk(15, sum by (cust) (metric)))"
+ }],
+ "options": {
+ "orientation": "horizontal",
+ "displayMode": "gradient"
+ }
+}
+```
+
+**Table:**
+```json
+{
+ "type": "table",
+ "options": {
+ "sortBy": [{"displayName": "Count", "desc": true}]
+ }
+}
+```
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..02d954c
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+Follow AGENT.md
diff --git a/Magefile.go b/Magefile.go
new file mode 100644
index 0000000..3cce6e0
--- /dev/null
+++ b/Magefile.go
@@ -0,0 +1,253 @@
+//go:build mage
+// +build mage
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/magefile/mage/mg"
+ "github.com/magefile/mage/sh"
+)
+
+const (
+ binaryName = "epimetheus"
+ mainPath = "./cmd/epimetheus"
+)
+
+// Default target to run when none is specified
+var Default = Build
+
+// Build compiles the epimetheus binary
+func Build() error {
+ fmt.Println("Building epimetheus...")
+ return sh.RunV("go", "build", "-o", binaryName, mainPath)
+}
+
+// Install installs the binary to $GOPATH/bin
+func Install() error {
+ fmt.Println("Installing epimetheus...")
+ return sh.RunV("go", "install", mainPath)
+}
+
+// Run executes the epimetheus binary in realtime mode
+func Run() error {
+ mg.Deps(Build)
+ fmt.Println("Running epimetheus in realtime mode...")
+ return sh.RunV("./"+binaryName, "-mode=realtime", "-continuous")
+}
+
+// RunHistoric runs epimetheus in historic mode
+func RunHistoric() error {
+ mg.Deps(Build)
+ fmt.Println("Running epimetheus in historic mode (24 hours ago)...")
+ return sh.RunV("./"+binaryName, "-mode=historic", "-hours-ago=24")
+}
+
+// RunAuto runs epimetheus in auto mode with a file
+func RunAuto(file string) error {
+ mg.Deps(Build)
+ if file == "" {
+ return fmt.Errorf("file parameter required: mage RunAuto <file>")
+ }
+ fmt.Printf("Running epimetheus in auto mode with file: %s\n", file)
+ return sh.RunV("./"+binaryName, "-mode=auto", "-file="+file)
+}
+
+// RunWatchClickHouse runs epimetheus in watch mode with ClickHouse ingestion
+func RunWatchClickHouse(file string) error {
+ mg.Deps(Build)
+ if file == "" {
+ file = "test-data/watch-clickhouse-test.csv"
+ }
+ fmt.Printf("Running epimetheus in watch mode with ClickHouse (file: %s)\n", file)
+ return sh.RunV("./"+binaryName, "-mode=watch", "-file="+file, "-metric-name=watch_test",
+ "-clickhouse=http://localhost:8123", "-prometheus=")
+}
+
+// Test runs all tests
+func Test() error {
+ fmt.Println("Running tests...")
+ return sh.RunV("go", "test", "./...", "-v")
+}
+
+// TestCoverage runs tests with coverage report
+func TestCoverage() error {
+ fmt.Println("Running tests with coverage...")
+ if err := sh.RunV("go", "test", "./...", "-cover", "-coverprofile=coverage.out"); err != nil {
+ return err
+ }
+ return sh.RunV("go", "tool", "cover", "-html=coverage.out", "-o", "coverage.html")
+}
+
+// TestRace runs tests with race detector
+func TestRace() error {
+ fmt.Println("Running tests with race detector...")
+ return sh.RunV("go", "test", "./...", "-race", "-v")
+}
+
+// Benchmark runs all benchmarks
+func Benchmark() error {
+ fmt.Println("Running benchmarks...")
+ return sh.RunV("go", "test", "./...", "-bench=.", "-benchmem")
+}
+
+// Lint runs golangci-lint
+func Lint() error {
+ fmt.Println("Running linter...")
+ return sh.RunV("golangci-lint", "run", "./...")
+}
+
+// Fmt formats all Go code
+func Fmt() error {
+ fmt.Println("Formatting code...")
+ return sh.RunV("go", "fmt", "./...")
+}
+
+// Vet runs go vet
+func Vet() error {
+ fmt.Println("Running go vet...")
+ return sh.RunV("go", "vet", "./...")
+}
+
+// Tidy runs go mod tidy
+func Tidy() error {
+ fmt.Println("Tidying dependencies...")
+ return sh.RunV("go", "mod", "tidy")
+}
+
+// Clean removes build artifacts
+func Clean() error {
+ fmt.Println("Cleaning build artifacts...")
+ files := []string{
+ binaryName,
+ "coverage.out",
+ "coverage.html",
+ }
+ for _, f := range files {
+ if err := sh.Rm(f); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ return nil
+}
+
+// Generate runs go generate
+func Generate() error {
+ fmt.Println("Running go generate...")
+ return sh.RunV("go", "generate", "./...")
+}
+
+// Version prints the version
+func Version() error {
+ fmt.Println("Printing version...")
+ mg.Deps(Build)
+ return sh.RunV("./"+binaryName, "-version")
+}
+
+// All runs format, vet, test, and build
+func All() {
+ mg.Deps(Fmt, Vet, Test, Build)
+}
+
+// CI runs the full CI pipeline (format check, vet, test, build)
+func CI() error {
+ fmt.Println("Running CI pipeline...")
+ mg.Deps(Tidy, Vet, Test)
+ return Build()
+}
+
+// Dev starts development mode with port-forwarding
+func Dev() error {
+ mg.Deps(Build)
+ fmt.Println("Starting development mode...")
+ fmt.Println("Setting up port-forward to Pushgateway...")
+
+ // Start port-forward in background
+ portForwardCmd := sh.RunCmd("kubectl", "port-forward", "-n", "monitoring", "svc/pushgateway", "9091:9091")
+ go func() {
+ if err := portForwardCmd(); err != nil {
+ fmt.Printf("Port-forward error: %v\n", err)
+ }
+ }()
+
+ fmt.Println("Running epimetheus in realtime mode...")
+ return sh.RunV("./"+binaryName, "-mode=realtime", "-continuous")
+}
+
+// GenerateTestData creates test data files
+func GenerateTestData() error {
+ fmt.Println("Generating test data...")
+ return sh.RunV("./generate-test-data.sh")
+}
+
+// Backfill runs backfill for the last 48 hours
+func Backfill() error {
+ mg.Deps(Build)
+ fmt.Println("Running backfill (last 48 hours)...")
+ return sh.RunV("./"+binaryName, "-mode=backfill", "-start-hours=48", "-end-hours=0", "-interval=1")
+}
+
+// Benchmark100MB runs the 100MB benchmark
+func Benchmark100MB() error {
+ fmt.Println("Running 100MB benchmark...")
+ return sh.RunV("./benchmark-100mb.sh")
+}
+
+// Benchmark1GB runs the 1GB benchmark
+func Benchmark1GB() error {
+ fmt.Println("Running 1GB benchmark...")
+ return sh.RunV("./benchmark-1gb.sh")
+}
+
+// CleanupBenchmarkData removes benchmark data from Prometheus
+func CleanupBenchmarkData() error {
+ fmt.Println("Cleaning up benchmark data...")
+ return sh.RunV("./cleanup-benchmark-data.sh")
+}
+
+// CleanupBenchmarkMetrics removes benchmark metric files
+func CleanupBenchmarkMetrics() error {
+ fmt.Println("Cleaning up benchmark metric files...")
+ return sh.RunV("./cleanup-benchmark-metrics.sh")
+}
+
+// DeployDashboard deploys the Grafana dashboard
+func DeployDashboard() error {
+ fmt.Println("Deploying Grafana dashboard...")
+ return sh.RunV("./deploy-dashboard.sh")
+}
+
+// Help prints available targets
+func Help() {
+ fmt.Println("Available targets:")
+ fmt.Println(" build - Build the epimetheus binary (default)")
+ fmt.Println(" install - Install the binary to $GOPATH/bin")
+ fmt.Println(" run - Build and run in realtime mode")
+ fmt.Println(" runHistoric - Build and run in historic mode")
+ fmt.Println(" runAuto <file> - Build and run in auto mode with file")
+ fmt.Println(" runWatchClickHouse [file] - Build and run watch mode with ClickHouse (default: test-data/watch-clickhouse-test.csv)")
+ fmt.Println(" test - Run all tests")
+ fmt.Println(" testCoverage - Run tests with coverage report")
+ fmt.Println(" testRace - Run tests with race detector")
+ fmt.Println(" benchmark - Run Go benchmarks")
+ fmt.Println(" lint - Run golangci-lint")
+ fmt.Println(" fmt - Format all Go code")
+ fmt.Println(" vet - Run go vet")
+ fmt.Println(" tidy - Run go mod tidy")
+ fmt.Println(" clean - Remove build artifacts")
+ fmt.Println(" generate - Run go generate")
+ fmt.Println(" version - Print version")
+ fmt.Println(" all - Run fmt, vet, test, and build")
+ fmt.Println(" ci - Run full CI pipeline")
+ fmt.Println(" dev - Start development mode with port-forwarding")
+ fmt.Println(" generateTestData - Generate test data files")
+ fmt.Println(" backfill - Run backfill for last 48 hours")
+ fmt.Println(" benchmark100MB - Run 100MB benchmark")
+ fmt.Println(" benchmark1GB - Run 1GB benchmark")
+ fmt.Println(" cleanupBenchmarkData - Clean up benchmark data from Prometheus")
+ fmt.Println(" cleanupBenchmarkMetrics - Clean up benchmark metric files")
+ fmt.Println(" deployDashboard - Deploy Grafana dashboard")
+ fmt.Println(" help - Print this help message")
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ba10a76
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1000 @@
+<div align="center">
+ <img src="logo.png" alt="Epimetheus Logo" width="400"/>
+</div>
+
+# Epimetheus
+
+A versatile Go tool for pushing metrics to Prometheus with support for both realtime and historic data ingestion.
+
+## Why "Epimetheus"?
+
+In Greek mythology, [Epimetheus](https://en.wikipedia.org/wiki/Epimetheus_(mythology)) is Prometheus's brother, whose name means "afterthought" or "hindsight" (while Prometheus means "forethought"). This name cleverly captures the tool's purpose: bringing data to Prometheus **after** collection, whether it's historic data from hours, days, or weeks ago, or realtime data pushed on-demand.
+
+While Epimetheus is sometimes depicted as foolish in myths (he accepted Pandora's box despite warnings), this tool embraces the "afterthought" aspect productively - it's never too late to bring your metrics home to Prometheus!
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ Epimetheus │
+│ (Metrics Ingestion Tool) │
+│ │
+│ Modes: │
+│ • Realtime - Current metrics (< 5 min old) │
+│ • Historic - Historic metrics (≥ 5 min old) │
+│ • Backfill - Range of historic data │
+│ • Auto - Automatic routing based on timestamp age │
+└─────────────────────────────────────────────────────────────────────────┘
+ │ │
+ │ Realtime Data │ Historic Data
+ │ (via HTTP POST) │ (via Remote Write API)
+ │ Uses "now" timestamp │ Preserves timestamps
+ ▼ ▼
+┌─────────────────────┐ ┌─────────────────────┐
+│ Pushgateway │ │ Prometheus │
+│ (Port 9091) │ │ (Port 9090) │
+│ │ │ │
+│ • Buffers metrics │ │ Remote Write API: │
+│ • Scraped by │──── Scraped ─────▶ │ /api/v1/write │
+│ Prometheus │ every 15-30s │ │
+│ • No timestamp │ │ Feature Required: │
+│ preservation │ │ --enable-feature= │
+│ │ │ remote-write- │
+│ │ │ receiver │
+└─────────────────────┘ └─────────────────────┘
+ │
+ │ Prometheus Query API
+ │ /api/v1/query
+ ▼
+ ┌─────────────────────┐
+ │ Grafana │
+ │ (Port 3000) │
+ │ │
+ │ • Prometheus as │
+ │ datasource │
+ │ • Dashboards: │
+ │ - Epimetheus │
+ │ Test Metrics │
+ │ • Auto-refresh │
+ └─────────────────────┘
+```
+
+### Data Flow
+
+1. **Realtime Path** (for current data):
+ - Epimetheus → Pushgateway (HTTP POST)
+ - Prometheus scrapes Pushgateway periodically
+ - Timestamp = "now" when Prometheus scrapes
+
+2. **Historic Path** (for old data):
+ - Epimetheus → Prometheus Remote Write API (HTTP POST)
+ - Direct write to Prometheus TSDB
+ - Timestamp preserved from original data
+
+3. **Visualization**:
+ - Grafana queries Prometheus
+ - Displays metrics in dashboards
+ - Auto-refresh every 10 seconds
+
+## Overview
+
+**epimetheus** is a standalone binary that:
+- **Generates** realistic example metrics simulating production applications
+- **Pushes** metrics via Pushgateway (realtime) or Remote Write API (historic)
+- **Automatically detects** timestamp age and chooses the optimal ingestion method
+- **Supports** multiple data formats (CSV, JSON) and all Prometheus metric types
+- **Provides** Grafana dashboard for visualizing test metrics
+
+## Quick Start
+
+### 1. Deploy Pushgateway (one-time setup)
+
+The Pushgateway Helm chart is available in the [conf repository](https://codeberg.org/snonux/conf) at `f3s/pushgateway/helm-chart`.
+
+```bash
+# Clone the conf repository if you haven't already
+git clone https://codeberg.org/snonux/conf.git
+cd conf/f3s/pushgateway/helm-chart
+
+# Deploy Pushgateway
+helm upgrade --install pushgateway . -n monitoring --create-namespace
+```
+
+Alternatively, deploy Pushgateway using the official chart:
+
+```bash
+helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
+helm install pushgateway prometheus-community/prometheus-pushgateway -n monitoring --create-namespace
+```
+
+### 2. Run in Realtime Mode
+
+```bash
+# Port-forward Pushgateway
+kubectl port-forward -n monitoring svc/pushgateway 9091:9091 &
+
+# Push test metrics continuously
+cd /home/paul/git/conf/f3s/epimetheus
+./epimetheus -mode=realtime -continuous
+```
+
+The binary pushes metrics every 15 seconds. Press Ctrl+C to stop.
+
+### 3. View Metrics
+
+```bash
+# Pushgateway UI
+open http://localhost:9091
+
+# Prometheus UI
+kubectl port-forward -n monitoring svc/prometheus-kube-prometheus-prometheus 9090:9090 &
+open http://localhost:9090
+```
+
+## Operating Modes
+
+### 👁️ Watch Mode
+Monitor CSV files for changes and push metrics to Prometheus with file modification timestamps.
+
+**Works with ANY CSV format** - automatically detects numeric vs string columns and sanitizes names.
+
+**NEW: Automatic DNS Resolution** - IP addresses are automatically resolved to hostnames for better observability in Grafana.
+
+```bash
+./epimetheus -mode=watch \
+ -file=mydata.csv \
+ -metric-name=myapp \
+ -prometheus=http://localhost:9090/api/v1/write
+```
+
+**Features:**
+- 🔍 **Format-agnostic**: Works with any tabular CSV structure
+- 📊 **Automatic detection**: Numeric columns → metrics, String columns → labels
+- 🏷️ **Name sanitization**: `min(potatoes)`, `avg(time)`, `p99(latency)` → valid metric names
+- 🌐 **DNS Resolution**: IP addresses → hostnames (e.g., `10.50.52.61` → `foo.example.lan`)
+- 💾 **Smart Caching**: In-memory cache prevents redundant DNS lookups
+- ⏱️ **Timestamp preservation**: Uses file modification time
+- 🔄 **Continuous monitoring**: Polls file every 1 second
+- 💪 **Error resilient**: Continues watching despite failures
+- 🎯 **Remote Write**: Pushes to Prometheus (preserves timestamps)
+
+**CSV Format:**
+Works with any tabular CSV:
+- First row: column headers (automatically sanitized)
+- Subsequent rows: data values
+- Column names can be anything: `min(x)`, `avg(y)`, `p99(latency)`, etc.
+
+**Example 1** - Web metrics:
+```csv
+avg(response_time),p99(latency),endpoint,method
+45.2,120.5,/api/users,GET
+52.1,135.8,/api/orders,POST
+```
+
+Generates:
+```promql
+web_avg_response_time{endpoint="/api/users",method="GET"} 45.2
+web_p99_latency{endpoint="/api/users",method="GET"} 120.5
+web_avg_response_time{endpoint="/api/orders",method="POST"} 52.1
+web_p99_latency{endpoint="/api/orders",method="POST"} 135.8
+```
+
+**Example 2** - Food metrics:
+```csv
+min(potatoes),last(coke),avg(price),country,store_type
+5.2,10.5,12.99,USA,grocery
+3.8,8.2,9.99,Canada,convenience
+```
+
+Generates:
+```promql
+food_min_potatoes{country="USA",store_type="grocery"} 5.2
+food_last_coke{country="USA",store_type="grocery"} 10.5
+food_avg_price{country="USA",store_type="grocery"} 12.99
+# ... etc
+```
+
+Each row generates N samples (N = number of numeric columns).
+
+See [CSV-FORMAT-FLEXIBILITY.md](CSV-FORMAT-FLEXIBILITY.md) for more examples.
+
+**Options:**
+- `-file` - CSV file to watch (required)
+- `-metric-name` - Base metric name (required, e.g., `food`, `network`, `database`)
+- `-prometheus` - Prometheus Remote Write URL (default: http://localhost:9090/api/v1/write)
+- `-clickhouse` - ClickHouse HTTP URL (e.g. http://localhost:8123) to also ingest metrics
+- `-clickhouse-table` - ClickHouse table name (default: epimetheus_metrics)
+- `-job` - Job name for metrics (default: example_metrics_pusher)
+- `-resolve-ip-labels` - Additional IP labels to resolve via DNS (default: ip is always resolved)
+
+**ClickHouse Support:**
+Watch mode can ingest to ClickHouse in addition to (or instead of) Prometheus:
+
+```bash
+# Ingest to both Prometheus and ClickHouse
+./epimetheus -mode=watch -file=data.csv -metric-name=myapp \
+ -prometheus=http://localhost:9090/api/v1/write \
+ -clickhouse=http://localhost:8123
+
+# ClickHouse only (use -prometheus= to disable Prometheus)
+./epimetheus -mode=watch -file=test-data/watch-clickhouse-test.csv \
+ -metric-name=watch_test -clickhouse=http://localhost:8123 -prometheus=
+
+# Verify data in ClickHouse
+./verify-clickhouse.sh
+```
+
+**DNS Resolution:**
+By default, the `ip` label is automatically resolved to a hostname. To resolve additional IP labels:
+
+```bash
+./epimetheus -mode=watch \
+ -file=network.csv \
+ -metric-name=network \
+ -resolve-ip-labels=source_ip,dest_ip
+```
+
+This will resolve: `ip` (default) + `source_ip` + `dest_ip`
+
+**Example:**
+- Input: `ip="10.50.52.61"`
+- Output: `ip="foo.example.lan"`
+- Failed lookups: IP remains unchanged
+
+**Documentation:**
+- [DNS-RESOLUTION-FEATURE.md](DNS-RESOLUTION-FEATURE.md) - Complete DNS resolution guide
+- [CSV-FORMAT-FLEXIBILITY.md](CSV-FORMAT-FLEXIBILITY.md) - Works with ANY CSV format
+- [DTAIL-METRICS-EXAMPLE.md](DTAIL-METRICS-EXAMPLE.md) - Detailed dtail.csv example
+
+### 🔄 Realtime Mode (Default)
+Push current metrics to Pushgateway with "now" timestamp.
+
+```bash
+./epimetheus -mode=realtime -continuous
+```
+
+**Options:**
+- `-pushgateway` - Pushgateway URL (default: http://localhost:9091)
+- `-job` - Job name (default: example_metrics_pusher)
+- `-continuous` - Keep pushing every 15 seconds
+
+### ⏰ Historic Mode
+Push a single datapoint from the past using Remote Write API.
+
+```bash
+# Port-forward Prometheus
+kubectl port-forward -n monitoring svc/prometheus-kube-prometheus-prometheus 9090:9090 &
+
+# Push data from 24 hours ago
+./epimetheus -mode=historic -hours-ago=24
+```
+
+**Options:**
+- `-prometheus` - Prometheus URL (default: http://localhost:9090/api/v1/write)
+- `-hours-ago` - Hours in the past (default: 24)
+
+### 📦 Backfill Mode
+Import a range of historic data points.
+
+```bash
+# Backfill last 48 hours with 1-hour intervals
+./epimetheus -mode=backfill -start-hours=48 -end-hours=0 -interval=1
+
+# Backfill last week with 6-hour intervals
+./epimetheus -mode=backfill -start-hours=168 -end-hours=0 -interval=6
+```
+
+**Options:**
+- `-start-hours` - Start time in hours ago
+- `-end-hours` - End time in hours ago (0 = now)
+- `-interval` - Interval between points in hours
+
+### 🤖 Auto Mode (Recommended!)
+Automatically detect timestamp age and route to the correct ingestion method.
+
+```bash
+# Generate test data
+./generate-test-data.sh
+
+# Import mixed current and historic data
+./epimetheus -mode=auto -file=test-all-ages.csv
+```
+
+**Detection Logic:**
+- Data < 5 minutes old → Pushgateway (realtime)
+- Data ≥ 5 minutes old → Remote Write (historic)
+
+**Options:**
+- `-file` - Input file path
+- `-format` - Data format: csv or json (default: csv)
+- `-pushgateway` - Pushgateway URL
+- `-prometheus` - Prometheus Remote Write URL
+
+## Data Formats
+
+### CSV Format
+
+```csv
+# Format: metric_name,labels,value,timestamp_ms
+# Labels: key1=value1;key2=value2
+epimetheus_test_requests_total,instance=web1;env=prod,100,1767125148000
+epimetheus_test_temperature_celsius,instance=web2,22.5,1767038748000
+
+# Timestamp is optional (uses "now" if omitted)
+epimetheus_test_active_connections,instance=web3,42,
+```
+
+### JSON Format
+
+```json
+[
+ {
+ "metric": "epimetheus_test_requests_total",
+ "labels": {"instance": "web1", "env": "prod"},
+ "value": 100,
+ "timestamp_ms": 1767125148000
+ },
+ {
+ "metric": "epimetheus_test_temperature_celsius",
+ "labels": {"instance": "web2"},
+ "value": 22.5,
+ "timestamp_ms": 1767038748000
+ }
+]
+```
+
+## Test Metrics
+
+All generated metrics use the `epimetheus_test_` prefix to clearly identify them as test data.
+
+### Counter: `epimetheus_test_requests_total`
+- **Type:** Counter (monotonically increasing)
+- **Description:** Total number of requests processed
+- **Use case:** Counting total events, requests, errors
+
+### Gauge: `epimetheus_test_active_connections`
+- **Type:** Gauge (can increase or decrease)
+- **Description:** Current number of active connections (0-100)
+- **Use case:** Current state measurements, capacity
+
+### Gauge: `epimetheus_test_temperature_celsius`
+- **Type:** Gauge
+- **Description:** Current temperature in Celsius (0-50°C)
+- **Use case:** Environmental monitoring
+
+### Histogram: `epimetheus_test_request_duration_seconds`
+- **Type:** Histogram (distribution)
+- **Description:** Request duration distribution
+- **Buckets:** 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10 seconds
+- **Use case:** Latency measurements, SLO tracking
+
+### Labeled Counter: `epimetheus_test_jobs_processed_total`
+- **Type:** Counter with labels
+- **Description:** Jobs processed by type and status
+- **Labels:**
+ - `job_type`: email, report, backup
+ - `status`: success, failed
+- **Use case:** Categorized counting, multi-dimensional metrics
+
+## Grafana Dashboard
+
+A comprehensive dashboard is available showcasing all test metrics.
+
+### Dashboard Features
+
+- **8 Panels:**
+ 1. Request Rate (line graph)
+ 2. Total Requests (stat panel)
+ 3. Active Connections (gauge with thresholds)
+ 4. Temperature (gauge with thresholds)
+ 5. Request Duration Histogram (p50, p90, p99)
+ 6. Average Request Duration (stat)
+ 7. Jobs Processed by Type (bar gauge)
+ 8. Jobs Status Breakdown (table)
+
+- **Auto-refresh:** Every 10 seconds
+- **Time range:** Last 15 minutes (customizable)
+- **Dark theme optimized**
+
+### Deploy Dashboard
+
+#### Option 1: Helm/Kubernetes ConfigMap (Recommended)
+
+```bash
+# Deploy via Kubernetes ConfigMap
+kubectl apply -f ../prometheus/epimetheus-dashboard.yaml
+```
+
+The dashboard will be automatically discovered by Grafana.
+
+#### Option 2: Manual Import
+
+```bash
+# Port-forward Grafana
+kubectl port-forward -n monitoring svc/prometheus-grafana 3000:80
+
+# Open Grafana
+open http://localhost:3000
+
+# Go to Dashboards → Import → Upload grafana-dashboard.json
+```
+
+#### Option 3: Automated Script
+
+```bash
+# Deploy via API
+./deploy-dashboard.sh
+
+# Or with custom credentials
+GRAFANA_URL="http://localhost:3000" \
+GRAFANA_USER="admin" \
+GRAFANA_PASSWORD="yourpassword" \
+./deploy-dashboard.sh
+```