Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
1364929
refactor(several): switch to klog
yuvaldekel Mar 16, 2026
9a95c88
feat(go.mod go.sum): add klog
yuvaldekel Mar 16, 2026
76f9ed2
feat(main.go): add klog custom format
yuvaldekel Mar 16, 2026
f2e7b29
fix(main.go): change slog opts var name
yuvaldekel Mar 16, 2026
15b839d
fead(routes.go): logging for enforceQueryValues
yuvaldekel Mar 16, 2026
2857c4a
feat(routes.go): add injectMatcher logging
yuvaldekel Mar 16, 2026
3e8e026
feat(routes.go, main.go): add logginf for ExtractLabel
yuvaldekel Mar 16, 2026
af556fb
fix change sle label valuees to list
yuvaldekel Mar 16, 2026
5d98bd1
fix(routes_test.go): fix label enforcer test
yuvaldekel Mar 16, 2026
01fd1ed
fix syntex error
yuvaldekel Mar 16, 2026
f33b2be
fix typo
yuvaldekel Mar 16, 2026
47e1d17
feat(enforce.go): add logging to enforce functions
yuvaldekel Mar 16, 2026
3837744
fix go formating error
yuvaldekel Mar 16, 2026
fcc4c25
fix go formatting
yuvaldekel Mar 16, 2026
44fe611
refactor(several): switch to slog
yuvaldekel Mar 20, 2026
444099d
chore(deps): go mod tidy
yuvaldekel Mar 20, 2026
1a4fff5
fix(routes.go): import slog
yuvaldekel Mar 20, 2026
e99b10a
fix(enforce.go main.go): double import
yuvaldekel Mar 20, 2026
459f3ad
fix typo enforce.go
yuvaldekel Mar 20, 2026
3222d71
feat(main.go): nolint errcheck promslogConfig Set
yuvaldekel Mar 20, 2026
485f5c9
fix(main.go) got lint fmt
yuvaldekel Mar 20, 2026
e760be6
chore(deps): go mod tidy
yuvaldekel Mar 20, 2026
3e73746
feat(routes.go): add deafult proxy logging
yuvaldekel Mar 20, 2026
bd110d1
add(routes.go): req to error handler
yuvaldekel Mar 20, 2026
cba9e1d
feat(utils.go): add debug logging for api error
yuvaldekel Mar 20, 2026
89e3682
fix(utils.go): undefined var
yuvaldekel Mar 20, 2026
6788419
feat(routes.go, utils.go): add dubgging logging for https requests
yuvaldekel Mar 20, 2026
fac05cc
fix go fmt
yuvaldekel Mar 20, 2026
4752282
fix go fmt
yuvaldekel Mar 21, 2026
cb43490
fix undefined var
yuvaldekel Mar 21, 2026
1507588
fix gofmt
yuvaldekel Mar 21, 2026
b3cde9f
fix gofmt
yuvaldekel Mar 21, 2026
5f4079a
git fmt
yuvaldekel Mar 21, 2026
a51bb7b
feat(routes.go, utils.go): add r to api error
yuvaldekel Mar 21, 2026
51439fb
fix(routes.go): var name mistake
yuvaldekel Mar 21, 2026
9cbade5
pass req to api error
yuvaldekel Mar 21, 2026
188e281
fix writer var name
yuvaldekel Mar 21, 2026
efa2ed5
fix api error r name
yuvaldekel Mar 21, 2026
f312581
fix syntex error
yuvaldekel Mar 21, 2026
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/oklog/run v1.2.0
github.com/prometheus/alertmanager v0.31.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5
github.com/prometheus/prometheus v0.310.0
gotest.tools/v3 v3.5.2
)
Expand Down Expand Up @@ -46,7 +47,6 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions injectproxy/enforce.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package injectproxy
import (
"errors"
"fmt"
"log/slog"
"slices"

"github.com/prometheus/prometheus/model/labels"
Expand Down Expand Up @@ -56,14 +57,17 @@ var (
func (ms *PromQLEnforcer) Enforce(q string) (string, error) {
expr, err := parser.ParseExpr(q)
if err != nil {
slog.Error("Failed to parse PromQL expression", "error", err, "query", q)
return "", fmt.Errorf("%w: %w", ErrQueryParse, err)
}

if err := ms.EnforceNode(expr); err != nil {
if errors.Is(err, ErrIllegalLabelMatcher) {
slog.Warn("Illegal label matcher encountered during enforcement", "query", q, "error", err)
return "", err
}

slog.Error("Failed to enforce label on AST node", "error", err, "query", q)
return "", fmt.Errorf("%w: %w", ErrEnforceLabel, err)
}

Expand Down Expand Up @@ -271,6 +275,7 @@ func (ms PromQLEnforcer) EnforceMatchers(targets []*labels.Matcher) ([]*labels.M
}

if !ok {
slog.Info("Label matcher conflict detected", "targetMatcher", target.String(), "injectedMatcher", matcher.String())
return res, fmt.Errorf("%w: label matcher %q conflicts with injected matcher %q", ErrIllegalLabelMatcher, target.String(), matcher.String())
}
}
Expand All @@ -282,6 +287,7 @@ func (ms PromQLEnforcer) EnforceMatchers(targets []*labels.Matcher) ([]*labels.M
// In both cases, the enforced matcher will be added after
// iterating on all the expression's matchers.
if matcher.Type == labels.MatchEqual || matcher.String() == target.String() {
slog.Info("Dropping existing label matcher in favor of injected matcher", "droppedMatcher", target.String(), "injectedMatcher", matcher.String())
continue
}

Expand Down
107 changes: 80 additions & 27 deletions injectproxy/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
Expand Down Expand Up @@ -54,8 +54,6 @@ type routes struct {
errorOnReplace bool
regexMatch bool
rulesWithActiveAlerts bool

logger *log.Logger
}

type options struct {
Expand Down Expand Up @@ -146,6 +144,26 @@ func WithRegexMatch() Option {
})
}

// loggingResponseWriter wraps http.ResponseWriter to capture the HTTP status code and prevent double-logging.
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}

// WriteHeader captures the status code before writing it to the underlying response writer.
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}

