Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 14 additions & 34 deletions src/acceptance/assets/app/go_app/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@ go 1.24.3

require (
github.com/cloudfoundry-community/go-cfenv v1.18.0
github.com/fgrosse/zaptest v1.2.1
github.com/gin-contrib/zap v1.1.5
github.com/gin-gonic/gin v1.10.1
github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.1.0
github.com/go-logr/logr v1.4.2
github.com/go-logr/zapr v1.3.0
github.com/json-iterator/go v1.1.12
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2
github.com/mitchellh/mapstructure v1.5.0
Expand All @@ -19,56 +14,41 @@ require (
github.com/onsi/gomega v1.37.0
github.com/prometheus/procfs v0.16.1
github.com/steinfletcher/apitest v1.6.0
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.57.0
go.opentelemetry.io/otel v1.32.0
go.opentelemetry.io/otel/metric v1.32.0
go.opentelemetry.io/otel/sdk v1.32.0
go.opentelemetry.io/otel/trace v1.32.0
go.uber.org/zap v1.27.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
)

require (
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
golang.org/x/mod v0.26.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
124 changes: 32 additions & 92 deletions src/acceptance/assets/app/go_app/go.sum

Large diffs are not rendered by default.

170 changes: 129 additions & 41 deletions src/acceptance/assets/app/go_app/internal/app/app.go
Original file line number Diff line number Diff line change
@@ -1,64 +1,153 @@
package app

import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"runtime/debug"
"time"

ginzap "github.com/gin-contrib/zap"
"github.com/gin-gonic/gin"
"github.com/go-logr/zapr"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

func Router(logger *zap.Logger, timewaster TimeWaster, memoryTest MemoryGobbler,
cpuTest CPUWaster, diskOccupier DiskOccupier, customMetricTest CustomMetricClient) *gin.Engine {
r := gin.New()
// JSONResponse represents a JSON response structure
type JSONResponse map[string]interface{}

otel.SetTracerProvider(sdktrace.NewTracerProvider())
otel.SetTextMapPropagator(propagation.TraceContext{})
r.Use(otelgin.Middleware("acceptance-tests-go-app"))

r.Use(ginzap.GinzapWithConfig(logger, &ginzap.Config{
TimeFormat: time.RFC3339,
UTC: true,
Context: ginzap.Fn(func(c *gin.Context) []zapcore.Field {
fields := []zapcore.Field{}
// log CF ID
if requestID := c.Request.Header.Get("X-Vcap-Request-Id"); requestID != "" {
fields = append(fields, zap.String("vcap_request_id", requestID))
// writeJSON writes a JSON response with the given status code
func writeJSON(w http.ResponseWriter, statusCode int, data JSONResponse) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(data)
}

// loggingMiddleware provides request logging functionality
func loggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

// Wrap ResponseWriter to capture status code
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

attrs := []slog.Attr{
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote_addr", r.RemoteAddr),
slog.String("user_agent", r.UserAgent()),
}

// Log CF ID
if requestID := r.Header.Get("X-Vcap-Request-Id"); requestID != "" {
attrs = append(attrs, slog.String("vcap_request_id", requestID))
}
if passport := c.Request.Header.Get("SAP-PASSPORT"); passport != "" {
fields = append(fields, zap.String("sap_passport", passport))
if passport := r.Header.Get("SAP-PASSPORT"); passport != "" {
attrs = append(attrs, slog.String("sap_passport", passport))
}
// support opentelemetry trace ID
fields = append(fields, zap.String("w3c_trace-id", trace.SpanFromContext(c.Request.Context()).SpanContext().TraceID().String()))

return fields
}),
}))
// Support OpenTelemetry trace ID
if span := trace.SpanFromContext(r.Context()); span.SpanContext().IsValid() {
attrs = append(attrs, slog.String("w3c_trace-id", span.SpanContext().TraceID().String()))
}

next.ServeHTTP(wrapped, r)

duration := time.Since(start)
attrs = append(attrs,
slog.Int("status_code", wrapped.statusCode),
slog.Duration("duration", duration),
)

logger.LogAttrs(r.Context(), slog.LevelInfo, "HTTP request", attrs...)
})
}
}

// responseWriter wraps http.ResponseWriter to capture status code
type responseWriter struct {
http.ResponseWriter
statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}

// recoveryMiddleware provides panic recovery functionality
func recoveryMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
logger.Error("Panic recovered",
slog.Any("error", err),
slog.String("stack", string(debug.Stack())),
slog.String("path", r.URL.Path),
slog.String("method", r.Method),
)
http.Error(w, "Internal Server Errorf", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
}

// otelMiddleware provides OpenTelemetry tracing
func otelMiddleware(next http.Handler) http.Handler {
tracer := otel.Tracer("acceptance-tests-go-app")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
ctx, span := tracer.Start(ctx, fmt.Sprintf("%s %s", r.Method, r.URL.Path))
defer span.End()

r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}

