Note
This project was written by AI (Claude Code).
A Go code generator that weaves statements into functions receiving context-like parameters.
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
// 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.
Using go install
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).
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: falseSee ctxweaver.example.yaml for a complete example with all 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
templatecan be an inline string or an object withfilekey pointing to a template file.- CLI override behavior:
- Package patterns (CLI args): Override
packages.patternswhen provided -testflag: Overridetestconfig when explicitly passed
- Package patterns (CLI args): Override
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).
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| 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 |
# 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 ./...Tip
For Go text/template syntax guide, see: https://docs.gomplate.ca/syntax/
| 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}} 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 |
| Function | Description |
|---|---|
quote |
Wraps string in double quotes |
backtick |
Wraps string in backticks |
New Relic
template: |
defer newrelic.FromContext({{.Ctx}}).StartSegment({{.FuncName | quote}}).End()
imports:
- github.com/newrelic/go-agent/v3/newrelicOpenTelemetry
template: |
{{.CtxVar}}, span := otel.Tracer({{.PackageName | quote}}).Start({{.Ctx}}, {{.FuncName | quote}}); defer span.End()
imports:
- go.opentelemetry.io/otelIf 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/newrelicThis 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}}-trueif method,falseif function{{.IsPointerReceiver}}-trueif pointer receiver{{.IsGenericFunc}}-trueif generic function (e.g.,func Foo[T any]()){{.IsGenericReceiver}}-trueif generic receiver type (e.g.,func (c *Container[T]) Method())
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 |
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| 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()) |
| Field | Type | Default | Description |
|---|---|---|---|
custom |
[]Carrier |
[] |
Custom carrier definitions |
default |
bool |
true |
Whether to include built-in default carriers |
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 skippedctxweaver detects if a matching statement already exists and:
- Skips if the statement is up-to-date
- Updates if the function name in the statement doesn't match (e.g., after rename)
- Inserts if no matching statement exists
Currently, detection is specific to the defer XXX.StartSegment(ctx, "name").End() pattern.
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
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.
ctxweaver supports pre and post hooks to run shell commands before and after processing.
hooks:
pre:
- go mod tidy
post:
- gci write .
- gofmt -w .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 tidyto ensure dependencies are up to date - Validating preconditions
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
gciorgoimports - 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).
- Architecture - Technical specification and design decisions
- CLAUDE.md - AI assistant guidance for development
# Run tests
go test ./...
# Build CLI
go build -o bin/ctxweaver ./cmd/ctxweaver
# Run on a project
./bin/ctxweaver -config=ctxweaver.yaml ./...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 |
-
You want visible, reviewable code: Generated statements appear in your source files and git history. Code reviewers can see exactly what instrumentation is added.
-
You need full template control: Define exactly what gets inserted using Go templates. Not limited to predefined patterns.
-
You want vendor independence: Works with any APM (New Relic, OpenTelemetry, custom solutions). No SDK lock-in.
-
You use context-carrying frameworks: Built-in support for Echo, Gin, Fiber, Cobra, urfave/cli context types.
-
You want idempotent updates: Re-running ctxweaver updates existing statements (e.g., after function rename) without duplication.
-
You prefer zero source changes: Instrumentation happens at compile time with no visible code modifications.
-
You use Datadog: Native integration with Datadog APM and ASM.
-
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.
- goroutinectx - Goroutine context propagation linter
- zerologlintctx - Zerolog context propagation linter
- gormreuse - GORM instance reuse linter
MIT License