// Write ensures a default 200 OK status is captured if WriteHeader wasn't explicitly called.
func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
if lrw.statusCode == 0 {
lrw.statusCode = http.StatusOK
}
return lrw.ResponseWriter.Write(b)
}

// mux abstracts away the behavior we expect from the http.ServeMux type in this package.
type mux interface {
http.Handler
Expand Down Expand Up @@ -223,14 +241,16 @@ type ExtractLabeler interface {
// HTTPFormEnforcer enforces a label value extracted from the HTTP form and query parameters.
type HTTPFormEnforcer struct {
ParameterName string
Label string
}

// ExtractLabel implements the ExtractLabeler interface.
func (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labelValues, err := hff.getLabelValues(r)
if err != nil {
prometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest)
slog.Error("Failed to extract labels from PostForm", "error", err, "source", "queryParameter", "parameter", hff.ParameterName)
prometheusAPIError(w, r, humanFriendlyErrorMessage(err), http.StatusBadRequest)
return
}

Expand All @@ -242,7 +262,8 @@ func (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
// Remove the param from the PostForm.
if r.Method == http.MethodPost {
if err := r.ParseForm(); err != nil {
prometheusAPIError(w, fmt.Sprintf("Failed to parse the PostForm: %v", err), http.StatusInternalServerError)
slog.Error("Failed to parse the PostForm", "error", err, "source", "queryParameter", "parameter", hff.ParameterName)
prometheusAPIError(w, r, fmt.Sprintf("Failed to parse the PostForm: %v", err), http.StatusInternalServerError)
return
}
if r.PostForm.Get(hff.ParameterName) != "" {
Expand All @@ -255,6 +276,7 @@ func (hff HTTPFormEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
}
}

slog.Info("Extracted label from PostForm", "source", "parameter", "queryParameter", hff.ParameterName, "label", hff.Label, "values", labelValues)
next.ServeHTTP(w, r.WithContext(WithLabelValues(r.Context(), labelValues)))
})
}
Expand All @@ -276,6 +298,7 @@ func (hff HTTPFormEnforcer) getLabelValues(r *http.Request) ([]string, error) {
// HTTPHeaderEnforcer enforces a label value extracted from the HTTP headers.
type HTTPHeaderEnforcer struct {
Name string
Label string
ParseListSyntax bool
}

Expand All @@ -284,10 +307,12 @@ func (hhe HTTPHeaderEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labelValues, err := hhe.getLabelValues(r)
if err != nil {
prometheusAPIError(w, humanFriendlyErrorMessage(err), http.StatusBadRequest)
slog.Error("Failed to extract labels from header", "error", err, "source", "header", "header", hhe.Name)
prometheusAPIError(w, r, humanFriendlyErrorMessage(err), http.StatusBadRequest)
return
}

slog.Info("Extracted label from header", "source", "header", "header", hhe.Name, "label", hhe.Label, "values", labelValues)
next.ServeHTTP(w, r.WithContext(WithLabelValues(r.Context(), labelValues)))
})
}
Expand All @@ -309,12 +334,16 @@ func (hhe HTTPHeaderEnforcer) getLabelValues(r *http.Request) ([]string, error)
}

