Skip to content
Open
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
63 changes: 63 additions & 0 deletions router/cmd/flightrecorder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# FlightRecorder

FlightRecorder uses the new [`flightrecorder`](https://go.dev/blog/flight-recorder) module from Go 1.25 to record trace data when a request takes longer than a specified threshold.

```go
package main

import (
routercmd "github.com/wundergraph/cosmo/router/cmd"
// Import your modules here
_ "github.com/wundergraph/cosmo/router/cmd/flightrecorder/module"
)

func main() {
routercmd.Main()
}
```

## Configuration

FlightRecorder module is configured as follows in the main router configuration file:

```yaml
modules:
flightRecorder:
outputPath: './flight_recorder_data'
requestLatencyRecordThreshold: 100
recordMultiple: true
```

### `outputPath`

The `outputPath` is the path where the flight recorder will store the data.

### `requestLatencyRecordThreshold`

The `requestLatencyRecordThreshold` is the threshold in milliseconds above which a trace will be recorded.

### `recordMultiple`

The `recordMultiple` is a boolean that indicates whether the flight recorder should record multiple traces or not. Defaults to `false`.

## Run the Router

Before you can run the router, you need to copy the `.env.example` to `.env` and adjust the values.

```bash
go run ./cmd/flightrecorder/main.go
```



## Build your own Router

```bash
go build -o router ./cmd/flightrecorder/main.go
```

## Run tests

Tests for this module can be found within the [integration tests](../router-tests/module).

_All commands are run from the root of the router directory._
10 changes: 10 additions & 0 deletions router/cmd/flightrecorder/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
routercmd "github.com/wundergraph/cosmo/router/cmd"
_ "github.com/wundergraph/cosmo/router/cmd/flightrecorder/module"
)

func main() {
routercmd.Main()
}
136 changes: 136 additions & 0 deletions router/cmd/flightrecorder/module/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package module

import (
"fmt"
"net/http"
"os"
"path/filepath"
"runtime/trace"
"time"

"github.com/wundergraph/cosmo/router/core"
"go.uber.org/zap"
)

const flightRecorderID = "flightRecorder"

func init() {
core.RegisterModule(&FlightRecorder{})
}

type FlightRecorder struct {
OutputPath string `mapstructure:"outputPath"`
RecordMultiple bool `mapstructure:"recordMultiple"`
RequestLatencyRecordThreshold uint64 `mapstructure:"requestLatencyRecordThreshold"`

requestLatencyRecordThresholdDuration time.Duration

fl *trace.FlightRecorder

Logger *zap.Logger
}

func (m *FlightRecorder) Provision(ctx *core.ModuleContext) error {
// Assign the logger to the module for non-request related logging
m.Logger = ctx.Logger

m.Logger.Info("Setting up flight recorder")

if m.RequestLatencyRecordThreshold <= 0 {
return fmt.Errorf("request latency threshold must be greater than 0")
}

if m.OutputPath == "" {
return fmt.Errorf("output path must be specified")
}

m.requestLatencyRecordThresholdDuration = time.Duration(m.RequestLatencyRecordThreshold) * time.Millisecond

// Create output directory if it doesn't exist
if err := os.MkdirAll(m.OutputPath, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}

// 10MB minimum
var maxBytes uint64 = 10 * 1024 * 1024

// We actually want ~10MB/s of MinAge
// 1000ms = 1 second, 1000 is close enough to 1024
// sub in the uint milliseconds count for one of the factors
// if it would result in a value greater than default maxBytes
if m.RequestLatencyRecordThreshold*2 > 1024 {
maxBytes = (m.RequestLatencyRecordThreshold * 2) * 1024 * 10
}

m.fl = trace.NewFlightRecorder(trace.FlightRecorderConfig{
MinAge: m.requestLatencyRecordThresholdDuration,
MaxBytes: maxBytes,
})

if err := m.fl.Start(); err != nil {
return fmt.Errorf("failed to start flight recorder: %w", err)
}

return nil
}

func (m *FlightRecorder) Cleanup() error {
m.fl.Stop()

return nil
}

func (m *FlightRecorder) RouterOnRequest(ctx core.RequestContext, next http.Handler) {
start := time.Now()

next.ServeHTTP(ctx.ResponseWriter(), ctx.Request())

requestDuration := time.Since(start)

if m.fl.Enabled() && requestDuration > m.requestLatencyRecordThresholdDuration {
operation := ctx.Operation()

m.Logger.Warn("Request took longer than threshold", zap.Duration("duration", requestDuration), zap.String("operation", operation.Name()))

m.RecordTrace(operation.Name())
}
}

func (m *FlightRecorder) RecordTrace(operationName string) {
// Generate timestamped filename
filename := fmt.Sprintf("trace-%s-%s.out", operationName, time.Now().Format("2006-01-02-15-04-05"))

// Create the file
file, err := os.Create(filepath.Join(m.OutputPath, filename))
if err != nil {
m.Logger.Error("failed to create trace file", zap.Error(err))
return
}
defer file.Close()

_, err = m.fl.WriteTo(file)
if err != nil {
m.Logger.Error("Failed to record request", zap.Error(err))
}

if !m.RecordMultiple {
m.fl.Stop()
}
}

func (m *FlightRecorder) Module() core.ModuleInfo {
return core.ModuleInfo{
ID: flightRecorderID,
Priority: 1,
New: func() core.Module {
return &FlightRecorder{}
},
}
}

// Interface guard
var (
_ core.RouterOnRequestHandler = (*FlightRecorder)(nil)
_ core.Provisioner = (*FlightRecorder)(nil)
_ core.Cleaner = (*FlightRecorder)(nil)
)
Loading