Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions pkg/sloop/server/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/golang/glog"
"github.com/pkg/errors"

"github.com/salesforce/sloop/pkg/sloop/server/server_metrics"
"github.com/salesforce/sloop/pkg/sloop/webserver"
)

Expand All @@ -29,9 +30,10 @@ type SloopConfig struct {
// These fields can only come from command line
ConfigFile string
// These fields can only come from file because they use complex types
LeftBarLinks []webserver.LinkTemplate `json:"leftBarLinks"`
ResourceLinks []webserver.ResourceLinkTemplate `json:"resourceLinks"`
ExclusionRules map[string][]any `json:"exclusionRules"`
LeftBarLinks []webserver.LinkTemplate `json:"leftBarLinks"`
ResourceLinks []webserver.ResourceLinkTemplate `json:"resourceLinks"`
ExclusionRules map[string][]any `json:"exclusionRules"`
UserMetricsHeaders []server_metrics.UserMetricsConfig `json:"userMetricsHeaders"`
// Normal fields that can come from file or cmd line
DisableKubeWatcher bool `json:"disableKubeWatch"`
KubeWatchResyncInterval time.Duration `json:"kubeWatchResyncInterval"`
Expand Down Expand Up @@ -76,6 +78,7 @@ type SloopConfig struct {
BadgerVLogTruncate bool `json:"badgerVLogTruncate"`
EnableDeleteKeys bool `json:"enableDeleteKeys"`
EnableGranularMetrics bool `json:"enableGranularMetrics"`
EnableUserMetrics bool `json:"enableUserMetrics"`
PrivilegedAccess bool `json:"PrivilegedAccess"`
BadgerDetailLogEnabled bool `json:"badgerDetailLogEnabled"`
}
Expand Down Expand Up @@ -124,6 +127,7 @@ func registerFlags(fs *flag.FlagSet, config *SloopConfig) {
fs.BoolVar(&config.BadgerSyncWrites, "badger-sync-writes", config.BadgerSyncWrites, "Sync Writes ensures writes are synced to disk if set to true")
fs.BoolVar(&config.EnableDeleteKeys, "enable-delete-keys", config.EnableDeleteKeys, "Use delete prefixes instead of dropPrefix for GC")
fs.BoolVar(&config.EnableGranularMetrics, "enable-granular-metrics", config.EnableGranularMetrics, "default:True , allows metrics of event kind to be granular with reason, type and name")
fs.BoolVar(&config.EnableUserMetrics, "enable-user-metrics", config.EnableUserMetrics, "Enable collection of user metrics from request headers")
fs.BoolVar(&config.PrivilegedAccess, "allow-privileged-access", config.PrivilegedAccess, "default:True , allows sloop to access kube root path")
fs.BoolVar(&config.BadgerVLogFileIOMapping, "badger-vlog-fileIO-mapping", config.BadgerVLogFileIOMapping, "Indicates which file loading mode should be used for the value log data, in memory constrained environments the value is recommended to be true")
fs.BoolVar(&config.BadgerVLogTruncate, "badger-vlog-truncate", config.BadgerVLogTruncate, "Truncate value log if badger db offset is different from badger db size")
Expand Down Expand Up @@ -176,6 +180,7 @@ func getDefaultConfig() *SloopConfig {
BadgerVLogTruncate: true,
EnableDeleteKeys: false,
EnableGranularMetrics: false,
EnableUserMetrics: false,
PrivilegedAccess: true,
BadgerDetailLogEnabled: false,
ExclusionRules: map[string][]any{},
Expand Down
29 changes: 18 additions & 11 deletions pkg/sloop/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/salesforce/sloop/pkg/sloop/ingress"
"github.com/salesforce/sloop/pkg/sloop/server/internal/config"
"github.com/salesforce/sloop/pkg/sloop/server/server_metrics"
"github.com/salesforce/sloop/pkg/sloop/store/typed"
"github.com/salesforce/sloop/pkg/sloop/store/untyped"

Expand Down Expand Up @@ -146,18 +147,24 @@ func RealMain() error {
displayContext = conf.DisplayContext
}

// Initialize user metrics if enabled
if conf.EnableUserMetrics {
server_metrics.InitUserMetrics(conf.UserMetricsHeaders)
}

webConfig := webserver.WebConfig{
BindAddress: conf.BindAddress,
Port: conf.Port,
WebFilesPath: conf.WebFilesPath,
ConfigYaml: conf.ToYaml(),
MaxLookback: conf.MaxLookback,
DefaultNamespace: conf.DefaultNamespace,
DefaultLookback: conf.DefaultLookback,
DefaultResources: conf.DefaultKind,
ResourceLinks: conf.ResourceLinks,
LeftBarLinks: conf.LeftBarLinks,
CurrentContext: displayContext,
BindAddress: conf.BindAddress,
Port: conf.Port,
WebFilesPath: conf.WebFilesPath,
ConfigYaml: conf.ToYaml(),
MaxLookback: conf.MaxLookback,
DefaultNamespace: conf.DefaultNamespace,
DefaultLookback: conf.DefaultLookback,
DefaultResources: conf.DefaultKind,
ResourceLinks: conf.ResourceLinks,
LeftBarLinks: conf.LeftBarLinks,
CurrentContext: displayContext,
EnableUserMetrics: conf.EnableUserMetrics,
}
err = webserver.Run(webConfig, tables)
if err != nil {
Expand Down
181 changes: 181 additions & 0 deletions pkg/sloop/server/server_metrics/server_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright (c) 2019, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

package server_metrics

import (
"net/http"
"sort"
"sync"

"github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus"
)

const (
// Metric names
MetricUserRequests = "user_requests"

// Built-in request context label keys (always included)
LabelHandler = "handler"
LabelCluster = "cluster"
LabelQuery = "query"
LabelNamespace = "namespace"
LabelKind = "kind"
LabelName = "name"
LabelLookback = "lookback"
)

// UserMetricsConfig defines a single header extraction rule for user metrics
type UserMetricsConfig struct {
// Label is the Prometheus metric label name (e.g., "username", "email", "team")
Label string `json:"label"`
// Headers is a list of HTTP header names to check, in order of preference
// The first non-empty header value found will be used
Headers []string `json:"headers"`
}

var (
userRequestCounter *prometheus.CounterVec
configuredLabels []string
configuredHeaderRules []UserMetricsConfig
initOnce sync.Once

// Built-in labels that are always included for request context
builtInLabels = []string{LabelHandler, LabelCluster, LabelQuery, LabelNamespace, LabelKind, LabelName, LabelLookback}
)

// GetDefaultUserMetricsConfig returns the default configuration for user metrics (username and user_agent)
func GetDefaultUserMetricsConfig() []UserMetricsConfig {
return []UserMetricsConfig{
{
Label: "username",
Headers: []string{"X-Username", "x-username"},
},
{
Label: "user_agent",
Headers: []string{"User-Agent"},
},
}
}

// InitUserMetrics initializes the user metrics with the given configuration
// This should be called once during application startup before serving requests
// If config is nil or empty, default configuration will be used
func InitUserMetrics(config []UserMetricsConfig) {
initOnce.Do(func() {
// Use default config if none provided
if len(config) == 0 {
config = GetDefaultUserMetricsConfig()
glog.Infof("No user metrics configuration provided, using defaults")
}

// Store the configuration
configuredHeaderRules = config

// Start with built-in labels for request context
labelSet := make(map[string]bool)
for _, label := range builtInLabels {
labelSet[label] = true
}

// Add header-based labels from configuration
for _, rule := range config {
if rule.Label != "" {
labelSet[rule.Label] = true
}
}

// Sort labels for consistent ordering
configuredLabels = make([]string, 0, len(labelSet))
for label := range labelSet {
configuredLabels = append(configuredLabels, label)
}
sort.Strings(configuredLabels)

// Create the counter with configured labels
userRequestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: MetricUserRequests,
Help: "Counter of requests broken out by user and request information.",
},
configuredLabels,
)

// Register with Prometheus
prometheus.MustRegister(userRequestCounter)

glog.Infof("User metrics initialized with labels: %v", configuredLabels)
glog.Infof(" Built-in request labels: %v", builtInLabels)
for _, rule := range config {
glog.Infof(" Header label '%s': headers=%v", rule.Label, rule.Headers)
}
})
}

// ============================================================================
// Header Information Extraction Functions
// ============================================================================

// getHeaderWithFallback tries multiple header names and returns the first non-empty value
func getHeaderWithFallback(headers *http.Header, headerNames []string) string {
for _, headerName := range headerNames {
if value := headers.Get(headerName); value != "" {
return value
}
}
return ""
}

// extractRequesterInfo extracts user information from request headers based on configuration
func extractRequesterInfo(headers *http.Header) map[string]string {
requesterInfo := make(map[string]string)

for _, rule := range configuredHeaderRules {
if rule.Label != "" && len(rule.Headers) > 0 {
requesterInfo[rule.Label] = getHeaderWithFallback(headers, rule.Headers)
}
}

return requesterInfo
}

// mergeMaps merges two maps, with values from additionalTags taking precedence
func mergeMaps(base, additional map[string]string) map[string]string {
result := make(map[string]string)

for k, v := range base {
result[k] = v
}

for k, v := range additional {
result[k] = v
}

return result
}

// PublishHeaderMetrics publishes header metrics to the metrics server
func PublishHeaderMetrics(headers *http.Header, additionalTags map[string]string, enableUserMetrics bool) {
if !enableUserMetrics {
return
}

userInfo := extractRequesterInfo(headers)
allLabels := mergeMaps(userInfo, additionalTags)

// Build label values in the correct order
labelValues := make([]string, len(configuredLabels))
for i, label := range configuredLabels {
labelValues[i] = allLabels[label]
}

userRequestCounter.WithLabelValues(labelValues...).Inc()

// Log user request at V(2) verbosity level for debugging
glog.V(2).Infof("User request made with labels: %v", allLabels)
}
Loading
Loading