func Router(logger *slog.Logger, timewaster TimeWaster, memoryTest MemoryGobbler,
cpuTest CPUWaster, diskOccupier DiskOccupier, customMetricTest CustomMetricClient) http.Handler {
mux := http.NewServeMux()

// Set up OpenTelemetry
otel.SetTracerProvider(sdktrace.NewTracerProvider())
otel.SetTextMapPropagator(propagation.TraceContext{})

// Root routes
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
if err := writeJSON(w, http.StatusOK, JSONResponse{"name": "test-app"}); err != nil {
logger.Error("Failed to write JSON response", slog.Any("error", err))
}
})

mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
if err := writeJSON(w, http.StatusOK, JSONResponse{"status": "ok"}); err != nil {
logger.Error("Failed to write JSON response", slog.Any("error", err))
}
})

r.Use(ginzap.RecoveryWithZap(logger, true))
// Register test endpoints
MemoryTests(logger, mux, memoryTest)
ResponseTimeTests(logger, mux, timewaster)
CPUTests(logger, mux, cpuTest)
DiskTest(logger, mux, diskOccupier)
CustomMetricsTests(logger, mux, customMetricTest)

logr := zapr.NewLogger(logger)
// Apply middleware in order: recovery -> logging -> otel -> router
var handler http.Handler = mux
handler = otelMiddleware(handler)
handler = loggingMiddleware(logger)(handler)
handler = recoveryMiddleware(logger)(handler)

r.GET("/", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"name": "test-app"}) })
r.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) })
MemoryTests(logr, r.Group("/memory"), memoryTest)
ResponseTimeTests(logr, r.Group("/responsetime"), timewaster)
CPUTests(logr, r.Group("/cpu"), cpuTest)
DiskTest(r.Group("/disk"), diskOccupier)
CustomMetricsTests(logr, r.Group("/custom-metrics"), customMetricTest)
return r
return handler
}

func New(logger *zap.Logger, address string) *http.Server {
errorLog, _ := zap.NewStdLogAt(logger, zapcore.ErrorLevel)
func New(logger *slog.Logger, address string) *http.Server {
return &http.Server{
Addr: address,
Handler: Router(
Expand All @@ -72,6 +161,5 @@ func New(logger *zap.Logger, address string) *http.Server {
ReadTimeout: 5 * time.Second,
IdleTimeout: 2 * time.Second,
WriteTimeout: 30 * time.Second,
ErrorLog: errorLog,
}
}
10 changes: 8 additions & 2 deletions src/acceptance/assets/app/go_app/internal/app/app_suite_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package app_test

import (
"log/slog"
"testing"

"github.com/gin-gonic/gin"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestApp(t *testing.T) {
RegisterFailHandler(Fail)
gin.SetMode(gin.ReleaseMode)
RunSpecs(t, "App Suite")
}

// testLogger creates an slog logger that writes to GinkgoWriter for better test output
func testLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(GinkgoWriter, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
}
21 changes: 9 additions & 12 deletions src/acceptance/assets/app/go_app/internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"time"

"code.cloudfoundry.org/app-autoscaler-release/src/acceptance/assets/app/go_app/internal/app"
"github.com/fgrosse/zaptest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/steinfletcher/apitest"
Expand All @@ -25,16 +24,22 @@ var _ = Describe("Ginkgo/Server", func() {
})

Context("basic endpoint tests", func() {
apiTest := func() *apitest.APITest {
GinkgoHelper()
logger := testLogger()
return apitest.New().Handler(app.Router(logger, nil, nil, nil, nil, nil))
}

It("Root should respond correctly", func() {
apiTest(nil, nil, nil, nil).
apiTest().
Get("/").
Expect(t).
Status(http.StatusOK).
Body(`{"name":"test-app"}`).
End()
})
It("health", func() {
apiTest(nil, nil, nil, nil).
apiTest().
Get("/health").
Expect(t).
Status(http.StatusOK).
Expand All @@ -48,7 +53,7 @@ var _ = Describe("Ginkgo/Server", func() {
var client *http.Client
var port int
BeforeEach(func() {
logger := zaptest.LoggerWriter(GinkgoWriter)
logger := testLogger()
/* #nosec G102 -- CF apps run in a container */
l, err := net.Listen("tcp", ":0")
Expect(err).ToNot(HaveOccurred())
Expand All @@ -74,11 +79,3 @@ var _ = Describe("Ginkgo/Server", func() {

})
})

func apiTest(timeWaster app.TimeWaster, memoryGobbler app.MemoryGobbler, cpuWaster app.CPUWaster, customMetricClient app.CustomMetricClient) *apitest.APITest {
GinkgoHelper()
logger := zaptest.LoggerWriter(GinkgoWriter)

return apitest.New().
Handler(app.Router(logger, timeWaster, memoryGobbler, cpuWaster, nil, customMetricClient))
}
Loading
Loading