Skip to content

mpyw/ctxweaver

Repository files navigation

ctxweaver

Go Reference CI Codecov Go Report Card License: MIT

Note

This project was written by AI (Claude Code).

A Go code generator that weaves statements into functions receiving context-like parameters.

Overview

ctxweaver automatically inserts or updates statements at the beginning of functions that receive context.Context or other context-carrying types. This is useful for:

  • APM instrumentation: Automatically add tracing segments to all context-aware functions
  • Logging setup: Insert structured logging initialization
  • Metrics collection: Add timing or counting instrumentation
  • Custom middleware: Any pattern that needs to run at function entry with context

How It Works

// Before: A function receiving context
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
    // business logic...
}

// After: ctxweaver inserts your template at the top
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
    defer newrelic.FromContext(ctx).StartSegment("myapp.(*Service).ProcessOrder").End()

    // business logic...
}

The inserted statement is fully customizable via Go templates.

Installation & Usage

go install github.com/mpyw/ctxweaver/cmd/ctxweaver@latest
ctxweaver ./...

Using go tool (Go 1.24+)

# Add to go.mod as a tool dependency
go get -tool github.com/mpyw/ctxweaver/cmd/ctxweaver@latest

# Run via go tool
go tool ctxweaver ./...

Using go run

go run github.com/mpyw/ctxweaver/cmd/ctxweaver@latest ./...

Caution

To prevent supply chain attacks, pin to a specific version tag instead of @latest in CI/CD pipelines (e.g., @v0.6.3).

Configuration

ctxweaver uses a YAML configuration file. Create ctxweaver.yaml in your project root:

# ctxweaver.yaml
template: |
  defer newrelic.FromContext({{.Ctx}}).StartSegment({{.FuncName | quote}}).End()

imports:
  - github.com/newrelic/go-agent/v3/newrelic

packages:
  patterns:
    - ./...

test: false

See ctxweaver.example.yaml for a complete example with all options.

Configuration Options

Option Type Required Default Description
template string | {file: string} Go template for the statement to insert (inline or file path)
imports []string [] Import paths to add when statement is inserted
packages.patterns []string Package patterns to process (overridden by CLI args)
packages.regexps.only []string [] Only process packages matching these regex patterns
packages.regexps.omit []string [] Skip packages matching these regex patterns
functions.types []FuncType ["function", "method"] Enum: "function" | "method"
functions.scopes []FuncScope ["exported", "unexported"] Enum: "exported" | "unexported"
functions.regexps.only []string [] Only process functions matching these regex patterns
functions.regexps.omit []string [] Skip functions matching these regex patterns
test bool false Whether to process test files (overridden by -test flag)
carriers []Carrier | CarriersConfig [] Context carrier configuration (see Custom Carriers)
hooks.pre []string [] Shell commands to run before processing
hooks.post []string [] Shell commands to run after processing

Note

  • template can be an inline string or an object with file key pointing to a template file.
  • CLI override behavior:
    • Package patterns (CLI args): Override packages.patterns when provided
    • -test flag: Override test config when explicitly passed

Package Filtering

Control which packages are processed using packages.patterns and regex filters:

packages:
  patterns:
    - ./...
  regexps:
    # Only process packages matching these patterns (empty = all)
    only:
      - /handler/
      - /service/
    # Skip packages matching these patterns
    omit:
      - /internal/
      - /mock/
      - _test$

Regex patterns are matched against the full import path (e.g., github.com/user/repo/internal/util).

Function Filtering

Control which functions are processed using type, scope, and regex filters:

functions:
  # Filter by function type (enum, default: both)
  types:
    - function  # "function": Top-level functions without receivers
    - method    # "method": Methods with receivers

  # Filter by export scope (enum, default: both)
  scopes:
    - exported    # "exported": Public functions (e.g., GetUser)
    - unexported  # "unexported": Private functions (e.g., parseInput)

  # Regex filters on function names
  regexps:
    only:
      - ^Handle   # Only functions starting with "Handle"
    omit:
      - Helper$   # Skip functions ending with "Helper"

Example: Only instrument exported handlers

functions:
  types: [function]
  scopes: [exported]
  regexps:
    only: [^Handle]

Example: Skip test helpers and mocks

functions:
  regexps:
    omit:
      - Mock
      - Helper$
      - ^setup

Flags

Flag Default Description
-config ctxweaver.yaml Path to configuration file
-dry-run false Print changes without writing files
-verbose false Print processed files
-silent false Suppress all output except errors
-test false Process test files (*_test.go)
-remove false Remove generated statements instead of adding them
-no-hooks false Skip pre/post hooks defined in config

Examples

# Use default config file (ctxweaver.yaml)
ctxweaver ./...

# Use custom config file
ctxweaver -config=.ctxweaver.yaml ./...

# Dry run - preview changes
ctxweaver -dry-run -verbose ./...

# Include test files
ctxweaver -test ./...

# Remove previously inserted statements
ctxweaver -remove ./...

