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