summaryrefslogtreecommitdiff
path: root/f3s/prometheus-pusher/internal/parser
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 /f3s/prometheus-pusher/internal/parser
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>
Diffstat (limited to 'f3s/prometheus-pusher/internal/parser')
-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
3 files changed, 219 insertions, 0 deletions
diff --git a/f3s/prometheus-pusher/internal/parser/csv.go b/f3s/prometheus-pusher/internal/parser/csv.go
new file mode 100644
index 0000000..a59b6e6
--- /dev/null
+++ b/f3s/prometheus-pusher/internal/parser/csv.go
@@ -0,0 +1,101 @@
+package parser
+
+import (
+ "context"
+ "encoding/csv"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+
+ "prometheus-pusher/internal/metrics"
+)
+
+// CSVParser parses metrics from CSV format
+type CSVParser struct{}
+
+// NewCSVParser creates a new CSV parser
+func NewCSVParser() *CSVParser {
+ return &CSVParser{}
+}
+
+// Parse reads metrics from CSV format
+// Format: metric_name,label1=value1;label2=value2,value,timestamp_ms
+func (p *CSVParser) Parse(ctx context.Context, reader io.Reader) ([]metrics.Sample, error) {
+ var samples []metrics.Sample
+
+ csvReader := csv.NewReader(reader)
+ csvReader.Comment = '#'
+
+ lineNum := 0
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ 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 {
+ continue // Skip invalid records
+ }
+
+ sample, err := p.parseRecord(record, lineNum)
+ if err != nil {
+ continue // Skip records with errors
+ }
+
+ samples = append(samples, sample)
+ }
+
+ return samples, nil
+}
+
+func (p *CSVParser) parseRecord(record []string, lineNum int) (metrics.Sample, error) {
+ metricName := strings.TrimSpace(record[0])
+ if metricName == "" {
+ return metrics.Sample{}, fmt.Errorf("empty metric name")
+ }
+
+ labels := parseLabels(record[1])
+
+ value, err := strconv.ParseFloat(strings.TrimSpace(record[2]), 64)
+ if err != nil {
+ return metrics.Sample{}, fmt.Errorf("invalid value: %w", err)
+ }
+
+ timestamp := time.Now()
+ if len(record) > 3 && record[3] != "" {
+ timestampMs, err := strconv.ParseInt(strings.TrimSpace(record[3]), 10, 64)
+ if err == nil {
+ timestamp = time.UnixMilli(timestampMs)
+ }
+ }
+
+ return metrics.NewSample(metricName, labels, value, timestamp), nil
+}
+
+func parseLabels(labelStr string) map[string]string {
+ labels := make(map[string]string)
+ if labelStr == "" {
+ return labels
+ }
+
+ labelPairs := strings.Split(labelStr, ";")
+ for _, pair := range labelPairs {
+ parts := strings.SplitN(pair, "=", 2)
+ if len(parts) == 2 {
+ labels[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
+ }
+ }
+ return labels
+}
diff --git a/f3s/prometheus-pusher/internal/parser/json.go b/f3s/prometheus-pusher/internal/parser/json.go
new file mode 100644
index 0000000..b6b230c
--- /dev/null
+++ b/f3s/prometheus-pusher/internal/parser/json.go
@@ -0,0 +1,62 @@
+package parser
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "time"
+
+ "prometheus-pusher/internal/metrics"
+)
+
+// JSONParser parses metrics from JSON format
+type JSONParser struct{}
+
+// NewJSONParser creates a new JSON parser
+func NewJSONParser() *JSONParser {
+ return &JSONParser{}
+}
+
+type jsonSample struct {
+ Metric string `json:"metric"`
+ Labels map[string]string `json:"labels"`
+ Value float64 `json:"value"`
+ TimestampMs int64 `json:"timestamp_ms,omitempty"`
+}
+
+// Parse reads metrics from JSON format
+func (p *JSONParser) Parse(ctx context.Context, reader io.Reader) ([]metrics.Sample, error) {
+ var rawSamples []jsonSample
+
+ decoder := json.NewDecoder(reader)
+ if err := decoder.Decode(&rawSamples); err != nil {
+ return nil, fmt.Errorf("failed to parse JSON: %w", err)
+ }
+
+ samples := make([]metrics.Sample, 0, len(rawSamples))
+ for _, raw := range rawSamples {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ if raw.Metric == "" {
+ continue
+ }
+
+ timestamp := time.Now()
+ if raw.TimestampMs > 0 {
+ timestamp = time.UnixMilli(raw.TimestampMs)
+ }
+
+ if raw.Labels == nil {
+ raw.Labels = make(map[string]string)
+ }
+
+ samples = append(samples, metrics.NewSample(raw.Metric, raw.Labels, raw.Value, timestamp))
+ }
+
+ return samples, nil
+}
diff --git a/f3s/prometheus-pusher/internal/parser/parser.go b/f3s/prometheus-pusher/internal/parser/parser.go
new file mode 100644
index 0000000..6a7732d
--- /dev/null
+++ b/f3s/prometheus-pusher/internal/parser/parser.go
@@ -0,0 +1,56 @@
+package parser
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+
+ "prometheus-pusher/internal/metrics"
+)
+
+// Parser defines the interface for metric parsers.
+type Parser interface {
+ Parse(ctx context.Context, reader io.Reader) ([]metrics.Sample, error)
+}
+
+// ParseFile parses metrics from a file.
+func ParseFile(ctx context.Context, filename, format string) ([]metrics.Sample, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return nil, fmt.Errorf("failed to open file: %w", err)
+ }
+ defer file.Close()
+
+ return parseWithFormat(ctx, file, format)
+}
+
+// ParseStdin parses metrics from standard input.
+func ParseStdin(ctx context.Context, format string) ([]metrics.Sample, error) {
+ return parseWithFormat(ctx, os.Stdin, format)
+}
+
+// parseWithFormat parses metrics using the specified format.
+func parseWithFormat(ctx context.Context, reader io.Reader, format string) ([]metrics.Sample, error) {
+ var parser Parser
+
+ switch format {
+ case "csv":
+ parser = NewCSVParser()
+ case "json":
+ parser = NewJSONParser()
+ default:
+ return nil, fmt.Errorf("unsupported format: %s (use csv or json)", format)
+ }
+
+ samples, err := parser.Parse(ctx, reader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse metrics: %w", err)
+ }
+
+ if len(samples) == 0 {
+ return nil, fmt.Errorf("no valid samples found")
+ }
+
+ return samples, nil
+}