Files
trufflehog/pkg/sources/git/metrics.go

131 lines
3.8 KiB
Go

package git
import (
"fmt"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
// metricsCollector defines the interface for recording Git scan metrics.
type metricsCollector interface {
// Clone metrics
RecordCloneOperation(status string, reason string, exitCode int)
// Scan metrics
RecordCommitScanned()
RecordRepoScanned(status string)
}
// Predefined status values
const (
statusSuccess = "success"
statusFailure = "failure"
)
// Predefined clone success reason
const (
cloneSuccess = "success"
)
// Predefined clone failure reasons to avoid high cardinality
const (
// Authentication/redirection errors
cloneFailureAuth = "auth_error"
// Rate limiting errors
cloneFailureRateLimit = "rate_limit"
// Permission errors
cloneFailurePermission = "permission_denied"
// Network/connection errors
cloneFailureNetwork = "network_error"
// Git reference errors
cloneFailureReference = "reference_error"
// Other/unknown errors
cloneFailureOther = "other_error"
)
type collector struct {
cloneOperations *prometheus.CounterVec
commitsScanned prometheus.Counter
reposScanned *prometheus.CounterVec
}
var metricsInstance metricsCollector
func init() {
// These are package-level metrics that are incremented by all git scans across the lifetime of the process.
metricsInstance = &collector{
cloneOperations: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "git_clone_operations_total",
Help: "Total number of git clone operations by status, reason, and exit code",
}, []string{"status", "reason", "exit_code"}),
commitsScanned: promauto.NewCounter(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "git_commits_scanned_total",
Help: "Total number of git commits scanned",
}),
reposScanned: promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: common.MetricsNamespace,
Subsystem: common.MetricsSubsystem,
Name: "git_repos_scanned_total",
Help: "Total number of git repositories scanned by status (success/failure)",
}, []string{"status"}),
}
}
func (c *collector) RecordCloneOperation(status string, reason string, exitCode int) {
c.cloneOperations.WithLabelValues(status, reason, fmt.Sprintf("%d", exitCode)).Inc()
}
func (c *collector) RecordCommitScanned() {
c.commitsScanned.Inc()
}
func (c *collector) RecordRepoScanned(status string) {
c.reposScanned.WithLabelValues(status).Inc()
}
// ClassifyCloneError analyzes the error message and returns the appropriate failure reason
func ClassifyCloneError(errMsg string) string {
switch {
case strings.Contains(errMsg, "unable to update url base from redirection") &&
strings.Contains(errMsg, "redirect:") && strings.Contains(errMsg, "users/sign_in"):
return cloneFailureAuth
case strings.Contains(errMsg, "The requested URL returned error: 429") ||
strings.Contains(errMsg, "remote: Retry later"):
return cloneFailureRateLimit
case strings.Contains(errMsg, "The requested URL returned error: 403") ||
strings.Contains(errMsg, "remote: You are not allowed to download code from this project"):
return cloneFailurePermission
case strings.Contains(errMsg, "RPC failed") ||
strings.Contains(errMsg, "unexpected disconnect") ||
strings.Contains(errMsg, "early EOF") ||
strings.Contains(errMsg, "Problem (3) in the Chunked-Encoded data"):
return cloneFailureNetwork
case strings.Contains(errMsg, "cannot process") ||
strings.Contains(errMsg, "multiple updates for ref") ||
strings.Contains(errMsg, "invalid index-pack output"):
return cloneFailureReference
default:
return cloneFailureOther
}
}