As discussed in #62627, this issue proposed to extend the API of x/exp/trace to support programmatically creating trace events for testing and analysis purposes.

Note: Given that the code for this lives in internal/trace right now and is only exposed via x/exp/trace, I think this issue doesn't need to go through the official proposal process.

Overview

The proposal is to add an API using a MakeEvent constructor that takes a EventConfig struct. Below is an example for creating a goroutine state transition event:

details := MakeGoStateTransition(5, GoRunning, GoWaiting)
details.Stack = MakeStack([]StackFrame{
    {PC: 1, Func: "time.Sleep", File: "runtime.go", Line: 10},
    {PC: 2, Func: "main", File: "main.go", Line: 20},
})
details.Reason = "sleep"
ev, err := MakeEvent(EventConfig[StateTransition]{
    Time:      timestamp,
    Goroutine: 1,
    Proc:      2,
    Thread:    3,
    Details:   details,
})

A partially completed prototype of this API is available in CL 691815.

API

// MakeEvent creates a new trace event with the given configuration.
func MakeEvent[T EventDetails](c EventConfig[T]) (Event, error)

// EventConfig holds the data for constructing a trace event.
type EventConfig[T EventDetails] struct {
    Kind      EventKind
    Time      Time
    Goroutine GoID
    Proc      ProcID
    Thread    ThreadID
    Stack     Stack
    Details   T
}

// EventDetails is a union type of all event kind specific details.
type EventDetails interface {
    StateTransition | Metric | Label | Range | Task | Region | Log
}

Functional options alternative

An earlier version of this proposal used functional options. But during the performance and diagnostics meeting on 2025-07-31 @mknyszek suggested that a preference towards a struct based API that reuses the existing struct types in this package.

Click to expand to see the previous version of the proposal that used functional options Usage Example:
stk := []StackFrame{
    {PC: 1, Func: "time.Sleep", File: "runtime.go", Line: 10},
    {PC: 2, Func: "main", File: "main.go", Line: 20},
}

event, err := NewEvent(timestamp,
    // goid, procid, threadid
    WithSchedulingContext(1, 2, 3),
    // goid, from, to, reason, stack
    WithGoroutineTransition(5, GoRunning, GoWaiting, "sleep", stk),
)
API:
// NewEvent creates a new trace event at the specified time with the given
// options. Options are applied in order and validate compatibility with each
// other. Returns an error if options are incompatible or invalid.
func NewEvent(time Time, opts ...eventOption) (Event, error)

// eventOption applies an option to an event.
type eventOption func(*Event) error

// WithSchedulingContext sets the scheduling context for the event.
func WithSchedulingContext(goroutine GoID, proc ProcID, thread ThreadID) eventOption

// WithGoroutineTransition specializes the event into a StateTransition event.
// May overwrite the scheduling context goroutine and event stack.
func WithGoroutineTransition(goroutine GoID, from, to GoState, reason string, stack []StackFrame) eventOption

// WithMetric specializes the event into a Metric event.
func WithMetric(name string, value Value) eventOption

// WithRange specializes the event into a Range event. The scope may overwrite
// the scheduling context.
func WithRange(kind EventKind, name string, scope ResourceID) eventOption

// WithLabel specializes the event into a Label event. Only ResourceGoroutine is
// supported right now and it overwrites the scheduling context goroutine.
func WithLabel(label string, resource ResourceID) eventOption

// WithProcTransition specializes the event into a ProcTransition event.
func WithProcTransition(proc ProcID, from, to ProcState) eventOption

// WithSync specializes the event into a Sync event.
func WithSync(n int, clockSnapshot *ClockSnapshot, experimentalBatches map[string][]ExperimentalBatch) eventOption

// WithTask specializes the event into a Task event.
func WithTask(id, parent TaskID, taskType string) eventOption

// WithRegion specializes the event into a Region event.
func WithRegion(task TaskID, regionType string) eventOption

// WithLog specializes the event into a Log event.
func WithLog(task TaskID, category, message string) eventOption

// WithStackSample specializes the event into a StackSample event.
func WithStackSample(stack []StackFrame) eventOption

Comment From: felixge

cc @mknyszek @prattmic @rhysh @nsrip-dd @dominikh

Comment From: gabyhelp

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: felixge

@mknyszek I updated the proposal based on the discussions in the meeting yesterday. I've also updated the CL to make sure this would work (the code is still wrapping the old functional API I had before, but that was just the fastest way to get a struct based API working).

Also: During the call we discussed that this doesn't need to go through the official proposal process since it targets APIs that don't fall under the go1 promise for now (x/exp, internal/trace). Can somebody remove the label on the issue? Is there a preferred prefix for the title for this?

Comment From: mknyszek

@felixge I updated the issue title and labels to match where it's at right now. I think eventually it should be upgraded to a proposal, or just made part of the #62627.

Overall, this looks very nice to me. I have no major feedback, just a little bit of name bikeshedding and tiny nits.

  1. Can we replace New with Make for where we produce value types? (So MakeEvent, MakeStack, etc.) My thinking is that since we're not returning a pointer to something, it aligns more with make vs. new in the language itself, even if they use references under the hood. WDYT?
  2. I worry that EventType as a name overlaps up too much with EventKind and could be confusing. WDYT about calling it EventDetails and calling the field in EventConfig Details (really all the information is necessary to constitute an event)?
  3. Inferring the Kind field is nice, but I'm wondering if we should make Kind compulsory just to make the API a little more explicit. We can always relax that in the future, or we can make more bespoke Range and StateTransition types (as well as an empty StackSample type, maybe). We can do a little validation and panic if the details don't match.

Comment From: felixge

@mknyszek thanks. Your suggestions all make sense to me. I'll update the proposal and the CL ASAP.

Edit: The proposal has been updated.