frontend/pkg/frontend/middleware_logging.go (118 lines of code) (raw):

// Copyright 2025 Microsoft Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package frontend import ( "context" "fmt" "io" "log/slog" "net/http" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/Azure/ARO-HCP/internal/api" "github.com/Azure/ARO-HCP/internal/tracing" ) type LoggingReadCloser struct { io.ReadCloser bytesRead int } func (rc *LoggingReadCloser) Read(b []byte) (int, error) { n, err := rc.ReadCloser.Read(b) rc.bytesRead += n return n, err } type LoggingResponseWriter struct { http.ResponseWriter statusCode int bytesWritten int } func (w *LoggingResponseWriter) Write(b []byte) (int, error) { n, err := w.ResponseWriter.Write(b) w.bytesWritten += n return n, err } func (w *LoggingResponseWriter) WriteHeader(statusCode int) { w.ResponseWriter.WriteHeader(statusCode) w.statusCode = statusCode } // MiddlewareLogging logs the HTTP request and response. func MiddlewareLogging(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { ctx := r.Context() logger := LoggerFromContext(ctx) // Capture the request and response data for logging. r.Body = &LoggingReadCloser{ReadCloser: r.Body} w = &LoggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK} startTime := time.Now() logger = logger.With( "request_method", r.Method, "request_path", r.URL.Path, "request_proto", r.Proto, "request_query", r.URL.RawQuery, "request_referer", r.Referer(), "request_remote_addr", r.RemoteAddr, "request_user_agent", r.UserAgent()) logger.Info("read request") next(w, r) logger.Info("send response", "body_read_bytes", r.Body.(*LoggingReadCloser).bytesRead, "body_written_bytes", w.(*LoggingResponseWriter).bytesWritten, "response_status_code", w.(*LoggingResponseWriter).statusCode, "duration", time.Since(startTime).Seconds()) } // MiddlewareLoggingPostMux extends the contextual logger with additional // attributes after the request has been matched by the ServeMux. func MiddlewareLoggingPostMux(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { ctx := r.Context() logger := LoggerFromContext(ctx) attrs := &attributes{ subscriptionID: r.PathValue(PathSegmentSubscriptionID), resourceGroup: r.PathValue(PathSegmentResourceGroupName), resourceName: r.PathValue(PathSegmentResourceName), } attrs.addToCurrentSpan(ctx) ctx = ContextWithLogger(ctx, attrs.extendLogger(logger)) r = r.WithContext(ctx) next(w, r) } type attributes struct { subscriptionID string resourceGroup string resourceName string } func (a *attributes) resourceID() string { if a.subscriptionID == "" || a.resourceGroup == "" || a.resourceName == "" { return "" } return fmt.Sprintf( "/subscriptions/%s/resourceGroups/%s/providers/%s/%s", a.subscriptionID, a.resourceGroup, api.ClusterResourceType, a.resourceName, ) } // extendLogger returns a new logger with additional Logging attributes based // on the wildcards from the matched pattern. func (a *attributes) extendLogger(logger *slog.Logger) *slog.Logger { var attrs []slog.Attr if a.subscriptionID != "" { attrs = append(attrs, slog.String("subscription_id", a.subscriptionID)) } if a.resourceGroup != "" { attrs = append(attrs, slog.String("resource_group", a.resourceGroup)) } if a.resourceName != "" { attrs = append(attrs, slog.String("resource_name", a.resourceName)) } if resourceID := a.resourceID(); resourceID != "" { attrs = append(attrs, slog.String("resource_id", resourceID)) } return slog.New(logger.Handler().WithAttrs(attrs)) } func (a *attributes) addToCurrentSpan(ctx context.Context) { span := trace.SpanFromContext(ctx) var attrs []attribute.KeyValue if a.subscriptionID != "" { attrs = append(attrs, tracing.SubscriptionIDKey.String(a.subscriptionID)) } if a.resourceGroup != "" { attrs = append(attrs, tracing.ResourceGroupNameKey.String(a.resourceGroup)) } if a.resourceName != "" { attrs = append(attrs, tracing.ResourceNameKey.String(a.resourceName)) } span.SetAttributes(attrs...) }