Repository: google/adk-go
File: server/adkrest/internal/services/debugtelemetry.go (Go)

Code before:
}

// GetSpansByEventID returns spans associated with the given event ID.
func (d *DebugTelemetry) GetSpansByEventID(eventID string) []DebugSpan {
	records := d.store.getByEventID(eventID)
	return convertRecords(records)
}

// GetSpansBySessionID returns spans associated with the given session ID.
func (d *DebugTelemetry) GetSpansBySessionID(sessionID string) []DebugSpan {
	records := d.store.getBySessionID(sessionID)
	return convertRecords(records)
}

func convertRecords(records []*spanRecord) []DebugSpan {
	debugSpans := make([]DebugSpan, len(records))
	for i, r := range records {
		debugSpans[i] = DebugSpan{
			Name:         r.Span.Name,
			StartTime:    r.Span.StartTime.UnixNano(),
			EndTime:      r.Span.EndTime.UnixNano(),
			TraceID:      r.Span.Context.TraceID().String(),
			SpanID:       r.Span.Context.SpanID().String(),
			ParentSpanID: r.Span.ParentSpanID.String(),
			Attributes:   r.Span.Attributes,
			Logs:         r.Logs,
		}
	}
	return debugSpans
}

func convertAttrs(in []attribute.KeyValue) map[string]string {
	out := make(map[string]string)
	for _, attr := range in {
		out[string(attr.Key)] = attr.Value.Emit()
	}
	return out
}

// SpanContext uniquely identifies a span.
type SpanContext struct {
	TraceID string `json:"trace_id"`
	SpanID  string `json:"span_id"`
}

// DebugSpan represents a span in the trace.
type DebugSpan struct {
	Name         string            `json:"name"`
	StartTime    int64             `json:"start_time"`
	EndTime      int64             `json:"end_time"`
	SpanID       string            `json:"span_id"`

Review comment (suggestion):
Let's use slices.Clone(r.Logs).

Otherwise GetSpansBySessionID will return a shared object (r.Logs) which will be read (e.g. on marshalling to return for HTTP request) and at the same time updated by Export method when it does: `record.Logs = append(record.Logs`.

Code after:
func (d *DebugTelemetry) LogProcessor() sdklog.Processor {
	return sdklog.NewBatchProcessor(d.store)
}

// GetSpansByEventID returns spans associated with the given event ID.
func (d *DebugTelemetry) GetSpansByEventID(eventID string) []DebugSpan {
	return d.store.getSpansByEventID(eventID)
}

// GetSpansBySessionID returns spans associated with the given session ID.
func (d *DebugTelemetry) GetSpansBySessionID(sessionID string) []DebugSpan {
	return d.store.getSpansBySessionID(sessionID)
}

func convertAttrs(in []attribute.KeyValue) map[string]string {
	out := make(map[string]string)
	for _, attr := range in {
		out[string(attr.Key)] = attr.Value.Emit()
	}
	return out
}

// DebugSpan represents a span in the trace.
type DebugSpan struct {
	Name         string            `json:"name"`
	StartTime    int64             `json:"start_time"`
	EndTime      int64             `json:"end_time"`
	SpanID       string            `json:"span_id"`
	TraceID      string            `json:"trace_id"`
	ParentSpanID string            `json:"parent_span_id"`
	Attributes   map[string]string `json:"attributes"`
	Logs         []DebugLog        `json:"logs"`
}

// DebugLog represents a log in the span.
type DebugLog struct {
	Body              any    `json:"body"`
	ObservedTimestamp string `json:"observed_timestamp"`
	TraceID           string `json:"trace_id"`
	SpanID            string `json:"span_id"`
	EventName         string `json:"event_name"`
}

// spanRecord stores a span and its associated logs.
type spanRecord struct {
	Span *inMemorySpan
	Logs []DebugLog
}

// inMemorySpan stores spans in memory for debug telemetry.
type inMemorySpan struct {