# Skip hooks (useful in CI)
ctxweaver -no-hooks ./...

Tip

Refreshing statements after template changes: When you modify your template, ctxweaver detects existing statements via skeleton matching. If the template structure changes significantly, old statements may not be recognized and will remain alongside newly inserted ones.

To cleanly refresh all statements after a template change:

# Step 1: Remove with the OLD template still in config
ctxweaver -remove ./...

# Step 2: Update your template in ctxweaver.yaml

# Step 3: Re-run to insert with the NEW template
ctxweaver ./...

Template System

Tip

For Go text/template syntax guide, see: https://docs.gomplate.ca/syntax/

Available Variables

Variable Type Description
{{.Ctx}} string Expression to access context.Context
{{.CtxVar}} string Name of the context parameter variable
{{.FuncName}} string Fully qualified function name
{{.PackageName}} string Package name
{{.PackagePath}} string Full import path of the package
{{.FuncBaseName}} string Function name without package/receiver
{{.ReceiverType}} string Receiver type name (empty if not a method)
{{.ReceiverVar}} string Receiver variable name (empty if not a method)
{{.IsMethod}} bool Whether this is a method
{{.IsPointerReceiver}} bool Whether the receiver is a pointer
{{.IsGenericFunc}} bool Whether the function has type parameters
{{.IsGenericReceiver}} bool Whether the receiver type has type parameters

FuncName Format

{{.FuncName}} provides a fully qualified function name in the following format:

Type Format Example
Function pkg.Func service.CreateUser
Method (pointer receiver) pkg.(*Type).Method service.(*UserService).GetByID
Method (value receiver) pkg.Type.Method service.UserService.String
Generic function pkg.Func[...] service.Process[...]
Generic method (pointer) pkg.(*Type[...]).Method service.(*Container[...]).Get
Generic method (value) pkg.Type[...].Method service.Wrapper[...].Unwrap

Built-in Functions

Function Description
quote Wraps string in double quotes
backtick Wraps string in backticks

Basic Example

New Relic

template: |
  defer newrelic.FromContext({{.Ctx}}).StartSegment({{.FuncName | quote}}).End()
imports:
  - github.com/newrelic/go-agent/v3/newrelic

OpenTelemetry

template: |
  {{.CtxVar}}, span := otel.Tracer({{.PackageName | quote}}).Start({{.Ctx}}, {{.FuncName | quote}}); defer span.End()
imports:
  - go.opentelemetry.io/otel

Custom Function Name Format

If you need a different naming format, you can build it yourself using template variables. The following example replicates the default {{.FuncName}} behavior:

template: |
  {{- $receiver := .ReceiverType -}}
  {{- if .IsGenericReceiver -}}
    {{- $receiver = printf "%s[...]" .ReceiverType -}}
  {{- end -}}
  {{- $name := "" -}}
  {{- if .IsMethod -}}
    {{- if .IsPointerReceiver -}}
      {{- $name = printf "%s.(*%s).%s" .PackageName $receiver .FuncBaseName -}}
    {{- else -}}
      {{- $name = printf "%s.%s.%s" .PackageName $receiver .FuncBaseName -}}
    {{- end -}}
  {{- else -}}
    {{- if .IsGenericFunc -}}
      {{- $name = printf "%s.%s[...]" .PackageName .FuncBaseName -}}
    {{- else -}}
      {{- $name = printf "%s.%s" .PackageName .FuncBaseName -}}
    {{- end -}}
  {{- end -}}
  defer newrelic.FromContext({{.Ctx}}).StartSegment({{$name | quote}}).End()
imports:
  - github.com/newrelic/go-agent/v3/newrelic

This gives you full control over the naming format. Available variables for building custom names:

  • {{.PackageName}} - Package name (e.g., service)
  • {{.PackagePath}} - Full import path (e.g., github.com/example/myapp/pkg/service)
  • {{.ReceiverType}} - Receiver type name without generics (e.g., UserService, Container)
  • {{.FuncBaseName}} - Function/method name (e.g., GetByID)
  • {{.IsMethod}} - true if method, false if function
  • {{.IsPointerReceiver}} - true if pointer receiver
  • {{.IsGenericFunc}} - true if generic function (e.g., func Foo[T any]())
  • {{.IsGenericReceiver}} - true if generic receiver type (e.g., func (c *Container[T]) Method())

Built-in Context Carriers

ctxweaver recognizes the following types as context carriers (checks the first parameter only):

Type Accessor Notes
context.Context (none) Standard library
*http.Request .Context() Standard library
echo.Context .Request().Context() Echo framework
*cli.Context .Context urfave/cli
*cobra.Command .Context() Cobra
*gin.Context .Request.Context() Gin
*fiber.Ctx .Context() Fiber

Custom Carriers

Add custom carriers in your config file:

# Simple form: array of carriers (default carriers remain enabled)
carriers:
  - package: github.com/example/myapp/pkg/web
    type: Context
    accessor: .Ctx()

