OpenFeature: Add tracing to feature flags service (#111895)

* Add tracing to feature flags service

* Apply review feedback

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Fix issues

---------

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
This commit is contained in:
Tania
2025-10-06 09:16:29 +02:00
committed by GitHub
co-authored by Dave Henderson
parent 64a483d112
commit 843956e795
3 changed files with 63 additions and 10 deletions
+16 -2
View File
@@ -2,6 +2,7 @@ package ofrep
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
@@ -14,14 +15,21 @@ import (
"strconv"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/util/proxyutil"
goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model"
)
func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(ctx, "ofrep.proxy.evalAllFlags")
defer span.End()
r = r.WithContext(ctx)
proxy, err := b.newProxy(ofrepPath)
if err != nil {
err = tracing.Error(span, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -61,9 +69,15 @@ func (b *APIBuilder) proxyAllFlagReq(isAuthedUser bool, w http.ResponseWriter, r
proxy.ServeHTTP(w, r)
}
func (b *APIBuilder) proxyFlagReq(flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
func (b *APIBuilder) proxyFlagReq(ctx context.Context, flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(ctx, "ofrep.proxy.evalFlag")
defer span.End()
r = r.WithContext(ctx)
proxy, err := b.newProxy(path.Join(ofrepPath, flagKey))
if err != nil {
err = tracing.Error(span, err)
b.logger.Error("Failed to create proxy", "key", flagKey, "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+27 -4
View File
@@ -10,6 +10,9 @@ import (
"net/url"
"github.com/gorilla/mux"
"github.com/grafana/grafana/pkg/infra/tracing"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -31,6 +34,8 @@ var _ builder.APIGroupBuilder = (*APIBuilder)(nil)
var _ builder.APIGroupRouteProvider = (*APIBuilder)(nil)
var _ builder.APIGroupVersionProvider = (*APIBuilder)(nil)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/registry/apis/ofrep")
const ofrepPath = "/ofrep/v1/evaluate/flags"
const namespaceMismatchMsg = "rejecting request with namespace mismatch"
@@ -240,7 +245,13 @@ func (b *APIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes {
}
func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalFlag")
defer span.End()
r = r.WithContext(ctx)
if !b.validateNamespace(r) {
_ = tracing.Errorf(span, namespaceMismatchMsg)
b.logger.Error(namespaceMismatchMsg)
http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized)
return
@@ -248,42 +259,54 @@ func (b *APIBuilder) oneFlagHandler(w http.ResponseWriter, r *http.Request) {
flagKey := mux.Vars(r)["flagKey"]
if flagKey == "" {
_ = tracing.Errorf(span, "flagKey parameter is required")
http.Error(w, "flagKey parameter is required", http.StatusBadRequest)
return
}
span.SetAttributes(attribute.String("flag_key", flagKey))
isAuthedReq := b.isAuthenticatedRequest(r)
span.SetAttributes(attribute.Bool("authenticated", isAuthedReq))
// Unless the request is authenticated, we only allow public flags evaluations
if !isAuthedReq && !isPublicFlag(flagKey) {
_ = tracing.Errorf(span, "unauthorized to evaluate flag: %s", flagKey)
b.logger.Error("Unauthorized to evaluate flag", "flagKey", flagKey)
http.Error(w, "unauthorized to evaluate flag", http.StatusUnauthorized)
return
}
if b.providerType == setting.GOFFProviderType {
b.proxyFlagReq(flagKey, isAuthedReq, w, r)
b.proxyFlagReq(ctx, flagKey, isAuthedReq, w, r)
return
}
b.evalFlagStatic(flagKey, w, r)
b.evalFlagStatic(ctx, flagKey, w)
}
func (b *APIBuilder) allFlagsHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "ofrep.handler.evalAllFlags")
defer span.End()
r = r.WithContext(ctx)
if !b.validateNamespace(r) {
_ = tracing.Errorf(span, namespaceMismatchMsg)
b.logger.Error(namespaceMismatchMsg)
http.Error(w, namespaceMismatchMsg, http.StatusUnauthorized)
return
}
isAuthedReq := b.isAuthenticatedRequest(r)
span.SetAttributes(attribute.Bool("authenticated", isAuthedReq))
if b.providerType == setting.GOFFProviderType {
b.proxyAllFlagReq(isAuthedReq, w, r)
b.proxyAllFlagReq(ctx, isAuthedReq, w, r)
return
}
b.evalAllFlagsStatic(isAuthedReq, w, r)
b.evalAllFlagsStatic(ctx, isAuthedReq, w)
}
func writeResponse(statusCode int, result any, logger log.Logger, w http.ResponseWriter) {
+20 -4
View File
@@ -1,19 +1,28 @@
package ofrep
import (
"context"
"net/http"
"github.com/grafana/grafana/pkg/infra/tracing"
goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model"
"go.opentelemetry.io/otel/attribute"
)
func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
result, err := b.staticEvaluator.EvalAllFlags(r.Context())
func (b *APIBuilder) evalAllFlagsStatic(ctx context.Context, isAuthedUser bool, w http.ResponseWriter) {
_, span := tracer.Start(ctx, "ofrep.static.evalAllFlags")
defer span.End()
result, err := b.staticEvaluator.EvalAllFlags(ctx)
if err != nil {
err = tracing.Error(span, err)
b.logger.Error("Failed to evaluate all static flags", "error", err)
http.Error(w, "failed to evaluate flags", http.StatusInternalServerError)
return
}
span.SetAttributes(attribute.Int("total_flags_count", len(result.Flags)))
if !isAuthedUser {
var publicOnly []goffmodel.OFREPFlagBulkEvaluateSuccessResponse
@@ -24,14 +33,21 @@ func (b *APIBuilder) evalAllFlagsStatic(isAuthedUser bool, w http.ResponseWriter
}
result.Flags = publicOnly
span.SetAttributes(attribute.Int("public_flags_count", len(publicOnly)))
}
writeResponse(http.StatusOK, result, b.logger, w)
}
func (b *APIBuilder) evalFlagStatic(flagKey string, w http.ResponseWriter, r *http.Request) {
result, err := b.staticEvaluator.EvalFlag(r.Context(), flagKey)
func (b *APIBuilder) evalFlagStatic(ctx context.Context, flagKey string, w http.ResponseWriter) {
_, span := tracer.Start(ctx, "ofrep.static.evalFlag")
defer span.End()
span.SetAttributes(attribute.String("flag_key", flagKey))
result, err := b.staticEvaluator.EvalFlag(ctx, flagKey)
if err != nil {
err = tracing.Error(span, err)
b.logger.Error("Failed to evaluate static flag", "key", flagKey, "error", err)
http.Error(w, "failed to evaluate flag", http.StatusInternalServerError)
return