Skip to main content
โšก Calmops

Analytics and Reporting in Go

Analytics and Reporting in Go

Introduction

Analytics and reporting transform raw data into actionable insights. This guide covers implementing analytics systems and generating reports in Go.

Effective analytics and reporting enable data-driven decision making and performance monitoring.

Analytics Fundamentals

Basic Analytics

package main

import (
	"fmt"
	"sort"
	"time"
)

// Metric represents a metric
type Metric struct {
	Name      string
	Value     float64
	Timestamp time.Time
	Tags      map[string]string
}

// Analytics performs analytics on metrics
type Analytics struct {
	metrics []Metric
}

// NewAnalytics creates a new analytics
func NewAnalytics() *Analytics {
	return &Analytics{
		metrics: []Metric{},
	}
}

// AddMetric adds a metric
func (a *Analytics) AddMetric(metric Metric) {
	a.metrics = append(a.metrics, metric)
}

// Sum calculates sum of metrics
func (a *Analytics) Sum() float64 {
	var sum float64
	for _, metric := range a.metrics {
		sum += metric.Value
	}
	return sum
}

// Average calculates average
func (a *Analytics) Average() float64 {
	if len(a.metrics) == 0 {
		return 0
	}
	return a.Sum() / float64(len(a.metrics))
}

// Min finds minimum
func (a *Analytics) Min() float64 {
	if len(a.metrics) == 0 {
		return 0
	}

	min := a.metrics[0].Value
	for _, metric := range a.metrics[1:] {
		if metric.Value < min {
			min = metric.Value
		}
	}
	return min
}

// Max finds maximum
func (a *Analytics) Max() float64 {
	if len(a.metrics) == 0 {
		return 0
	}

	max := a.metrics[0].Value
	for _, metric := range a.metrics[1:] {
		if metric.Value > max {
			max = metric.Value
		}
	}
	return max
}

// Count returns count of metrics
func (a *Analytics) Count() int {
	return len(a.metrics)
}

// Percentile calculates percentile
func (a *Analytics) Percentile(p float64) float64 {
	if len(a.metrics) == 0 {
		return 0
	}

	values := make([]float64, len(a.metrics))
	for i, metric := range a.metrics {
		values[i] = metric.Value
	}

	sort.Float64s(values)

	index := int(float64(len(values)) * p / 100)
	if index >= len(values) {
		index = len(values) - 1
	}

	return values[index]
}

Good: Proper Analytics Implementation

package main

import (
	"fmt"
	"sync"
	"time"
)

// Report represents an analytics report
type Report struct {
	Title      string
	Generated  time.Time
	Metrics    map[string]interface{}
	Dimensions map[string][]string
}

// ReportGenerator generates reports
type ReportGenerator struct {
	analytics *Analytics
	mu        sync.RWMutex
}

// NewReportGenerator creates a new report generator
func NewReportGenerator(analytics *Analytics) *ReportGenerator {
	return &ReportGenerator{
		analytics: analytics,
	}
}

// GenerateReport generates a report
func (rg *ReportGenerator) GenerateReport(title string) *Report {
	rg.mu.RLock()
	defer rg.mu.RUnlock()

	report := &Report{
		Title:      title,
		Generated:  time.Now(),
		Metrics:    make(map[string]interface{}),
		Dimensions: make(map[string][]string),
	}

	// Calculate metrics
	report.Metrics["count"] = rg.analytics.Count()
	report.Metrics["sum"] = rg.analytics.Sum()
	report.Metrics["average"] = rg.analytics.Average()
	report.Metrics["min"] = rg.analytics.Min()
	report.Metrics["max"] = rg.analytics.Max()
	report.Metrics["p50"] = rg.analytics.Percentile(50)
	report.Metrics["p95"] = rg.analytics.Percentile(95)
	report.Metrics["p99"] = rg.analytics.Percentile(99)

	return report
}

// GroupBy groups metrics by dimension
func (rg *ReportGenerator) GroupBy(dimension string) map[string]*Analytics {
	rg.mu.RLock()
	defer rg.mu.RUnlock()

	groups := make(map[string]*Analytics)

	for _, metric := range rg.analytics.metrics {
		if value, exists := metric.Tags[dimension]; exists {
			if _, ok := groups[value]; !ok {
				groups[value] = NewAnalytics()
			}
			groups[value].AddMetric(metric)
		}
	}

	return groups
}

// TimeBucket buckets metrics by time
func (rg *ReportGenerator) TimeBucket(duration time.Duration) map[time.Time]*Analytics {
	rg.mu.RLock()
	defer rg.mu.RUnlock()

	buckets := make(map[time.Time]*Analytics)

	for _, metric := range rg.analytics.metrics {
		bucket := metric.Timestamp.Truncate(duration)

		if _, ok := buckets[bucket]; !ok {
			buckets[bucket] = NewAnalytics()
		}
		buckets[bucket].AddMetric(metric)
	}

	return buckets
}

// ReportFormatter formats reports
type ReportFormatter interface {
	Format(report *Report) string
}

// TextReportFormatter formats reports as text
type TextReportFormatter struct{}

