package metrics
import (
"lunar/engine/utils"
"regexp"
"strings"
"sync"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
)
const pathLabel = "path"
type LabeledEndpointManager struct {
mu sync.RWMutex
supportedLabelPatterns []string
}
// NewLabeledEndpointManager creates a new instance of LabeledEndpointManager
func NewLabeledEndpointManager(labeledEndpoints []string) *LabeledEndpointManager {
mng := &LabeledEndpointManager{}
mng.SetLabeledEndpoints(labeledEndpoints)
return mng
}
// SetLabeledEndpoints sets the labeled endpoints
func (m *LabeledEndpointManager) SetLabeledEndpoints(labeledEndpoints []string) {
m.mu.Lock()
defer m.mu.Unlock()
m.supportedLabelPatterns = nil
for _, label := range labeledEndpoints {
m.supportedLabelPatterns = append(m.supportedLabelPatterns, convertPatternToRegex(label))
}
log.Info().Msgf("Supported labeled endpoints: %v", labeledEndpoints)
}
// ExtractLabel checks if the specified URL is defined as a labeled endpoint
// and returns the attribute path label if found.
func (m *LabeledEndpointManager) ExtractLabel(url string) *attribute.KeyValue {
m.mu.RLock()
defer m.mu.RUnlock()
for _, pattern := range m.supportedLabelPatterns {
if matched, _ := regexp.MatchString(pattern, url); matched {
if path := utils.ExtractPath(url); path != "" {
pathAttribute := attribute.String(pathLabel, path)
return &pathAttribute
}
}
}
return nil
}
// convertPatternToRegex converts a URL definition with placeholders (e.g. "{param}")
// into a regex string that will match URLs where the value inside the curly braces
// can change. In this example, the placeholder is replaced with "([^/]+)", which
// matches one or more characters that are not a slash.
//
// The literal parts of the pattern are escaped so that any regex special characters
// (like "." or "-") are treated literally.
func convertPatternToRegex(pattern string) string {
// regex to find placeholders (everything between "{" and "}")
placeholderRegex := regexp.MustCompile(`\{[^}]+\}`)
var builder strings.Builder
lastIndex := 0
for _, match := range placeholderRegex.FindAllStringIndex(pattern, -1) {
start, end := match[0], match[1]
builder.WriteString(regexp.QuoteMeta(pattern[lastIndex:start]))
// Replace the placeholder with a regex capturing group.
// "([^/]+)" matches one or more characters (except the '/')
builder.WriteString("([^/]+)")
lastIndex = end
}
// Append any remaining literal text after the last placeholder.
builder.WriteString(regexp.QuoteMeta(pattern[lastIndex:]))
// Anchor the regex to match the entire string.
return "^" + builder.String() + "$"
}