// StaticLabelEnforcer enforces a static label value.
type StaticLabelEnforcer []string
type StaticLabelEnforcer struct {
Label string
LabelValues []string
}

// ExtractLabel implements the ExtractLabeler interface.
func (sle StaticLabelEnforcer) ExtractLabel(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next(w, r.WithContext(WithLabelValues(r.Context(), sle)))
slog.Info("Extracted static label", "source", "static", "label", sle.Label, "values", sle.LabelValues)
next(w, r.WithContext(WithLabelValues(r.Context(), sle.LabelValues)))
})
}

Expand All @@ -338,7 +367,6 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o
errorOnReplace: opt.errorOnReplace,
regexMatch: opt.regexMatch,
rulesWithActiveAlerts: opt.rulesWithActiveAlerts,
logger: log.Default(),
}
mux := newStrictMux(newInstrumentedMux(http.NewServeMux(), opt.registerer))

Expand Down Expand Up @@ -451,13 +479,23 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o
proxy.Transport = transport
proxy.ModifyResponse = r.ModifyResponse
proxy.ErrorHandler = r.errorHandler
proxy.ErrorLog = log.Default()

proxy.ErrorLog = slog.NewLogLogger(slog.Default().Handler(), slog.LevelError)
return r, nil
}

func (r *routes) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}

r.mux.ServeHTTP(lrw, req)

// log if it's an error AND it hasn't been logged yet
if lrw.statusCode >= 400 && lrw.Header().Get("X-Proxy-Error-Logged") == "" {
slog.Debug("HTTP request failed",
"method", req.Method,
"path", req.URL.Path,
"status", lrw.statusCode,
)
}
}

