Skip to content
Merged
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
57 changes: 57 additions & 0 deletions router/cmd/flightrecorder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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.
Comment thread
endigma marked this conversation as resolved.

```go
package main

import (
routercmd "github.com/wundergraph/cosmo/router/cmd"
// Import your modules here
_ "github.com/wundergraph/cosmo/router/cmd/flightrecorder/module"
Comment thread
endigma marked this conversation as resolved.
)

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
Comment thread
endigma marked this conversation as resolved.
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. 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
```
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"`
Comment thread
ysmolski marked this conversation as resolved.
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 * 2,
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())
Comment thread
ysmolski marked this conversation as resolved.
}
}
Comment thread
endigma marked this conversation as resolved.

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()
}
Comment thread
endigma marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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