summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-12-30 22:29:41 +0200
committerPaul Buetow <paul@buetow.org>2025-12-30 22:29:41 +0200
commit24592b36da26e7c6ef30aca3017f9da6ceb2f086 (patch)
tree231c6abb8cdb5c2e9d56d708bebbf55c57b72fa2
parent88075b925598f438d15a352364ce17c302a21351 (diff)
Refactor prometheus-pusher following Go best practices
Major refactoring to improve code organization and follow Go conventions: - Moved main entry point to cmd/prometheus-pusher/main.go - Organized code into internal packages (config, metrics, parser, ingester, version) - Implemented proper dependency injection (no package-level variables) - Added context.Context to all blocking operations - Used value semantics where feasible (Sample, Config, Ingesters) - Proper error wrapping with %w throughout - All functions under 50 lines, focused and single-purpose - Consistent ordering: constants, types, constructors, public, private - Added -version flag to display version from internal/version package Package structure: - cmd/prometheus-pusher: Main entry point with flag parsing and mode routing - internal/config: Configuration types and constants - internal/version: Version constant (0.0.0) - internal/metrics: Sample type and Collectors for metric generation - internal/parser: CSV/JSON parsers with context support - internal/ingester: Pushgateway, RemoteWrite, and Auto ingesters All modes tested and working: realtime, historic, backfill, auto šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
-rw-r--r--f3s/prometheus-pusher/auto-ingest.go371
-rw-r--r--f3s/prometheus-pusher/cmd/prometheus-pusher/main.go193
-rw-r--r--f3s/prometheus-pusher/historic.go217
-rw-r--r--f3s/prometheus-pusher/internal/config/config.go49
-rw-r--r--f3s/prometheus-pusher/internal/ingester/auto.go131
-rw-r--r--f3s/prometheus-pusher/internal/ingester/pushgateway.go51
-rw-r--r--f3s/prometheus-pusher/internal/ingester/remotewrite.go252
-rw-r--r--f3s/prometheus-pusher/internal/metrics/generator.go83
-rw-r--r--f3s/prometheus-pusher/internal/metrics/sample.go34
-rw-r--r--f3s/prometheus-pusher/internal/parser/csv.go101
-rw-r--r--f3s/prometheus-pusher/internal/parser/json.go62
-rw-r--r--f3s/prometheus-pusher/internal/parser/parser.go56
-rw-r--r--f3s/prometheus-pusher/internal/version/version.go4
-rw-r--r--f3s/prometheus-pusher/main.go91
-rwxr-xr-xf3s/prometheus-pusher/prometheus-pusherbin14022801 -> 14053904 bytes
-rw-r--r--f3s/prometheus-pusher/realtime.go100
16 files changed, 1016 insertions, 779 deletions
diff --git a/f3s/prometheus-pusher/auto-ingest.go b/f3s/prometheus-pusher/auto-ingest.go
deleted file mode 100644
index 0daff20..0000000
--- a/f3s/prometheus-pusher/auto-ingest.go
+++ /dev/null
@@ -1,371 +0,0 @@
-package main
-
-import (
- "bufio"
- "bytes"
- "encoding/csv"
- "encoding/json"
- "fmt"
- "io"
- "log"
- "net/http"
- "os"
- "strconv"
- "strings"
- "time"
-
- "github.com/golang/snappy"
- "github.com/prometheus/prometheus/prompb"
-)
-
-// MetricSample represents a single metric sample with timestamp
-type MetricSample struct {
- MetricName string
- Labels map[string]string
- Value float64
- Timestamp time.Time
-}
-
-// IngestMode represents the ingestion strategy
-type IngestMode string
-
-const (
- ModeRealtime IngestMode = "realtime" // Use Pushgateway (current data)
- ModeHistoric IngestMode = "historic" // Use Remote Write (old data)
-)
-
-// DetermineIngestMode automatically determines which ingestion mode to use
-// based on the age of the timestamp
-func DetermineIngestMode(timestamp time.Time) IngestMode {
- age := time.Since(timestamp)
-
- // Threshold: data older than 5 minutes uses historic mode
- // This allows for some clock skew and processing delay
- threshold := 5 * time.Minute
-
- if age > threshold {
- return ModeHistoric
- }
- return ModeRealtime
-}
-
-// ParseCSVMetrics parses metrics from CSV format
-// Expected format: metric_name,label1=value1;label2=value2,value,timestamp_unix_ms
-// Example: app_requests_total,instance=web1;env=prod,42,1735516800000
-func ParseCSVMetrics(reader io.Reader) ([]MetricSample, error) {
- var samples []MetricSample
-
- csvReader := csv.NewReader(reader)
- csvReader.Comment = '#'
-
- lineNum := 0
- for {
- record, err := csvReader.Read()
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, fmt.Errorf("line %d: %w", lineNum, err)
- }
- lineNum++
-
- if len(record) < 3 {
- log.Printf("Warning: line %d: skipping invalid record (need at least 3 fields)", lineNum)
- continue
- }
-
- // Parse metric name
- metricName := strings.TrimSpace(record[0])
- if metricName == "" {
- log.Printf("Warning: line %d: skipping empty metric name", lineNum)
- continue
- }
-
- // Parse labels
- labels := make(map[string]string)
- if len(record) > 1 && record[1] != "" {
- labelPairs := strings.Split(record[1], ";")
- for _, pair := range labelPairs {
- parts := strings.SplitN(pair, "=", 2)
- if len(parts) == 2 {
- labels[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
- }
- }
- }
-
- // Parse value
- value, err := strconv.ParseFloat(strings.TrimSpace(record[2]), 64)
- if err != nil {
- log.Printf("Warning: line %d: skipping invalid value: %v", lineNum, err)
- continue
- }
-
- // Parse timestamp (optional, defaults to now)
- var timestamp time.Time
- if len(record) > 3 && record[3] != "" {
- timestampMs, err := strconv.ParseInt(strings.TrimSpace(record[3]), 10, 64)
- if err != nil {
- log.Printf("Warning: line %d: invalid timestamp, using current time: %v", lineNum, err)
- timestamp = time.Now()
- } else {
- timestamp = time.UnixMilli(timestampMs)
- }
- } else {
- timestamp = time.Now()
- }
-
- samples = append(samples, MetricSample{
- MetricName: metricName,
- Labels: labels,
- Value: value,
- Timestamp: timestamp,
- })
- }
-
- return samples, nil
-}
-
-// ParseJSONMetrics parses metrics from JSON format
-// Expected format: array of {metric: string, labels: {}, value: number, timestamp_ms: number}
-func ParseJSONMetrics(reader io.Reader) ([]MetricSample, error) {
- var rawSamples []struct {
- Metric string `json:"metric"`
- Labels map[string]string `json:"labels"`
- Value float64 `json:"value"`
- TimestampMs int64 `json:"timestamp_ms,omitempty"`
- }
-
- decoder := json.NewDecoder(reader)
- if err := decoder.Decode(&rawSamples); err != nil {
- return nil, fmt.Errorf("failed to parse JSON: %w", err)
- }
-
- var samples []MetricSample
- for i, raw := range rawSamples {
- timestamp := time.Now()
- if raw.TimestampMs > 0 {
- timestamp = time.UnixMilli(raw.TimestampMs)
- }
-
- if raw.Metric == "" {
- log.Printf("Warning: sample %d: skipping empty metric name", i)
- continue
- }
-
- if raw.Labels == nil {
- raw.Labels = make(map[string]string)
- }
-
- samples = append(samples, MetricSample{
- MetricName: raw.Metric,
- Labels: raw.Labels,
- Value: raw.Value,
- Timestamp: timestamp,
- })
- }
-
- return samples, nil
-}
-
-// AutoIngestMetrics automatically ingests metrics using the appropriate method
-// based on timestamp age
-func AutoIngestMetrics(samples []MetricSample, pushgatewayURL, prometheusURL, jobName string) error {
- if len(samples) == 0 {
- return fmt.Errorf("no samples to ingest")
- }
-
- // Group samples by ingestion mode
- realtimeSamples := make([]MetricSample, 0)
- historicSamples := make([]MetricSample, 0)
-
- for _, sample := range samples {
- mode := DetermineIngestMode(sample.Timestamp)
- if mode == ModeRealtime {
- realtimeSamples = append(realtimeSamples, sample)
- } else {
- historicSamples = append(historicSamples, sample)
- }
- }
-
- log.Printf("šŸ“Š Auto-ingest summary:")
- log.Printf(" Total samples: %d", len(samples))
- log.Printf(" Realtime samples (< 5min old): %d", len(realtimeSamples))
- log.Printf(" Historic samples (> 5min old): %d", len(historicSamples))
-
- // Ingest realtime samples via Pushgateway
- if len(realtimeSamples) > 0 {
- log.Printf("\nšŸ”„ Ingesting %d REALTIME samples via Pushgateway...", len(realtimeSamples))
- if err := ingestViaPushgateway(realtimeSamples, pushgatewayURL, jobName); err != nil {
- return fmt.Errorf("failed to ingest realtime samples: %w", err)
- }
- log.Printf("āœ… Successfully ingested %d realtime samples", len(realtimeSamples))
- }
-
- // Ingest historic samples via Remote Write
- if len(historicSamples) > 0 {
- log.Printf("\nā° Ingesting %d HISTORIC samples via Remote Write...", len(historicSamples))
- for i, sample := range historicSamples {
- age := time.Since(sample.Timestamp)
- log.Printf(" [%d/%d] %s (age: %s)", i+1, len(historicSamples), sample.MetricName, formatDuration(age))
- }
-
- if err := ingestViaRemoteWrite(historicSamples, prometheusURL); err != nil {
- return fmt.Errorf("failed to ingest historic samples: %w", err)
- }
- log.Printf("āœ… Successfully ingested %d historic samples", len(historicSamples))
- }
-
- log.Printf("\nšŸŽ‰ Auto-ingest complete!")
- return nil
-}
-
-// ingestViaPushgateway ingests samples using Pushgateway (for realtime data)
-// Note: Pushgateway doesn't preserve timestamps, so this is only for current data
-func ingestViaPushgateway(samples []MetricSample, pushgatewayURL, jobName string) error {
- log.Printf(" Note: Pushgateway ingestion uses current timestamp (original timestamps ignored)")
- log.Printf(" Samples will appear with 'now' timestamp in Prometheus")
-
- // We use the existing pushMetrics function for realtime data
- // Since Pushgateway doesn't support custom timestamps, we just push current values
- simulateMetrics() // Generate current metrics
- return pushMetrics(pushgatewayURL, jobName)
-}
-
-// ingestViaRemoteWrite ingests samples using Remote Write API (preserves timestamps)
-func ingestViaRemoteWrite(samples []MetricSample, prometheusURL string) error {
- var timeSeries []prompb.TimeSeries
-
- for _, sample := range samples {
- labels := []prompb.Label{
- {Name: "__name__", Value: sample.MetricName},
- }
-
- for k, v := range sample.Labels {
- labels = append(labels, prompb.Label{Name: k, Value: v})
- }
-
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: labels,
- Samples: []prompb.Sample{
- {
- Value: sample.Value,
- Timestamp: sample.Timestamp.UnixMilli(),
- },
- },
- })
- }
-
- writeRequest := &prompb.WriteRequest{
- Timeseries: timeSeries,
- }
-
- return sendRemoteWrite(prometheusURL, writeRequest)
-}
-
-// formatDuration formats a duration in human-readable form
-func formatDuration(d time.Duration) string {
- if d < time.Minute {
- return fmt.Sprintf("%.0f seconds", d.Seconds())
- } else if d < time.Hour {
- return fmt.Sprintf("%.0f minutes", d.Minutes())
- } else if d < 24*time.Hour {
- return fmt.Sprintf("%.1f hours", d.Hours())
- } else {
- return fmt.Sprintf("%.1f days", d.Hours()/24)
- }
-}
-
-// AutoIngestFromFile reads a file and automatically ingests metrics
-func AutoIngestFromFile(filename, format, pushgatewayURL, prometheusURL, jobName string) error {
- file, err := os.Open(filename)
- if err != nil {
- return fmt.Errorf("failed to open file: %w", err)
- }
- defer file.Close()
-
- log.Printf("šŸ“ Reading metrics from: %s (format: %s)", filename, format)
-
- var samples []MetricSample
-
- switch format {
- case "csv":
- samples, err = ParseCSVMetrics(file)
- case "json":
- samples, err = ParseJSONMetrics(file)
- default:
- return fmt.Errorf("unsupported format: %s (use csv or json)", format)
- }
-
- if err != nil {
- return fmt.Errorf("failed to parse metrics: %w", err)
- }
-
- if len(samples) == 0 {
- return fmt.Errorf("no valid samples found in file")
- }
-
- return AutoIngestMetrics(samples, pushgatewayURL, prometheusURL, jobName)
-}
-
-// AutoIngestFromStdin reads metrics from stdin and automatically ingests them
-func AutoIngestFromStdin(format, pushgatewayURL, prometheusURL, jobName string) error {
- log.Printf("šŸ“„ Reading metrics from stdin (format: %s)", format)
- log.Printf(" Enter metrics, then press Ctrl+D when done")
-
- var samples []MetricSample
- var err error
-
- reader := bufio.NewReader(os.Stdin)
-
- switch format {
- case "csv":
- samples, err = ParseCSVMetrics(reader)
- case "json":
- samples, err = ParseJSONMetrics(reader)
- default:
- return fmt.Errorf("unsupported format: %s (use csv or json)", format)
- }
-
- if err != nil {
- return fmt.Errorf("failed to parse metrics: %w", err)
- }
-
- if len(samples) == 0 {
- return fmt.Errorf("no valid samples found")
- }
-
- return AutoIngestMetrics(samples, pushgatewayURL, prometheusURL, jobName)
-}
-
-// Helper function to send remote write request (reuses code from historic.go)
-func sendRemoteWrite(prometheusURL string, writeRequest *prompb.WriteRequest) error {
- data, err := writeRequest.Marshal()
- if err != nil {
- return fmt.Errorf("failed to marshal write request: %w", err)
- }
-
- compressed := snappy.Encode(nil, data)
-
- req, err := http.NewRequest("POST", prometheusURL, bytes.NewReader(compressed))
- if err != nil {
- return fmt.Errorf("failed to create HTTP request: %w", err)
- }
-
- req.Header.Set("Content-Type", "application/x-protobuf")
- req.Header.Set("Content-Encoding", "snappy")
- req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
-
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Do(req)
- if err != nil {
- return fmt.Errorf("failed to send remote write request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
- body, _ := io.ReadAll(resp.Body)
- return fmt.Errorf("remote write failed with status %d: %s", resp.StatusCode, string(body))
- }
-
- return nil
-}
diff --git a/f3s/prometheus-pusher/cmd/prometheus-pusher/main.go b/f3s/prometheus-pusher/cmd/prometheus-pusher/main.go
new file mode 100644
index 0000000..905efa1
--- /dev/null
+++ b/f3s/prometheus-pusher/cmd/prometheus-pusher/main.go
@@ -0,0 +1,193 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "math/rand"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "prometheus-pusher/internal/config"
+ "prometheus-pusher/internal/ingester"
+ "prometheus-pusher/internal/metrics"
+ "prometheus-pusher/internal/parser"
+ "prometheus-pusher/internal/version"
+)
+
+func main() {
+ cfg := parseFlags()
+
+ rand.Seed(time.Now().UnixNano())
+
+ ctx, cancel := createContextWithSignalHandler()
+ defer cancel()
+
+ if err := run(ctx, cfg); err != nil {
+ log.Fatalf("Error: %v", err)
+ }
+}
+
+// parseFlags parses command-line flags and returns a Config.
+func parseFlags() config.Config {
+ cfg := config.NewConfig()
+
+ showVersion := flag.Bool("version", false, "Print version and exit")
+ mode := flag.String("mode", "realtime", "Mode: realtime, historic, backfill, or auto")
+ pushgatewayURL := flag.String("pushgateway", cfg.PushgatewayURL, "Pushgateway URL for realtime mode")
+ prometheusURL := flag.String("prometheus", cfg.PrometheusURL, "Prometheus remote write URL for historic mode")
+ jobName := flag.String("job", cfg.JobName, "Job name for metrics")
+ continuous := flag.Bool("continuous", false, "For realtime mode: push continuously every 15s")
+
+ hoursAgo := flag.Int("hours-ago", cfg.HoursAgo, "For historic mode: how many hours ago (single datapoint)")
+ startHours := flag.Int("start-hours", cfg.StartHours, "For backfill: start time in hours ago")
+ endHours := flag.Int("end-hours", cfg.EndHours, "For backfill: end time in hours ago")
+ interval := flag.Int("interval", cfg.Interval, "For backfill: interval between datapoints in hours")
+
+ inputFile := flag.String("file", "", "For auto mode: input file with metrics")
+ inputFormat := flag.String("format", cfg.InputFormat, "For auto mode: input format (csv or json)")
+
+ flag.Parse()
+
+ if *showVersion {
+ fmt.Printf("prometheus-pusher version %s\n", version.Version)
+ os.Exit(0)
+ }
+
+ cfg.Mode = config.Mode(*mode)
+ cfg.PushgatewayURL = *pushgatewayURL
+ cfg.PrometheusURL = *prometheusURL
+ cfg.JobName = *jobName
+ cfg.Continuous = *continuous
+ cfg.HoursAgo = *hoursAgo
+ cfg.StartHours = *startHours
+ cfg.EndHours = *endHours
+ cfg.Interval = *interval
+ cfg.InputFile = *inputFile
+ cfg.InputFormat = *inputFormat
+
+ return cfg
+}
+
+// createContextWithSignalHandler creates a context that cancels on interrupt signals.
+func createContextWithSignalHandler() (context.Context, context.CancelFunc) {
+ ctx, cancel := context.WithCancel(context.Background())
+
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
+
+ go func() {
+ <-sigChan
+ log.Printf("\nReceived interrupt signal, shutting down...")
+ cancel()
+ }()
+
+ return ctx, cancel
+}
+
+// run executes the appropriate mode based on configuration.
+func run(ctx context.Context, cfg config.Config) error {
+ switch cfg.Mode {
+ case config.ModeRealtime:
+ return runRealtimeMode(ctx, cfg)
+ case config.ModeHistoric:
+ return runHistoricMode(ctx, cfg)
+ case config.ModeBackfill:
+ return runBackfillMode(ctx, cfg)
+ case config.ModeAuto:
+ return runAutoMode(ctx, cfg)
+ default:
+ return fmt.Errorf("unknown mode: %s (use realtime, historic, backfill, or auto)", cfg.Mode)
+ }
+}
+
+// runRealtimeMode runs the realtime ingestion mode.
+func runRealtimeMode(ctx context.Context, cfg config.Config) error {
+ log.Printf("Starting Prometheus metrics pusher in REALTIME mode")
+ log.Printf("Pushgateway URL: %s", cfg.PushgatewayURL)
+ log.Printf("Job name: %s", cfg.JobName)
+
+ collectors := metrics.NewCollectors()
+ pushgateway := ingester.NewPushgatewayIngester()
+
+ if err := pushgateway.Ingest(ctx, collectors, cfg.PushgatewayURL, cfg.JobName); err != nil {
+ return fmt.Errorf("failed to push metrics: %w", err)
+ }
+ log.Printf("Successfully pushed metrics to Pushgateway")
+
+ if cfg.Continuous {
+ return runContinuousMode(ctx, pushgateway, collectors, cfg)
+ }
+
+ return nil
+}
+
+// runContinuousMode pushes metrics continuously every 15 seconds.
+func runContinuousMode(ctx context.Context, pushgateway ingester.PushgatewayIngester, collectors metrics.Collectors, cfg config.Config) error {
+ ticker := time.NewTicker(15 * time.Second)
+ defer ticker.Stop()
+
+ log.Printf("Continuous mode: pushing metrics every 15 seconds. Press Ctrl+C to stop.")
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-ticker.C:
+ if err := pushgateway.Ingest(ctx, collectors, cfg.PushgatewayURL, cfg.JobName); err != nil {
+ log.Printf("Error pushing metrics: %v", err)
+ } else {
+ log.Printf("Successfully pushed metrics to Pushgateway")
+ }
+ }
+ }
+}
+
+// runHistoricMode runs the historic ingestion mode.
+func runHistoricMode(ctx context.Context, cfg config.Config) error {
+ remoteWrite := ingester.NewRemoteWriteIngester()
+ return remoteWrite.IngestHistoric(ctx, cfg.PrometheusURL, cfg.HoursAgo)
+}
+
+// runBackfillMode runs the backfill ingestion mode.
+func runBackfillMode(ctx context.Context, cfg config.Config) error {
+ remoteWrite := ingester.NewRemoteWriteIngester()
+ return remoteWrite.Backfill(ctx, cfg.PrometheusURL, cfg.StartHours, cfg.EndHours, cfg.Interval)
+}
+
+// runAutoMode runs the auto ingestion mode.
+func runAutoMode(ctx context.Context, cfg config.Config) error {
+ log.Printf("šŸ¤– AUTO mode: Automatically detecting timestamp age and choosing ingestion method")
+
+ samples, err := loadSamples(ctx, cfg)
+ if err != nil {
+ return err
+ }
+
+ logFileSource(cfg)
+
+ collectors := metrics.NewCollectors()
+ autoIngester := ingester.NewAutoIngester(collectors)
+
+ return autoIngester.Ingest(ctx, samples, cfg)
+}
+
+// loadSamples loads samples from file or stdin based on configuration.
+func loadSamples(ctx context.Context, cfg config.Config) ([]metrics.Sample, error) {
+ if cfg.InputFile != "" {
+ return parser.ParseFile(ctx, cfg.InputFile, cfg.InputFormat)
+ }
+ return parser.ParseStdin(ctx, cfg.InputFormat)
+}
+
+// logFileSource logs the source of the input data.
+func logFileSource(cfg config.Config) {
+ if cfg.InputFile != "" {
+ log.Printf("šŸ“ Reading metrics from: %s (format: %s)", cfg.InputFile, cfg.InputFormat)
+ } else {
+ log.Printf("šŸ“„ Reading metrics from stdin (format: %s)", cfg.InputFormat)
+ }
+}
diff --git a/f3s/prometheus-pusher/historic.go b/f3s/prometheus-pusher/historic.go
deleted file mode 100644
index 66a2ae2..0000000
--- a/f3s/prometheus-pusher/historic.go
+++ /dev/null
@@ -1,217 +0,0 @@
-package main
-
-import (
- "bytes"
- "fmt"
- "io"
- "log"
- "math/rand"
- "net/http"
- "time"
-
- "github.com/golang/snappy"
- "github.com/prometheus/prometheus/prompb"
-)
-
-// GenerateHistoricMetrics generates metric samples for a specific time in the past
-// hoursAgo: how many hours in the past to generate data for
-func GenerateHistoricMetrics(hoursAgo int) []prompb.TimeSeries {
- timestamp := time.Now().Add(-time.Duration(hoursAgo) * time.Hour).UnixMilli()
-
- var timeSeries []prompb.TimeSeries
-
- // Counter: app_requests_total
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_requests_total"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- },
- Samples: []prompb.Sample{
- {Value: float64(rand.Intn(100) + 1), Timestamp: timestamp},
- },
- })
-
- // Gauge: app_active_connections
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_active_connections"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- },
- Samples: []prompb.Sample{
- {Value: float64(rand.Intn(100)), Timestamp: timestamp},
- },
- })
-
- // Gauge: app_temperature_celsius
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_temperature_celsius"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- },
- Samples: []prompb.Sample{
- {Value: 15 + rand.Float64()*20, Timestamp: timestamp},
- },
- })
-
- // Histogram buckets: app_request_duration_seconds
- buckets := []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10}
- cumulativeCount := 0
- for _, bucket := range buckets {
- cumulativeCount += rand.Intn(5)
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_request_duration_seconds_bucket"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- {Name: "le", Value: fmt.Sprintf("%g", bucket)},
- },
- Samples: []prompb.Sample{
- {Value: float64(cumulativeCount), Timestamp: timestamp},
- },
- })
- }
-
- // +Inf bucket
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_request_duration_seconds_bucket"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- {Name: "le", Value: "+Inf"},
- },
- Samples: []prompb.Sample{
- {Value: float64(cumulativeCount), Timestamp: timestamp},
- },
- })
-
- // Histogram sum
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_request_duration_seconds_sum"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- },
- Samples: []prompb.Sample{
- {Value: rand.Float64() * 100, Timestamp: timestamp},
- },
- })
-
- // Histogram count
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_request_duration_seconds_count"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- },
- Samples: []prompb.Sample{
- {Value: float64(cumulativeCount), Timestamp: timestamp},
- },
- })
-
- // Labeled counters: app_jobs_processed_total
- jobTypes := []string{"email", "report", "backup"}
- statuses := []string{"success", "failed"}
- for _, jobType := range jobTypes {
- for _, status := range statuses {
- timeSeries = append(timeSeries, prompb.TimeSeries{
- Labels: []prompb.Label{
- {Name: "__name__", Value: "app_jobs_processed_total"},
- {Name: "instance", Value: "example-app"},
- {Name: "job", Value: "historic_data"},
- {Name: "job_type", Value: jobType},
- {Name: "status", Value: status},
- },
- Samples: []prompb.Sample{
- {Value: float64(rand.Intn(20)), Timestamp: timestamp},
- },
- })
- }
- }
-
- return timeSeries
-}
-
-// PushHistoricData sends historic data to Prometheus via Remote Write API
-// prometheusURL: URL of Prometheus remote write endpoint (e.g., "http://localhost:9090/api/v1/write")
-// hoursAgo: how many hours in the past to generate data for
-func PushHistoricData(prometheusURL string, hoursAgo int) error {
- // Generate historic metrics
- timeSeries := GenerateHistoricMetrics(hoursAgo)
-
- // Create write request
- writeRequest := &prompb.WriteRequest{
- Timeseries: timeSeries,
- }
-
- // Marshal to protobuf
- data, err := writeRequest.Marshal()
- if err != nil {
- return fmt.Errorf("failed to marshal write request: %w", err)
- }
-
- // Compress with snappy
- compressed := snappy.Encode(nil, data)
-
- // Send HTTP POST request
- req, err := http.NewRequest("POST", prometheusURL, bytes.NewReader(compressed))
- if err != nil {
- return fmt.Errorf("failed to create HTTP request: %w", err)
- }
-
- req.Header.Set("Content-Type", "application/x-protobuf")
- req.Header.Set("Content-Encoding", "snappy")
- req.Header.Set("X-Prometheus-Remote-Write-Version", "0.1.0")
-
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Do(req)
- if err != nil {
- return fmt.Errorf("failed to send remote write request: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
- body, _ := io.ReadAll(resp.Body)
- return fmt.Errorf("remote write failed with status %d: %s", resp.StatusCode, string(body))
- }
-
- log.Printf("Successfully pushed historic data for %d hours ago (timestamp: %s)",
- hoursAgo, time.Now().Add(-time.Duration(hoursAgo)*time.Hour).Format(time.RFC3339))
-
- return nil
-}
-
-// BackfillHistoricData backfills data for multiple time points
-// prometheusURL: URL of Prometheus remote write endpoint
-// startHoursAgo: how many hours ago to start backfilling
-// endHoursAgo: how many hours ago to end backfilling
-// intervalHours: interval between data points in hours
-func BackfillHistoricData(prometheusURL string, startHoursAgo, endHoursAgo, intervalHours int) error {
- log.Printf("Starting backfill from %d hours ago to %d hours ago (interval: %d hours)",
- startHoursAgo, endHoursAgo, intervalHours)
-
- successCount := 0
- errorCount := 0
-
- for hoursAgo := startHoursAgo; hoursAgo >= endHoursAgo; hoursAgo -= intervalHours {
- if err := PushHistoricData(prometheusURL, hoursAgo); err != nil {
- log.Printf("Error pushing data for %d hours ago: %v", hoursAgo, err)
- errorCount++
- } else {
- successCount++
- }
-
- // Small delay to avoid overwhelming Prometheus
- time.Sleep(100 * time.Millisecond)
- }
-
- log.Printf("Backfill complete: %d successful, %d errors", successCount, errorCount)
-
- if errorCount > 0 {
- return fmt.Errorf("backfill completed with %d errors", errorCount)
- }
-
- return nil
-}
diff --git a/f3s/prometheus-pusher/internal/config/config.go b/f3s/prometheus-pusher/internal/config/config.go
new file mode 100644
index 0000000..9919e52
--- /dev/null
+++ b/f3s/prometheus-pusher/internal/config/config.go
@@ -0,0 +1,49 @@
+package config
+
+import "time"
+
+// Mode represents the ingestion mode
+type Mode string
+
+const (
+ ModeRealtime Mode = "realtime"
+ ModeHistoric Mode = "historic"
+ ModeBackfill Mode = "backfill"
+ ModeAuto Mode = "auto"
+)
+
+// Config holds all configuration for the prometheus-pusher
+type Config struct {
+ Mode Mode
+ PushgatewayURL string
+ PrometheusURL string
+ JobName string
+ Continuous bool
+ InputFile string
+ InputFormat string
+ HoursAgo int
+ StartHours int
+ EndHours int
+ Interval int
+}
+
+// NewConfig creates a new Config with default values
+func NewConfig() Config {
+ return Config{
+ Mode: ModeRealtime,
+ PushgatewayURL: "http://localhost:9091",
+ PrometheusURL: "http://localhost:9090/api/v1/write",
+ JobName: "example_metrics_pusher",
+ InputFormat: "csv",
+ HoursAgo: 24,
+ StartHours: 48,
+ EndHours: 0,
+ Interval: 1,
+ }
+}
+
+// AutoIngestThreshold is the age threshold for auto mode routing
+const AutoIngestThreshold = 5 * time.Minute
+
+// DefaultHTTPTimeout is the default timeout for HTTP requests
+const DefaultHTTPTimeout = 10 * time.Second
diff --git a/f3s/prometheus-pusher/internal/ingester/auto.go b/f3s/prometheus-pusher/internal/ingester/auto.go
new file mode 100644
index 0000000..c40754e
--- /dev/null
+++ b/f3s/prometheus-pusher/internal/ingester/auto.go
@@ -0,0 +1,131 @@
+package ingester
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "prometheus-pusher/internal/config"
+ "prometheus-pusher/internal/metrics"
+)
+
+const ageThreshold = 5 * time.Minute
+
+// DetermineMode automatically determines which ingestion mode to use based on timestamp age.
+// Data older than 5 minutes uses historic mode (Remote Write), newer data uses realtime mode (Pushgateway).
+func DetermineMode(timestamp time.Time) config.Mode {
+ age := time.Since(timestamp)
+ if age > ageThreshold {
+ return config.ModeHistoric
+ }
+ return config.ModeRealtime
+}
+
+// AutoIngester handles automatic ingestion by routing samples to appropriate ingesters.
+type AutoIngester struct {
+ pushgateway PushgatewayIngester
+ remoteWrite RemoteWriteIngester
+ collectors metrics.Collectors
+}
+
+// NewAutoIngester creates a new auto ingester.
+func NewAutoIngester(collectors metrics.Collectors) AutoIngester {
+ return AutoIngester{
+ pushgateway: NewPushgatewayIngester(),
+ remoteWrite: NewRemoteWriteIngester(),
+ collectors: collectors,
+ }
+}
+
+// Ingest automatically routes samples to appropriate ingestion method based on timestamp age.
+func (a AutoIngester) Ingest(ctx context.Context, samples []metrics.Sample, cfg config.Config) error {
+ if len(samples) == 0 {
+ return fmt.Errorf("no samples to ingest")
+ }
+
+ realtimeSamples, historicSamples := groupSamplesByMode(samples)
+
+ logIngestSummary(len(samples), len(realtimeSamples), len(historicSamples))
+
+ if len(realtimeSamples) > 0 {
+ if err := a.ingestRealtime(ctx, cfg); err != nil {
+ return fmt.Errorf("failed to ingest realtime samples: %w", err)
+ }
+ }
+
+ if len(historicSamples) > 0 {
+ if err := a.ingestHistoric(ctx, historicSamples, cfg); err != nil {
+ return fmt.Errorf("failed to ingest historic samples: %w", err)
+ }
+ }
+
+ log.Printf("\nšŸŽ‰ Auto-ingest complete!")
+ return nil
+}
+
+// groupSamplesByMode separates samples into realtime and historic groups.
+func groupSamplesByMode(samples []metrics.Sample) (realtime, historic []metrics.Sample) {
+ realtimeSamples := make([]metrics.Sample, 0)
+ historicSamples := make([]metrics.Sample, 0)
+
+ for _, sample := range samples {
+ if DetermineMode(sample.Timestamp) == config.ModeRealtime {
+ realtimeSamples = append(realtimeSamples, sample)
+ } else {
+ historicSamples = append(historicSamples, sample)
+ }
+ }
+
+ return realtimeSamples, historicSamples
+}
+
+// logIngestSummary logs the ingestion summary.
+func logIngestSummary(total, realtime, historic int) {
+ log.Printf("šŸ“Š Auto-ingest summary:")
+ log.Printf(" Total samples: %d", total)
+ log.Printf(" Realtime samples (< 5min old): %d", realtime)
+ log.Printf(" Historic samples (> 5min old): %d", historic)
+}
+
+// ingestRealtime ingests realtime samples via Pushgateway.
+func (a AutoIngester) ingestRealtime(ctx context.Context, cfg config.Config) error {
+ log.Printf("\nšŸ”„ Ingesting REALTIME samples via Pushgateway...")
+ log.Printf(" Note: Pushgateway uses current timestamp (original timestamps ignored)")
+
+ if err := a.pushgateway.Ingest(ctx, a.collectors, cfg.PushgatewayURL, cfg.JobName); err != nil {
+ return err
+ }
+
+ log.Printf("āœ… Successfully ingested realtime samples")
+ return nil
+}
+
+// ingestHistoric ingests historic samples via Remote Write.
+func (a AutoIngester) ingestHistoric(ctx context.Context, samples []metrics.Sample, cfg config.Config) error {
+ log.Printf("\nā° Ingesting %d HISTORIC samples via Remote Write...", len(samples))
+
+ for i, sample := range samples {
+ age := time.Since(sample.Timestamp)
+ log.Printf(" [%d/%d] %s (age: %s)", i+1, len(samples), sample.MetricName, formatDuration(age))
+ }
+
+ if err := a.remoteWrite.Ingest(ctx, samples, cfg.PrometheusURL); err != nil {
+ return err
+ }
+
+ log.Printf("āœ… Successfully ingested %d historic samples", len(samples))</