func (r *routes) ModifyResponse(resp *http.Response) error {
Expand All @@ -470,8 +508,15 @@ func (r *routes) ModifyResponse(resp *http.Response) error {
return m(resp)
}

func (r *routes) errorHandler(rw http.ResponseWriter, _ *http.Request, err error) {
r.logger.Printf("http: proxy error: %v", err)
func (r *routes) errorHandler(rw http.ResponseWriter, req *http.Request, err error) {
rw.Header().Set("X-Proxy-Error-Logged", "true")

slog.Error("HTTP proxy error",
"error", err,
"path", req.URL.Path,
"method", req.Method,
)

if errors.Is(err, errModifyResponseFailed) {
rw.WriteHeader(http.StatusBadRequest)
}
Expand All @@ -492,7 +537,7 @@ func enforceMethods(h http.HandlerFunc, methods ...string) http.HandlerFunc {
func (r *routes) errorIfRegexpMatch(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if r.regexMatch {
prometheusAPIError(w, "support for regex match not implemented", http.StatusNotImplemented)
prometheusAPIError(w, req, "support for regex match not implemented", http.StatusNotImplemented)
return
}

Expand Down Expand Up @@ -551,7 +596,7 @@ func (r *routes) passthrough(w http.ResponseWriter, req *http.Request) {
func (r *routes) query(w http.ResponseWriter, req *http.Request) {
matcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...)
if err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
return
}

Expand All @@ -566,11 +611,11 @@ func (r *routes) query(w http.ResponseWriter, req *http.Request) {
if err != nil {
switch {
case errors.Is(err, ErrIllegalLabelMatcher):
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
case errors.Is(err, ErrQueryParse):
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
case errors.Is(err, ErrEnforceLabel):
prometheusAPIError(w, err.Error(), http.StatusInternalServerError)
prometheusAPIError(w, req, err.Error(), http.StatusInternalServerError)
}

return
Expand All @@ -581,17 +626,17 @@ func (r *routes) query(w http.ResponseWriter, req *http.Request) {
// Enforce the query in the POST body if needed.
if req.Method == http.MethodPost {
if err := req.ParseForm(); err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
}
q, found2, err = enforceQueryValues(e, req.PostForm)
if err != nil {
switch {
case errors.Is(err, ErrIllegalLabelMatcher):
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
case errors.Is(err, ErrQueryParse):
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
case errors.Is(err, ErrEnforceLabel):
prometheusAPIError(w, err.Error(), http.StatusInternalServerError)
prometheusAPIError(w, req, err.Error(), http.StatusInternalServerError)
}

return
Expand All @@ -615,15 +660,18 @@ func enforceQueryValues(e *PromQLEnforcer, v url.Values) (values string, noQuery
// If no values were given or no query is present,
// e.g. because the query came in the POST body
// but the URL query string was passed, then finish early.
if v.Get(queryParam) == "" {
origQuery := v.Get(queryParam)
if origQuery == "" {
return v.Encode(), false, nil
}

q, err := e.Enforce(v.Get(queryParam))
q, err := e.Enforce(origQuery)
if err != nil {
slog.Error("Failed to enforce query", "error", err, "query", origQuery)
return "", true, err
}

slog.Info("Successfully enforced query", "originalQuery", origQuery, "enforcedQuery", q)
v.Set(queryParam, q)

return v.Encode(), true, nil
Expand Down Expand Up @@ -668,13 +716,13 @@ func (r *routes) newLabelMatcher(vals ...string) (*labels.Matcher, error) {
func (r *routes) matcher(w http.ResponseWriter, req *http.Request) {
matcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...)
if err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
return
}

q := req.URL.Query()
if err := injectMatcher(q, matcher); err != nil {
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
prometheusAPIError(w, req, err.Error(), http.StatusBadRequest)
return
}

Expand All @@ -700,22 +748,27 @@ func (r *routes) matcher(w http.ResponseWriter, req *http.Request) {
}

func injectMatcher(q url.Values, matcher *labels.Matcher) error {
origMatchers := append([]string(nil), q[matchersParam]...)

matchers := q[matchersParam]
if len(matchers) == 0 {
q.Set(matchersParam, matchersToString(matcher))
slog.Info("Successfully injected matcher", "originalMatchers", origMatchers, "enforcedMatchers", q[matchersParam])
return nil
}

// Inject label into existing matchers.
for i, m := range matchers {
ms, err := parser.ParseMetricSelector(m)
if err != nil {
slog.Error("Failed to parse metric selector during matcher injection", "error", err, "matcher", m)
return err
}

matchers[i] = matchersToString(append(ms, matcher)...)
}
q[matchersParam] = matchers
slog.Info("Successfully injected matchers", "originalMatchers", origMatchers, "enforcedMatchers", q[matchersParam])

return nil
}
Expand Down
Loading