diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a59e6ae9d..8ea7e99e481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **Github Runner Scaler**: Improve URL construction and error handling ([#7495](https://github.com/kedacore/keda/pull/7495)) - **Github Runner Scaler**: Limit HTTP error response logging ([#7469](https://github.com/kedacore/keda/pull/7469)) - **Loki Scaler**: Limit HTTP error response logging ([#7469](https://github.com/kedacore/keda/pull/7469)) +- **Loki Scaler**: `serverAddress` now appends `/loki/api/v1/query` to the end of existing path instead of overriding ([#7648](https://github.com/kedacore/keda/pull/7648)) - **Metrics API Scaler**: Fix `aggregateFromKubeServiceEndpoints` using empty label selector that matched all EndpointSlices in the namespace instead of only the target service's ([#7641](https://github.com/kedacore/keda/issues/7641)) - **NATS JetStream Scaler**: URL-encode user input in monitoring URL construction ([#7483](https://github.com/kedacore/keda/pull/7483)) - **Prometheus Scaler**: Handle NaN results in the same manner as Inf ([#7475](https://github.com/kedacore/keda/issues/7475)) diff --git a/pkg/scalers/loki_scaler.go b/pkg/scalers/loki_scaler.go index c565eb9b11b..a77641f1d45 100644 --- a/pkg/scalers/loki_scaler.go +++ b/pkg/scalers/loki_scaler.go @@ -7,7 +7,9 @@ import ( "io" "net/http" "net/url" + "path" "strconv" + "strings" "github.com/go-logr/logr" v2 "k8s.io/api/autoscaling/v2" @@ -115,11 +117,10 @@ func (s *lokiScaler) GetMetricSpecForScaling(context.Context) []v2.MetricSpec { } func (s *lokiScaler) ExecuteLokiQuery(ctx context.Context) (float64, error) { - u, err := url.ParseRequestURI(s.metadata.ServerAddress) + u, err := getServerAddress(&s.metadata) if err != nil { return -1, err } - u.Path = "/loki/api/v1/query" u.RawQuery = url.Values{"query": []string{s.metadata.Query}}.Encode() req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) @@ -218,3 +219,26 @@ func (s *lokiScaler) GetMetricsAndActivity(ctx context.Context, metricName strin metric := GenerateMetricInMili(metricName, val) return []external_metrics.ExternalMetricValue{metric}, val > s.metadata.ActivationThreshold, nil } + +func getServerAddress(metadata *lokiMetadata) (url.URL, error) { + const lokiPath = "/loki/api/v1/query" + u, err := url.ParseRequestURI(metadata.ServerAddress) + if err != nil { + return url.URL{}, err + } + + u.Path = strings.TrimRight(u.Path, "/") + + if strings.HasSuffix(u.Path, lokiPath) { + return *u, nil + } + + if strings.HasPrefix(lokiPath, u.Path+"/") { + remaining := strings.TrimPrefix(lokiPath, u.Path) + u.Path = path.Join(u.Path, remaining) + } else { + u.Path = path.Join(u.Path, lokiPath) + } + + return *u, nil +} diff --git a/pkg/scalers/loki_scaler_test.go b/pkg/scalers/loki_scaler_test.go index a089f186dc0..2852aee4a07 100644 --- a/pkg/scalers/loki_scaler_test.go +++ b/pkg/scalers/loki_scaler_test.go @@ -250,3 +250,87 @@ func TestLokiScalerTenantHeader(t *testing.T) { _, err := scaler.ExecuteLokiQuery(context.TODO()) assert.NoError(t, err) } + +type getServerAddressTestData struct { + name string + serverAddress string + expectedPath string + isError bool +} + +var testGetServerAddressData = []getServerAddressTestData{ + { + name: "no path in URL", + serverAddress: "http://localhost:3100", + expectedPath: "/loki/api/v1/query", + isError: false, + }, + { + name: "URL with trailing slash", + serverAddress: "http://localhost:3100/", + expectedPath: "/loki/api/v1/query", + isError: false, + }, + { + name: "URL with partial loki path", + serverAddress: "http://localhost:3100/loki", + expectedPath: "/loki/api/v1/query", + isError: false, + }, + { + name: "URL with longer partial loki path", + serverAddress: "http://localhost:3100/loki/api/v1", + expectedPath: "/loki/api/v1/query", + isError: false, + }, + { + name: "URL with full loki path already present", + serverAddress: "http://localhost:3100/loki/api/v1/query", + expectedPath: "/loki/api/v1/query", + isError: false, + }, + { + name: "URL with full loki path and trailing slash", + serverAddress: "http://localhost:3100/loki/api/v1/query/", + expectedPath: "/loki/api/v1/query", + isError: false, + }, + { + name: "URL with custom prefix", + serverAddress: "http://localhost:3100/custom", + expectedPath: "/custom/loki/api/v1/query", + isError: false, + }, + { + name: "URL with custom prefix and full loki path already present", + serverAddress: "http://localhost:3100/custom/loki/api/v1/query", + expectedPath: "/custom/loki/api/v1/query", + isError: false, + }, + { + name: "URL with partial loki path not on segment boundary", + serverAddress: "http://localhost:3100/lo", + expectedPath: "/lo/loki/api/v1/query", + isError: false, + }, + { + name: "invalid URL", + serverAddress: "not-a-url", + isError: true, + }, +} + +func TestGetServerAddress(t *testing.T) { + for _, testData := range testGetServerAddressData { + t.Run(testData.name, func(t *testing.T) { + meta := &lokiMetadata{ServerAddress: testData.serverAddress} + u, err := getServerAddress(meta) + if testData.isError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, testData.expectedPath, u.Path) + }) + } +}