Skip to content

Commit 745db2d

Browse files
authored
Merge branch 'CarbonScribe:main' into feat
2 parents ccc82b8 + 8c11f57 commit 745db2d

56 files changed

Lines changed: 12474 additions & 165 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎project-portal/project-portal-backend/api/v1/monitoring.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package v1
22

33
import (
4+
"errors"
45
"net/http"
56
"time"
67

78
"carbon-scribe/project-portal/project-portal-backend/internal/monitoring"
9+
"carbon-scribe/project-portal/project-portal-backend/internal/monitoring/analytics"
810
"carbon-scribe/project-portal/project-portal-backend/internal/monitoring/ingestion"
911

1012
"github.com/gin-gonic/gin"
@@ -13,13 +15,71 @@ import (
1315
// MonitoringHandler handles monitoring-related API endpoints.
1416
type MonitoringHandler struct {
1517
service *monitoring.Service
18+
// performance is optional: when nil the benchmark endpoint reports that
19+
// the analytics service is not configured rather than panicking.
20+
performance *analytics.PerformanceService
1621
}
1722

1823
// NewMonitoringHandler creates a new monitoring handler.
1924
func NewMonitoringHandler(service *monitoring.Service) *MonitoringHandler {
2025
return &MonitoringHandler{service: service}
2126
}
2227

28+
// WithPerformanceAnalytics attaches the SLA benchmark service, enabling
29+
// GET /api/v1/monitoring/analytics/performance.
30+
func (h *MonitoringHandler) WithPerformanceAnalytics(svc *analytics.PerformanceService) *MonitoringHandler {
31+
h.performance = svc
32+
return h
33+
}
34+
35+
// GetPerformanceBenchmark handles GET /api/v1/monitoring/analytics/performance.
36+
//
37+
// @Summary Benchmark service performance against SLA thresholds
38+
// @Description Computes latency percentiles, error rate and uptime for a service
39+
// @Description over a window and compares each against the configured SLA.
40+
// @Tags monitoring
41+
// @Produce json
42+
// @Param service query string true "Service name to benchmark"
43+
// @Param window query string false "Look-back window as a Go duration (default 1h)"
44+
// @Success 200 {object} analytics.BenchmarkReport
45+
// @Failure 400 {object} map[string]string
46+
// @Failure 500 {object} map[string]string
47+
// @Failure 503 {object} map[string]string
48+
// @Router /api/v1/monitoring/analytics/performance [get]
49+
func (h *MonitoringHandler) GetPerformanceBenchmark(c *gin.Context) {
50+
if h.performance == nil {
51+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "performance analytics is not configured"})
52+
return
53+
}
54+
55+
req := analytics.BenchmarkRequest{Service: c.Query("service")}
56+
57+
if raw := c.Query("window"); raw != "" {
58+
window, err := time.ParseDuration(raw)
59+
if err != nil {
60+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid window: " + err.Error()})
61+
return
62+
}
63+
if window <= 0 {
64+
c.JSON(http.StatusBadRequest, gin.H{"error": "window must be positive"})
65+
return
66+
}
67+
req.Window = window
68+
}
69+
70+
report, err := h.performance.Benchmark(c.Request.Context(), req)
71+
if err != nil {
72+
if errors.Is(err, analytics.ErrServiceRequired) {
73+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
74+
return
75+
}
76+
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
77+
return
78+
}
79+
80+
c.JSON(http.StatusOK, report)
81+
}
82+
2383
// IngestSatellite handles POST /api/v1/monitoring/satellite.
2484
func (h *MonitoringHandler) IngestSatellite(c *gin.Context) {
2585
var req monitoring.IngestSatelliteRequest
@@ -279,5 +339,6 @@ func RegisterMonitoringRoutes(r *gin.RouterGroup, handler *MonitoringHandler) {
279339
monitoring.GET("/iot", handler.ListIoTReadings)
280340
monitoring.GET("/ndvi/tile/:z/:x/:y", handler.GetNDVITile)
281341
monitoring.GET("/ndvi/timeseries/animation/:z/:x/:y", handler.GetNDVITimeSeriesAnimation)
342+
monitoring.GET("/analytics/performance", handler.GetPerformanceBenchmark)
282343
}
283344
}

‎project-portal/project-portal-backend/cmd/api/main.go‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
integrationstellar "carbon-scribe/project-portal/project-portal-backend/internal/integration/stellar"
2525
"carbon-scribe/project-portal/project-portal-backend/internal/middleware"
2626
"carbon-scribe/project-portal/project-portal-backend/internal/monitoring"
27+
"carbon-scribe/project-portal/project-portal-backend/internal/monitoring/analytics"
2728
"carbon-scribe/project-portal/project-portal-backend/internal/notifications"
2829
"carbon-scribe/project-portal/project-portal-backend/internal/notifications/channels"
2930
"carbon-scribe/project-portal/project-portal-backend/internal/project"
@@ -186,6 +187,18 @@ func main() {
186187
}
187188
mintingCapValidator := minting.NewCapValidator(methodologyCapService)
188189
mintingService := minting.NewService(db, mintingContractClient, mintingCapValidator)
190+
191+
// Apply the configured minting retry policy (exponential backoff with
192+
// jitter). NewService already resolves this from the environment; this
193+
// makes the application config the single source of truth.
194+
if configurable, ok := mintingService.(minting.RetryPolicyConfigurer); ok {
195+
configurable.SetRetryPolicy(minting.RetryPolicy{
196+
MaxAttempts: cfg.Minting.MaxAttempts,
197+
BaseBackoff: cfg.Minting.BaseBackoff,
198+
MaxBackoff: cfg.Minting.MaxBackoff,
199+
JitterFactor: cfg.Minting.JitterFactor,
200+
})
201+
}
189202
mintingHandler := minting.NewHandler(mintingService).WithRateLimiter(rateLimiter)
190203

191204
projectService := project.NewService(projectRepo, methodologyService, mintingService, validation.Validator(methodologyValidator))
@@ -318,7 +331,26 @@ func main() {
318331

319332
monitoringRepo := monitoring.NewPostgresRepository(sqlDB)
320333
monitoringService := monitoring.NewService(monitoringRepo)
321-
monitoringHandler := api.NewMonitoringHandler(monitoringService)
334+
335+
// Performance analytics benchmarks stored monitoring data against the SLA
336+
// thresholds in configuration and is exposed at
337+
// GET /api/v1/monitoring/analytics/performance.
338+
performanceAnalytics := analytics.NewPerformanceService(
339+
monitoringRepo,
340+
monitoringRepo,
341+
analytics.SLAThresholds{
342+
LatencyP50Ms: cfg.Monitoring.SLA.LatencyP50Ms,
343+
LatencyP95Ms: cfg.Monitoring.SLA.LatencyP95Ms,
344+
LatencyP99Ms: cfg.Monitoring.SLA.LatencyP99Ms,
345+
MaxErrorRate: cfg.Monitoring.SLA.MaxErrorRate,
346+
MinUptime: cfg.Monitoring.SLA.MinUptime,
347+
LatencyMetricName: cfg.Monitoring.SLA.LatencyMetricName,
348+
ErrorRateMetricName: cfg.Monitoring.SLA.ErrorRateMetricName,
349+
},
350+
)
351+
352+
monitoringHandler := api.NewMonitoringHandler(monitoringService).
353+
WithPerformanceAnalytics(performanceAnalytics)
322354
log.Println("✅ Monitoring service initialized")
323355

324356
// ============================================================================

‎project-portal/project-portal-backend/internal/config/config.go‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"strconv"
77
"strings"
8+
"time"
89
)
910

1011
// Config holds application configuration
@@ -25,6 +26,44 @@ type Config struct {
2526
Notifications NotificationsConfig
2627
MQTT MQTTConfig
2728
SES SESConfig
29+
Monitoring MonitoringConfig
30+
Minting MintingConfig
31+
}
32+
33+
// MonitoringConfig holds monitoring-module settings.
34+
type MonitoringConfig struct {
35+
SLA SLAConfig
36+
}
37+
38+
// SLAConfig holds the SLA thresholds the performance analytics service
39+
// benchmarks observed monitoring data against. A zero threshold means
40+
// "not configured": that metric is reported but not judged, so an unset SLA
41+
// can never manufacture a breach.
42+
type SLAConfig struct {
43+
// Latency ceilings, in milliseconds.
44+
LatencyP50Ms float64
45+
LatencyP95Ms float64
46+
LatencyP99Ms float64
47+
// MaxErrorRate is a ceiling expressed as a fraction in [0,1] (0.01 = 1%).
48+
MaxErrorRate float64
49+
// MinUptime is a floor expressed as a fraction in [0,1] (0.999 = 99.9%).
50+
MinUptime float64
51+
// Names of the stored metrics the observed latency and error rate are
52+
// read from.
53+
LatencyMetricName string
54+
ErrorRateMetricName string
55+
}
56+
57+
// MintingConfig holds the retry policy for Soroban carbon-credit minting.
58+
// Backoff grows exponentially from BaseBackoff, doubling per attempt, capped
59+
// at MaxBackoff, with jitter applied to each delay.
60+
type MintingConfig struct {
61+
MaxAttempts int
62+
BaseBackoff time.Duration
63+
MaxBackoff time.Duration
64+
// JitterFactor is the fraction of the computed delay that is randomised,
65+
// in [0,1]. 0.2 means the actual delay lands in [0.8d, 1.0d].
66+
JitterFactor float64
2867
}
2968

3069
// ElasticsearchConfig holds configuration for Elasticsearch
@@ -295,6 +334,23 @@ func Load() (*Config, error) {
295334
TwilioAuthToken: os.Getenv("TWILIO_AUTH_TOKEN"),
296335
TwilioFromNumber: os.Getenv("TWILIO_FROM_NUMBER"),
297336
},
337+
Monitoring: MonitoringConfig{
338+
SLA: SLAConfig{
339+
LatencyP50Ms: getFloatOrDefault("SLA_LATENCY_P50_MS", 100),
340+
LatencyP95Ms: getFloatOrDefault("SLA_LATENCY_P95_MS", 300),
341+
LatencyP99Ms: getFloatOrDefault("SLA_LATENCY_P99_MS", 750),
342+
MaxErrorRate: getFloatOrDefault("SLA_MAX_ERROR_RATE", 0.01),
343+
MinUptime: getFloatOrDefault("SLA_MIN_UPTIME", 0.99),
344+
LatencyMetricName: getEnvOrDefault("SLA_LATENCY_METRIC", "api_request_latency_ms"),
345+
ErrorRateMetricName: getEnvOrDefault("SLA_ERROR_RATE_METRIC", "api_error_rate"),
346+
},
347+
},
348+
Minting: MintingConfig{
349+
MaxAttempts: getIntOrDefault("MINTING_MAX_ATTEMPTS", 3),
350+
BaseBackoff: getDurationOrDefault("MINTING_BACKOFF_BASE", 2*time.Second),
351+
MaxBackoff: getDurationOrDefault("MINTING_BACKOFF_MAX", 60*time.Second),
352+
JitterFactor: getFloatOrDefault("MINTING_BACKOFF_JITTER", 0.2),
353+
},
298354
}, nil
299355
}
300356

@@ -316,3 +372,31 @@ func getIntOrDefault(key string, defaultVal int) int {
316372
}
317373
return parsed
318374
}
375+
376+
// getFloatOrDefault reads a float64 from the environment, falling back to
377+
// defaultVal when unset or unparsable.
378+
func getFloatOrDefault(key string, defaultVal float64) float64 {
379+
v := os.Getenv(key)
380+
if v == "" {
381+
return defaultVal
382+
}
383+
parsed, err := strconv.ParseFloat(v, 64)
384+
if err != nil {
385+
return defaultVal
386+
}
387+
return parsed
388+
}
389+
390+
// getDurationOrDefault reads a Go duration string (e.g. "2s", "500ms") from
391+
// the environment, falling back to defaultVal when unset or unparsable.
392+
func getDurationOrDefault(key string, defaultVal time.Duration) time.Duration {
393+
v := os.Getenv(key)
394+
if v == "" {
395+
return defaultVal
396+
}
397+
parsed, err := time.ParseDuration(v)
398+
if err != nil {
399+
return defaultVal
400+
}
401+
return parsed
402+
}

0 commit comments

Comments
 (0)