Files
sendico/api/gateway/mntx/internal/service/gateway/metrics.go
2025-12-26 14:09:16 +01:00

167 lines
4.4 KiB
Go

package gateway
import (
"errors"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/shopspring/decimal"
"github.com/tech/sendico/pkg/merrors"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
)
var (
metricsOnce sync.Once
rpcLatency *prometheus.HistogramVec
rpcStatus *prometheus.CounterVec
payoutCounter *prometheus.CounterVec
payoutAmountTotal *prometheus.CounterVec
payoutErrorCount *prometheus.CounterVec
payoutMissedAmounts *prometheus.CounterVec
)
func initMetrics() {
metricsOnce.Do(func() {
rpcLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "sendico",
Subsystem: "mntx_gateway",
Name: "rpc_latency_seconds",
Help: "Latency distribution for Monetix gateway RPC handlers.",
Buckets: prometheus.DefBuckets,
}, []string{"method"})
rpcStatus = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "sendico",
Subsystem: "mntx_gateway",
Name: "rpc_requests_total",
Help: "Total number of RPC invocations grouped by method and status.",
}, []string{"method", "status"})
payoutCounter = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "sendico",
Subsystem: "mntx_gateway",
Name: "payouts_total",
Help: "Total payouts processed grouped by outcome.",
}, []string{"status"})
payoutAmountTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "sendico",
Subsystem: "mntx_gateway",
Name: "payout_amount_total",
Help: "Total payout amount grouped by outcome and currency.",
}, []string{"status", "currency"})
payoutErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "sendico",
Subsystem: "mntx_gateway",
Name: "payout_errors_total",
Help: "Payout failures grouped by reason.",
}, []string{"reason"})
payoutMissedAmounts = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "sendico",
Subsystem: "mntx_gateway",
Name: "payout_missed_amount_total",
Help: "Total payout volume that failed grouped by reason and currency.",
}, []string{"reason", "currency"})
})
}
func observeRPC(method string, err error, duration time.Duration) {
if rpcLatency != nil {
rpcLatency.WithLabelValues(method).Observe(duration.Seconds())
}
if rpcStatus != nil {
rpcStatus.WithLabelValues(method, statusLabel(err)).Inc()
}
}
func observePayoutSuccess(amount *moneyv1.Money) {
if payoutCounter != nil {
payoutCounter.WithLabelValues("processed").Inc()
}
value, currency := monetaryValue(amount)
if value > 0 && payoutAmountTotal != nil {
payoutAmountTotal.WithLabelValues("processed", currency).Add(value)
}
}
func observePayoutError(reason string, amount *moneyv1.Money) {
reason = reasonLabel(reason)
if payoutCounter != nil {
payoutCounter.WithLabelValues("failed").Inc()
}
if payoutErrorCount != nil {
payoutErrorCount.WithLabelValues(reason).Inc()
}
value, currency := monetaryValue(amount)
if value <= 0 {
return
}
if payoutAmountTotal != nil {
payoutAmountTotal.WithLabelValues("failed", currency).Add(value)
}
if payoutMissedAmounts != nil {
payoutMissedAmounts.WithLabelValues(reason, currency).Add(value)
}
}
func monetaryValue(amount *moneyv1.Money) (float64, string) {
if amount == nil {
return 0, "unknown"
}
val := strings.TrimSpace(amount.Amount)
if val == "" {
return 0, currencyLabel(amount.Currency)
}
dec, err := decimal.NewFromString(val)
if err != nil {
return 0, currencyLabel(amount.Currency)
}
f, _ := dec.Float64()
if f < 0 {
return 0, currencyLabel(amount.Currency)
}
return f, currencyLabel(amount.Currency)
}
func currencyLabel(code string) string {
code = strings.ToUpper(strings.TrimSpace(code))
if code == "" {
return "unknown"
}
return code
}
func reasonLabel(reason string) string {
reason = strings.TrimSpace(reason)
if reason == "" {
return "unknown"
}
return strings.ToLower(reason)
}
func statusLabel(err error) string {
switch {
case err == nil:
return "ok"
case errors.Is(err, merrors.ErrInvalidArg):
return "invalid_argument"
case errors.Is(err, merrors.ErrNoData):
return "not_found"
case errors.Is(err, merrors.ErrDataConflict):
return "conflict"
case errors.Is(err, merrors.ErrAccessDenied):
return "denied"
case errors.Is(err, merrors.ErrInternal):
return "internal"
default:
return "error"
}
}