// Format formats report as text
func (trf *TextReportFormatter) Format(report *Report) string {
	output := fmt.Sprintf("=== %s ===\n", report.Title)
	output += fmt.Sprintf("Generated: %s\n\n", report.Generated.Format(time.RFC3339))

	output += "Metrics:\n"
	for key, value := range report.Metrics {
		output += fmt.Sprintf("  %s: %v\n", key, value)
	}

	return output
}

// JSONReportFormatter formats reports as JSON
type JSONReportFormatter struct{}

// Format formats report as JSON
func (jrf *JSONReportFormatter) Format(report *Report) string {
	// Implementation would use json.Marshal
	return fmt.Sprintf(`{"title":"%s","generated":"%s"}`, report.Title, report.Generated)
}

// CSVReportFormatter formats reports as CSV
type CSVReportFormatter struct{}

// Format formats report as CSV
func (crf *CSVReportFormatter) Format(report *Report) string {
	output := "Metric,Value\n"

	for key, value := range report.Metrics {
		output += fmt.Sprintf("%s,%v\n", key, value)
	}

	return output
}

Bad: Improper Analytics

package main

// BAD: No aggregation
func BadAnalytics(metrics []Metric) {
	// No calculations
	// No insights
}

// BAD: No grouping
func BadGrouping(metrics []Metric) {
	// No dimensional analysis
	// No segmentation
}

// BAD: No reporting
func BadReporting(analytics *Analytics) {
	// No formatted output
	// No insights
}

Problems:

  • No aggregation
  • No grouping
  • No reporting
  • No insights

Advanced Analytics Patterns

Cohort Analysis

package main

import (
	"time"
)

// CohortAnalyzer performs cohort analysis
type CohortAnalyzer struct {
	metrics []Metric
}

// NewCohortAnalyzer creates a new cohort analyzer
func NewCohortAnalyzer(metrics []Metric) *CohortAnalyzer {
	return &CohortAnalyzer{
		metrics: metrics,
	}
}

// AnalyzeCohort analyzes a cohort
func (ca *CohortAnalyzer) AnalyzeCohort(startDate time.Time, duration time.Duration) map[string]int {
	cohort := make(map[string]int)

	for _, metric := range ca.metrics {
		if metric.Timestamp.After(startDate) && metric.Timestamp.Before(startDate.Add(duration)) {
			key := metric.Timestamp.Format("2006-01-02")
			cohort[key]++
		}
	}

	return cohort
}

Trend Analysis

package main

// TrendAnalyzer analyzes trends
type TrendAnalyzer struct {
	metrics []Metric
}

// NewTrendAnalyzer creates a new trend analyzer
func NewTrendAnalyzer(metrics []Metric) *TrendAnalyzer {
	return &TrendAnalyzer{
		metrics: metrics,
	}
}

// CalculateTrend calculates trend
func (ta *TrendAnalyzer) CalculateTrend(period time.Duration) float64 {
	if len(ta.metrics) < 2 {
		return 0
	}

	// Calculate trend using linear regression
	var sumX, sumY, sumXY, sumX2 float64
	n := float64(len(ta.metrics))

	for i, metric := range ta.metrics {
		x := float64(i)
		y := metric.Value

		sumX += x
		sumY += y
		sumXY += x * y
		sumX2 += x * x
	}

	// Slope of trend line
	slope := (n*sumXY - sumX*sumY) / (n*sumX2 - sumX*sumX)
	return slope
}

Report Generation

package main

import (
	"fmt"
	"os"
	"time"
)

// ReportWriter writes reports to files
type ReportWriter struct {
	outputDir string
}

// NewReportWriter creates a new report writer
func NewReportWriter(outputDir string) *ReportWriter {
	return &ReportWriter{
		outputDir: outputDir,
	}
}

// WriteReport writes a report to file
func (rw *ReportWriter) WriteReport(report *Report, formatter ReportFormatter) error {
	filename := fmt.Sprintf("%s/%s_%s.txt", rw.outputDir, report.Title, time.Now().Format("20060102"))

	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	content := formatter.Format(report)
	_, err = file.WriteString(content)

	return err
}

Best Practices

1. Calculate Key Metrics

// Always calculate meaningful metrics
metrics["average"] = analytics.Average()
metrics["p95"] = analytics.Percentile(95)

2. Segment Data

// Segment by dimensions
groups := generator.GroupBy("region")

3. Format for Audience

// Use appropriate format
formatter := &TextReportFormatter{}

4. Schedule Reports

// Generate reports on schedule
ticker := time.NewTicker(24 * time.Hour)

Common Pitfalls

1. No Aggregation

Always aggregate data meaningfully.

2. No Segmentation

Segment data by relevant dimensions.

3. Poor Formatting

Format reports for the audience.

4. No Scheduling

Automate report generation.

Resources

Summary

Effective analytics and reporting are crucial. Key takeaways:

  • Calculate meaningful metrics
  • Segment data by dimensions
  • Generate reports automatically
  • Format for the audience
  • Monitor trends
  • Perform cohort analysis
  • Schedule regular reports

By mastering analytics and reporting, you can drive data-driven decisions.

Comments