To disable default carriers and use only custom ones:

# Extended form: object with custom carriers and default toggle
carriers:
  custom:
    - package: github.com/example/myapp/pkg/web
      type: Context
      accessor: .Ctx()
  default: false  # Disable built-in carriers

Carrier Schema

Field Type Required Description
package string Import path of the package containing the type
type string Name of the type
accessor string Expression to extract context.Context (e.g., .Context())

CarriersConfig Schema (Extended Form)

Field Type Default Description
custom []Carrier [] Custom carrier definitions
default bool true Whether to include built-in default carriers

Directives

//ctxweaver:skip

Skip processing for a specific function or entire file:

//ctxweaver:skip
func legacyHandler(ctx context.Context) {
    // This function will not be modified
}

File-level skip (place at the top of the file):

//ctxweaver:skip

package legacy

// All functions in this file will be skipped

Existing Statement Detection

ctxweaver detects if a matching statement already exists and:

  1. Skips if the statement is up-to-date
  2. Updates if the function name in the statement doesn't match (e.g., after rename)
  3. Inserts if no matching statement exists

Currently, detection is specific to the defer XXX.StartSegment(ctx, "name").End() pattern.

Performance

ctxweaver uses golang.org/x/tools/go/packages to load type information efficiently:

  • Single load: All target packages are loaded in one pass
  • Accurate type resolution: Import paths are resolved correctly via type information
  • Comment preservation: Uses DST (Decorated Syntax Tree) to preserve comments

Import Management

ctxweaver automatically adds imports specified in the config file when statements are inserted.

Note

ctxweaver does not reorder or reformat existing imports. Use goimports or gci after ctxweaver if you need consistent import formatting.

Hooks

ctxweaver supports pre and post hooks to run shell commands before and after processing.

hooks:
  pre:
    - go mod tidy
  post:
    - gci write .
    - gofmt -w .

Pre Hooks

Commands run sequentially before processing. If any command fails (non-zero exit), processing is aborted and no files are modified. Useful for:

  • Running go mod tidy to ensure dependencies are up to date
  • Validating preconditions

Post Hooks

Commands run sequentially after processing. If any command fails, an error is reported but files have already been modified. Useful for:

  • Formatting code with gofmt
  • Organizing imports with gci or goimports
  • Running linters with auto-fix

Tip

ctxweaver adds imports but does not organize them. Since goimports only adds/removes imports without reordering, use tools like gci or golangci-lint run --fix (with gci enabled) to enforce consistent import ordering.

Recommended post hooks:

hooks:
  post:
    - gci write .
    - gofmt -w .

Or with golangci-lint:

hooks:
  post:
    - golangci-lint run --fix ./...

Use the -no-hooks flag to skip hooks (useful for CI or when running ctxweaver as part of a larger pipeline).

Documentation

  • Architecture - Technical specification and design decisions
  • CLAUDE.md - AI assistant guidance for development

Development

# Run tests
go test ./...

# Build CLI
go build -o bin/ctxweaver ./cmd/ctxweaver

# Run on a project
./bin/ctxweaver -config=ctxweaver.yaml ./...

Why ctxweaver?

For Go instrumentation, there are two main approaches: compile-time instrumentation (like Datadog Orchestrion) and code generation (like ctxweaver). Here's how they compare:

Feature ctxweaver Orchestrion
Approach Explicit code generation Compile-time AST injection
Output visibility Generated code in source files Hidden in build process
Comment preservation Yes (DST) N/A (no source modification)
Vendor lock-in None (template-based) Datadog by default
Custom templates Full control via Go templates Limited (//dd:span directive)
Framework support Built-in (Echo, Gin, Fiber, etc.) Via integrations
Reversibility ctxweaver -remove Remove toolchain config
Git diff Visible changes No source changes

When to Choose ctxweaver

  1. You want visible, reviewable code: Generated statements appear in your source files and git history. Code reviewers can see exactly what instrumentation is added.

  2. You need full template control: Define exactly what gets inserted using Go templates. Not limited to predefined patterns.

  3. You want vendor independence: Works with any APM (New Relic, OpenTelemetry, custom solutions). No SDK lock-in.

  4. You use context-carrying frameworks: Built-in support for Echo, Gin, Fiber, Cobra, urfave/cli context types.

  5. You want idempotent updates: Re-running ctxweaver updates existing statements (e.g., after function rename) without duplication.

When to Choose Orchestrion

  1. You prefer zero source changes: Instrumentation happens at compile time with no visible code modifications.

  2. You use Datadog: Native integration with Datadog APM and ASM.

  3. You want automatic library instrumentation: Orchestrion can instrument third-party library calls automatically.

Note

Traditional AOP libraries (gogap/aop, AspectGo) exist but are largely unmaintained. Go's culture favors explicit code over implicit magic, which is why ctxweaver generates visible source code rather than hiding instrumentation in the build process.

Related Tools

License

MIT License

About

A Go code generator that weaves statements into functions receiving context-like parameters

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •