Skip to content
Open
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
44 changes: 36 additions & 8 deletions components/api-server/pkg/api/grpc/hypershell/v1/common.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions components/api-server/pkg/api/tracemeta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package api

import (
"context"
"fmt"

"go.opentelemetry.io/otel/trace"
)

// TraceMeta embeds alongside api.Meta to persist the originating W3C Trace
// Context on every resource. The json:"-" tag keeps these fields out of REST
// API responses (RTC-05). The columns are nullable so pre-existing rows and
// resources created with telemetry disabled have NULL trace context.
type TraceMeta struct {
Traceparent *string `json:"-" gorm:"column:traceparent"`
Tracestate *string `json:"-" gorm:"column:tracestate"`
}

// CaptureTraceContext extracts the active span's W3C traceparent and
// tracestate from ctx and stores them. When no valid span is active (OTel
// disabled or no sampled span), the fields are left nil.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Replace, when no valid span is active (telemetry disabled), these pointers stay nil. Depending on whether the DAO's Replace uses gorm Save (writes nil -> NULL) vs Updates with a struct (skips zero values), an update performed with telemetry off could null out a previously-stored traceparent. RTC-01's overwrite-on-update arguably permits this, but please confirm the intended behavior since it is a silent change to existing data.

func (t *TraceMeta) CaptureTraceContext(ctx context.Context) {
sc := trace.SpanFromContext(ctx).SpanContext()
if !sc.IsValid() {
return
}
tp := fmt.Sprintf("00-%s-%s-%s", sc.TraceID(), sc.SpanID(), sc.TraceFlags())
t.Traceparent = &tp
if ts := sc.TraceState().String(); ts != "" {
t.Tracestate = &ts
}
}
138 changes: 138 additions & 0 deletions components/api-server/pkg/api/tracemeta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package api

import (
"context"
"testing"

"go.opentelemetry.io/otel/trace"
)

func TestCaptureTraceContext(t *testing.T) {
traceID, _ := trace.TraceIDFromHex("0af7651916cd43dd8448eb211c80319c")
spanID, _ := trace.SpanIDFromHex("b7ad6b7169203331")

tests := []struct {
name string
ctx context.Context
wantTraceparent *string
wantTracestate *string
}{
{
name: "no span in context leaves fields nil",
ctx: context.Background(),
wantTraceparent: nil,
wantTracestate: nil,
},
{
name: "invalid span context leaves fields nil",
ctx: trace.ContextWithSpanContext(context.Background(), trace.SpanContext{}),
wantTraceparent: nil,
wantTracestate: nil,
},
{
name: "valid span context sets traceparent",
ctx: trace.ContextWithSpanContext(context.Background(),
trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
Remote: true,
}),
),
wantTraceparent: strPtr("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
wantTracestate: nil,
},
{
name: "valid span context with tracestate sets both fields",
ctx: trace.ContextWithSpanContext(context.Background(),
trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
TraceState: mustTraceState(t, "vendor=opaque"),
Remote: true,
}),
),
wantTraceparent: strPtr("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
wantTracestate: strPtr("vendor=opaque"),
},
{
name: "unsampled span still captures traceparent with flags 00",
ctx: trace.ContextWithSpanContext(context.Background(),
trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: 0,
Remote: true,
}),
),
wantTraceparent: strPtr("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00"),
wantTracestate: nil,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var tm TraceMeta
tm.CaptureTraceContext(tc.ctx)

if !strPtrEq(tm.Traceparent, tc.wantTraceparent) {
t.Errorf("Traceparent = %s, want %s", strPtrFmt(tm.Traceparent), strPtrFmt(tc.wantTraceparent))
}
if !strPtrEq(tm.Tracestate, tc.wantTracestate) {
t.Errorf("Tracestate = %s, want %s", strPtrFmt(tm.Tracestate), strPtrFmt(tc.wantTracestate))
}
})
}
}

func TestCaptureTraceContextIsIdempotent(t *testing.T) {
traceID, _ := trace.TraceIDFromHex("0af7651916cd43dd8448eb211c80319c")
spanID, _ := trace.SpanIDFromHex("b7ad6b7169203331")

ctx := trace.ContextWithSpanContext(context.Background(),
trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
Remote: true,
}),
)

var tm TraceMeta
tm.CaptureTraceContext(ctx)
first := *tm.Traceparent

tm.CaptureTraceContext(ctx)
if *tm.Traceparent != first {
t.Errorf("second call changed Traceparent: got %s, want %s", *tm.Traceparent, first)
}
}

func mustTraceState(t *testing.T, s string) trace.TraceState {
t.Helper()
ts, err := trace.ParseTraceState(s)
if err != nil {
t.Fatalf("ParseTraceState(%q): %v", s, err)
}
return ts
}

func strPtr(s string) *string { return &s }

func strPtrEq(a, b *string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}

func strPtrFmt(s *string) string {
if s == nil {
return "<nil>"
}
return *s
}
12 changes: 7 additions & 5 deletions components/api-server/plugins/fleets/grpc_presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
func fleetToProto(d *Fleet) *pb.Fleet {
return &pb.Fleet{
Metadata: &pb.ObjectReference{
Id: d.ID,
CreatedAt: timestamppb.New(d.CreatedAt),
UpdatedAt: timestamppb.New(d.UpdatedAt),
Kind: "Fleet",
Href: "/api/hypershell/v1/fleets/" + d.ID,
Id: d.ID,
CreatedAt: timestamppb.New(d.CreatedAt),
UpdatedAt: timestamppb.New(d.UpdatedAt),
Kind: "Fleet",
Href: "/api/hypershell/v1/fleets/" + d.ID,
Traceparent: d.Traceparent,
Tracestate: d.Tracestate,
},
Name: d.Name,
Description: d.Description,
Expand Down
20 changes: 20 additions & 0 deletions components/api-server/plugins/fleets/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import (
"github.com/openshift-online/rh-trex-ai/pkg/db"
)

func migrationAddTraceContext() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "2026082500000001",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`
ALTER TABLE fleets
ADD COLUMN IF NOT EXISTS traceparent TEXT,
ADD COLUMN IF NOT EXISTS tracestate TEXT
`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`
ALTER TABLE fleets
DROP COLUMN IF EXISTS traceparent,
DROP COLUMN IF EXISTS tracestate
`).Error
},
}
}

func migration() *gormigrate.Migration {
type Fleet struct {
db.Model
Expand Down
2 changes: 2 additions & 0 deletions components/api-server/plugins/fleets/model.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package fleets

import (
hypershellapi "github.com/openshift-online/hypershell/components/api-server/pkg/api"
"github.com/openshift-online/rh-trex-ai/pkg/api"
"gorm.io/gorm"
)

type Fleet struct {
api.Meta
hypershellapi.TraceMeta
Name string `json:"name"`
Description *string `json:"description"`
Status *string `json:"status"`
Expand Down
1 change: 1 addition & 0 deletions components/api-server/plugins/fleets/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ func init() {
presenters.RegisterKind(&Fleet{}, "Fleet")

db.RegisterMigration(migration())
db.RegisterMigration(migrationAddTraceContext())
}
2 changes: 2 additions & 0 deletions components/api-server/plugins/fleets/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (s *sqlFleetService) Get(ctx context.Context, id string) (*Fleet, *errors.S
}

func (s *sqlFleetService) Create(ctx context.Context, fleet *Fleet) (*Fleet, *errors.ServiceError) {
fleet.CaptureTraceContext(ctx)
fleet, err := s.fleetDao.Create(ctx, fleet)
if err != nil {
return nil, services.HandleCreateError("Fleet", err)
Expand All @@ -93,6 +94,7 @@ func (s *sqlFleetService) Replace(ctx context.Context, fleet *Fleet) (*Fleet, *e
}
defer s.lockFactory.Unlock(ctx, lockOwnerID)

fleet.CaptureTraceContext(ctx)
fleet, err = s.fleetDao.Replace(ctx, fleet)
if err != nil {
return nil, services.HandleUpdateError("Fleet", err)
Expand Down
12 changes: 7 additions & 5 deletions components/api-server/plugins/gatewayNetworks/grpc_presenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
func gatewayNetworkToProto(d *GatewayNetwork) *pb.GatewayNetwork {
return &pb.GatewayNetwork{
Metadata: &pb.ObjectReference{
Id: d.ID,
CreatedAt: timestamppb.New(d.CreatedAt),
UpdatedAt: timestamppb.New(d.UpdatedAt),
Kind: "GatewayNetwork",
Href: "/api/hypershell/v1/gateway_networks/" + d.ID,
Id: d.ID,
CreatedAt: timestamppb.New(d.CreatedAt),
UpdatedAt: timestamppb.New(d.UpdatedAt),
Kind: "GatewayNetwork",
Href: "/api/hypershell/v1/gateway_networks/" + d.ID,
Traceparent: d.Traceparent,
Tracestate: d.Tracestate,
},
Name: d.Name,
FleetId: d.FleetId,
Expand Down
20 changes: 20 additions & 0 deletions components/api-server/plugins/gatewayNetworks/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import (
"github.com/openshift-online/rh-trex-ai/pkg/db"
)

func migrationAddTraceContext() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "2026082500000003",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`
ALTER TABLE gateway_networks
ADD COLUMN IF NOT EXISTS traceparent TEXT,
ADD COLUMN IF NOT EXISTS tracestate TEXT
`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`
ALTER TABLE gateway_networks
DROP COLUMN IF EXISTS traceparent,
DROP COLUMN IF EXISTS tracestate
`).Error
},
}
}

func migration() *gormigrate.Migration {
type GatewayNetwork struct {
db.Model
Expand Down
2 changes: 2 additions & 0 deletions components/api-server/plugins/gatewayNetworks/model.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package gatewayNetworks

import (
hypershellapi "github.com/openshift-online/hypershell/components/api-server/pkg/api"
"github.com/openshift-online/rh-trex-ai/pkg/api"
"gorm.io/gorm"
)

type GatewayNetwork struct {
api.Meta
hypershellapi.TraceMeta
Name string `json:"name"`
FleetId string `json:"fleet_id"`
Topology *string `json:"topology"`
Expand Down
Loading
Loading