Add OTEL telemetry for exec sessions
This commit is contained in:
parent
b30bb5bedf
commit
a1088b6b8d
11
go.mod
11
go.mod
|
|
@ -37,10 +37,18 @@ require (
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0
|
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0
|
||||||
go.opentelemetry.io/otel v1.42.0
|
go.opentelemetry.io/otel v1.42.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
|
||||||
|
go.opentelemetry.io/otel/log v0.18.0
|
||||||
go.opentelemetry.io/otel/metric v1.42.0
|
go.opentelemetry.io/otel/metric v1.42.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.42.0
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.18.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0
|
go.opentelemetry.io/otel/sdk/metric v1.42.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.42.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.49.0
|
||||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
|
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
|
||||||
|
|
@ -137,8 +145,7 @@ require (
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
go.opencensus.io v0.22.5 // indirect
|
go.opencensus.io v0.22.5 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
|
|
||||||
16
go.sum
16
go.sum
|
|
@ -359,16 +359,32 @@ go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQ
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=
|
go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc=
|
||||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=
|
||||||
|
go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg=
|
||||||
|
go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI=
|
||||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw=
|
||||||
|
go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA=
|
||||||
|
go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||||
|
|
|
||||||
|
|
@ -62,28 +62,37 @@ func (controller *Controller) execVMLegacy(
|
||||||
runCommand string,
|
runCommand string,
|
||||||
wait uint64,
|
wait uint64,
|
||||||
) responder.Responder {
|
) responder.Responder {
|
||||||
|
key := execSessionKey{vmName: name}
|
||||||
|
setupTelemetry := controller.execTelemetry.startSetup(ctx.Request.Context(), key, spec)
|
||||||
|
|
||||||
// Look-up the VM
|
// Look-up the VM
|
||||||
waitContext, waitContextCancel := context.WithTimeout(ctx, time.Duration(wait)*time.Second)
|
waitContext, waitContextCancel := context.WithTimeout(ctx, time.Duration(wait)*time.Second)
|
||||||
defer waitContextCancel()
|
defer waitContextCancel()
|
||||||
|
|
||||||
vm, responderImpl := controller.waitForVM(waitContext, name)
|
vm, responderImpl := controller.waitForVM(waitContext, name)
|
||||||
if responderImpl != nil {
|
if responderImpl != nil {
|
||||||
|
setupTelemetry.finish(errors.New("failed to wait for VM"))
|
||||||
|
|
||||||
return responderImpl
|
return responderImpl
|
||||||
}
|
}
|
||||||
|
setupTelemetry.setVM(vm)
|
||||||
|
|
||||||
session, err := controller.newSSHExecSession(
|
session, err := controller.newSSHExecSession(
|
||||||
ctx,
|
|
||||||
waitContext,
|
waitContext,
|
||||||
vm,
|
vm,
|
||||||
execSessionKey{vmName: name},
|
key,
|
||||||
spec,
|
spec,
|
||||||
runCommand,
|
runCommand,
|
||||||
nil,
|
nil,
|
||||||
legacyExecSessionPolicy,
|
legacyExecSessionPolicy,
|
||||||
|
setupTelemetry,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
setupTelemetry.finish(err)
|
||||||
|
|
||||||
return responder.JSON(http.StatusServiceUnavailable, NewErrorResponse("%v", err))
|
return responder.JSON(http.StatusServiceUnavailable, NewErrorResponse("%v", err))
|
||||||
}
|
}
|
||||||
|
setupTelemetry.finish(nil)
|
||||||
|
|
||||||
// Upgrade HTTP request to a WebSocket connection
|
// Upgrade HTTP request to a WebSocket connection
|
||||||
wsConn, err := websocket.Accept(ctx.Writer, ctx.Request, &websocket.AcceptOptions{
|
wsConn, err := websocket.Accept(ctx.Writer, ctx.Request, &websocket.AcceptOptions{
|
||||||
|
|
@ -130,18 +139,22 @@ func (controller *Controller) execVMReconnectable(
|
||||||
NewErrorResponse("exec session %q does not exist", sessionID))
|
NewErrorResponse("exec session %q does not exist", sessionID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupTelemetry := controller.execTelemetry.startSetup(ctx.Request.Context(), key, spec)
|
||||||
|
|
||||||
waitContext, waitContextCancel := context.WithTimeout(ctx, time.Duration(wait)*time.Second)
|
waitContext, waitContextCancel := context.WithTimeout(ctx, time.Duration(wait)*time.Second)
|
||||||
defer waitContextCancel()
|
defer waitContextCancel()
|
||||||
|
|
||||||
vm, responderImpl := controller.waitForVM(waitContext, name)
|
vm, responderImpl := controller.waitForVM(waitContext, name)
|
||||||
if responderImpl != nil {
|
if responderImpl != nil {
|
||||||
|
setupTelemetry.finish(errors.New("failed to wait for VM"))
|
||||||
|
|
||||||
return responderImpl
|
return responderImpl
|
||||||
}
|
}
|
||||||
|
setupTelemetry.setVM(vm)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
session, _, err = controller.execSessions.getOrCreate(waitContext, key, func() (*execSession, error) {
|
session, _, err = controller.execSessions.getOrCreate(waitContext, key, func() (*execSession, error) {
|
||||||
return controller.newSSHExecSession(
|
return controller.newSSHExecSession(
|
||||||
ctx,
|
|
||||||
waitContext,
|
waitContext,
|
||||||
vm,
|
vm,
|
||||||
key,
|
key,
|
||||||
|
|
@ -149,11 +162,15 @@ func (controller *Controller) execVMReconnectable(
|
||||||
runCommand,
|
runCommand,
|
||||||
controller.execSessions,
|
controller.execSessions,
|
||||||
reconnectableExecSessionPolicy,
|
reconnectableExecSessionPolicy,
|
||||||
|
setupTelemetry,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
setupTelemetry.finish(err)
|
||||||
|
|
||||||
return responder.JSON(http.StatusServiceUnavailable, NewErrorResponse("%v", err))
|
return responder.JSON(http.StatusServiceUnavailable, NewErrorResponse("%v", err))
|
||||||
}
|
}
|
||||||
|
setupTelemetry.finish(nil)
|
||||||
|
|
||||||
if !session.specMatches(spec) {
|
if !session.specMatches(spec) {
|
||||||
return responder.JSON(http.StatusConflict,
|
return responder.JSON(http.StatusConflict,
|
||||||
|
|
@ -177,7 +194,6 @@ func (controller *Controller) execVMReconnectable(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *Controller) newSSHExecSession(
|
func (controller *Controller) newSSHExecSession(
|
||||||
_ *gin.Context,
|
|
||||||
waitContext context.Context,
|
waitContext context.Context,
|
||||||
vm *v1.VM,
|
vm *v1.VM,
|
||||||
key execSessionKey,
|
key execSessionKey,
|
||||||
|
|
@ -185,9 +201,8 @@ func (controller *Controller) newSSHExecSession(
|
||||||
runCommand string,
|
runCommand string,
|
||||||
registry *execSessionRegistry,
|
registry *execSessionRegistry,
|
||||||
policy execSessionPolicy,
|
policy execSessionPolicy,
|
||||||
|
setupTelemetry *execSetupTelemetry,
|
||||||
) (*execSession, error) {
|
) (*execSession, error) {
|
||||||
sessionContext, sessionContextCancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
type sshExecAttempt struct {
|
type sshExecAttempt struct {
|
||||||
exec *sshexec.Exec
|
exec *sshexec.Exec
|
||||||
}
|
}
|
||||||
|
|
@ -234,11 +249,12 @@ func (controller *Controller) newSSHExecSession(
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sessionContextCancel()
|
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionTraceContext, sessionTelemetry := setupTelemetry.startSession()
|
||||||
|
sessionContext, sessionContextCancel := context.WithCancel(sessionTraceContext)
|
||||||
|
|
||||||
return newExecSessionWithContextAndSpec(
|
return newExecSessionWithContextAndSpec(
|
||||||
sessionContext,
|
sessionContext,
|
||||||
sessionContextCancel,
|
sessionContextCancel,
|
||||||
|
|
@ -250,6 +266,7 @@ func (controller *Controller) newSSHExecSession(
|
||||||
registry,
|
registry,
|
||||||
controller.execSessionRetentionTTL,
|
controller.execSessionRetentionTTL,
|
||||||
policy,
|
policy,
|
||||||
|
sessionTelemetry,
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,7 +275,7 @@ func (controller *Controller) serveExecSession(
|
||||||
wsConn *websocket.Conn,
|
wsConn *websocket.Conn,
|
||||||
session *execSession,
|
session *execSession,
|
||||||
) responder.Responder {
|
) responder.Responder {
|
||||||
subscriber, err := session.attach()
|
subscriber, err := session.attach(ctx.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = wsConn.Close(websocket.StatusNormalClosure, err.Error())
|
_ = wsConn.Close(websocket.StatusNormalClosure, err.Error())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ type Controller struct {
|
||||||
sshServer *sshserver.SSHServer
|
sshServer *sshserver.SSHServer
|
||||||
execSessions *execSessionRegistry
|
execSessions *execSessionRegistry
|
||||||
execSSHClients *execSSHClientPool
|
execSSHClients *execSSHClientPool
|
||||||
|
execTelemetry *execTelemetry
|
||||||
|
|
||||||
single singleflight.Group
|
single singleflight.Group
|
||||||
|
|
||||||
|
|
@ -106,6 +107,11 @@ func New(opts ...Option) (*Controller, error) {
|
||||||
controller.execSSHConnectionKeepaliveInterval,
|
controller.execSSHConnectionKeepaliveInterval,
|
||||||
controller.logger.With("component", "exec-ssh"),
|
controller.logger.With("component", "exec-ssh"),
|
||||||
)
|
)
|
||||||
|
execTelemetry, err := newDefaultExecTelemetry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
controller.execTelemetry = execTelemetry
|
||||||
|
|
||||||
// Instantiate the database
|
// Instantiate the database
|
||||||
store, err := badger.NewBadgerStore(controller.dataDir.DBPath(), controller.disableDBCompression,
|
store, err := badger.NewBadgerStore(controller.dataDir.DBPath(), controller.disableDBCompression,
|
||||||
|
|
|
||||||
|
|
@ -238,12 +238,14 @@ type execSessionSubscriber struct {
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
sendMu sync.Mutex
|
sendMu sync.Mutex
|
||||||
sentWatermark uint64
|
sentWatermark uint64
|
||||||
|
telemetry *execAttachmentTelemetry
|
||||||
}
|
}
|
||||||
|
|
||||||
func newExecSessionSubscriber() *execSessionSubscriber {
|
func newExecSessionSubscriber(telemetry *execAttachmentTelemetry) *execSessionSubscriber {
|
||||||
return &execSessionSubscriber{
|
return &execSessionSubscriber{
|
||||||
frames: make(chan *execstream.Frame, 128),
|
frames: make(chan *execstream.Frame, 128),
|
||||||
closed: make(chan struct{}),
|
closed: make(chan struct{}),
|
||||||
|
telemetry: telemetry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,8 +317,12 @@ func (subscriber *execSessionSubscriber) markSentLocked(frame *execstream.Frame)
|
||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
func (subscriber *execSessionSubscriber) close() {
|
func (subscriber *execSessionSubscriber) close(outcome string) {
|
||||||
subscriber.closeOnce.Do(func() {
|
subscriber.closeOnce.Do(func() {
|
||||||
|
if subscriber.telemetry != nil {
|
||||||
|
subscriber.telemetry.finish(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
close(subscriber.closed)
|
close(subscriber.closed)
|
||||||
subscriber.sendMu.Lock()
|
subscriber.sendMu.Lock()
|
||||||
close(subscriber.frames)
|
close(subscriber.frames)
|
||||||
|
|
@ -347,6 +353,9 @@ type execSession struct {
|
||||||
closed bool
|
closed bool
|
||||||
expiryTimer *time.Timer
|
expiryTimer *time.Timer
|
||||||
|
|
||||||
|
telemetry *execSessionTelemetry
|
||||||
|
attachmentCount uint64
|
||||||
|
|
||||||
startOnce sync.Once
|
startOnce sync.Once
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
doneOnce sync.Once
|
doneOnce sync.Once
|
||||||
|
|
@ -374,6 +383,7 @@ func newExecSession(
|
||||||
registry,
|
registry,
|
||||||
retentionTTL,
|
retentionTTL,
|
||||||
policy,
|
policy,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -388,6 +398,7 @@ func newExecSessionWithContextAndSpec(
|
||||||
registry *execSessionRegistry,
|
registry *execSessionRegistry,
|
||||||
retentionTTL time.Duration,
|
retentionTTL time.Duration,
|
||||||
policy execSessionPolicy,
|
policy execSessionPolicy,
|
||||||
|
telemetry *execSessionTelemetry,
|
||||||
) *execSession {
|
) *execSession {
|
||||||
if ctx == nil || cancel == nil {
|
if ctx == nil || cancel == nil {
|
||||||
ctx, cancel = context.WithCancel(context.Background())
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
|
@ -406,6 +417,7 @@ func newExecSessionWithContextAndSpec(
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
stdin: exec.Stdin(),
|
stdin: exec.Stdin(),
|
||||||
subscribers: map[*execSessionSubscriber]struct{}{},
|
subscribers: map[*execSessionSubscriber]struct{}{},
|
||||||
|
telemetry: telemetry,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,6 +439,10 @@ func (session *execSession) start() {
|
||||||
session.started = true
|
session.started = true
|
||||||
session.mu.Unlock()
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
if session.telemetry != nil {
|
||||||
|
session.telemetry.start()
|
||||||
|
}
|
||||||
|
|
||||||
go session.run()
|
go session.run()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -441,7 +457,7 @@ func (session *execSession) closeIfUnused() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *execSession) attach() (*execSessionSubscriber, error) {
|
func (session *execSession) attach(requestCtx context.Context) (*execSessionSubscriber, error) {
|
||||||
session.mu.Lock()
|
session.mu.Lock()
|
||||||
defer session.mu.Unlock()
|
defer session.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -449,7 +465,18 @@ func (session *execSession) attach() (*execSessionSubscriber, error) {
|
||||||
return nil, errors.New("exec session is closed")
|
return nil, errors.New("exec session is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber := newExecSessionSubscriber()
|
attachmentKind := execTelemetryAttachmentInitial
|
||||||
|
if session.attachmentCount > 0 {
|
||||||
|
attachmentKind = execTelemetryAttachmentReconnect
|
||||||
|
}
|
||||||
|
session.attachmentCount++
|
||||||
|
|
||||||
|
var telemetry *execAttachmentTelemetry
|
||||||
|
if session.telemetry != nil {
|
||||||
|
telemetry = session.telemetry.attach(requestCtx, attachmentKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber := newExecSessionSubscriber(telemetry)
|
||||||
session.subscribers[subscriber] = struct{}{}
|
session.subscribers[subscriber] = struct{}{}
|
||||||
|
|
||||||
return subscriber, nil
|
return subscriber, nil
|
||||||
|
|
@ -457,6 +484,7 @@ func (session *execSession) attach() (*execSessionSubscriber, error) {
|
||||||
|
|
||||||
func (session *execSession) detach(subscriber *execSessionSubscriber) {
|
func (session *execSession) detach(subscriber *execSessionSubscriber) {
|
||||||
if session.policy.closeOnDetach {
|
if session.policy.closeOnDetach {
|
||||||
|
session.detachSubscriber(subscriber, execTelemetryOutcomeDetached)
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -465,16 +493,23 @@ func (session *execSession) detach(subscriber *execSessionSubscriber) {
|
||||||
session.mu.Lock()
|
session.mu.Lock()
|
||||||
defer session.mu.Unlock()
|
defer session.mu.Unlock()
|
||||||
|
|
||||||
session.detachLocked(subscriber)
|
session.detachLocked(subscriber, execTelemetryOutcomeDetached)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *execSession) detachLocked(subscriber *execSessionSubscriber) {
|
func (session *execSession) detachSubscriber(subscriber *execSessionSubscriber, outcome string) {
|
||||||
|
session.mu.Lock()
|
||||||
|
defer session.mu.Unlock()
|
||||||
|
|
||||||
|
session.detachLocked(subscriber, outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *execSession) detachLocked(subscriber *execSessionSubscriber, outcome string) {
|
||||||
if _, ok := session.subscribers[subscriber]; !ok {
|
if _, ok := session.subscribers[subscriber]; !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(session.subscribers, subscriber)
|
delete(session.subscribers, subscriber)
|
||||||
subscriber.close()
|
subscriber.close(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *execSession) writeStdin(data []byte) error {
|
func (session *execSession) writeStdin(data []byte) error {
|
||||||
|
|
@ -569,9 +604,14 @@ func (session *execSession) close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribers := session.takeSubscribersLocked()
|
subscribers := session.takeSubscribersLocked()
|
||||||
|
started := session.started
|
||||||
session.mu.Unlock()
|
session.mu.Unlock()
|
||||||
|
|
||||||
closeSubscribers(subscribers)
|
closeSubscribers(subscribers, execTelemetryOutcomeClosed)
|
||||||
|
|
||||||
|
if !started && session.telemetry != nil {
|
||||||
|
session.telemetry.finish(execTelemetryOutcomeClosed, nil)
|
||||||
|
}
|
||||||
|
|
||||||
session.cancel()
|
session.cancel()
|
||||||
_ = session.exec.Close()
|
_ = session.exec.Close()
|
||||||
|
|
@ -602,9 +642,12 @@ func (session *execSession) run() {
|
||||||
Type: execstream.FrameTypeError,
|
Type: execstream.FrameTypeError,
|
||||||
Error: runErr.Error(),
|
Error: runErr.Error(),
|
||||||
})
|
})
|
||||||
|
if session.telemetry != nil {
|
||||||
|
session.telemetry.fail(runErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.markFinished()
|
session.markFinished(runErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *execSession) recordFrame(frame *execstream.Frame) {
|
func (session *execSession) recordFrame(frame *execstream.Frame) {
|
||||||
|
|
@ -635,7 +678,7 @@ func (session *execSession) recordFrame(frame *execstream.Frame) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (session *execSession) markFinished() {
|
func (session *execSession) markFinished(runErr error) {
|
||||||
session.mu.Lock()
|
session.mu.Lock()
|
||||||
if session.finished {
|
if session.finished {
|
||||||
session.mu.Unlock()
|
session.mu.Unlock()
|
||||||
|
|
@ -655,7 +698,11 @@ func (session *execSession) markFinished() {
|
||||||
}
|
}
|
||||||
session.mu.Unlock()
|
session.mu.Unlock()
|
||||||
|
|
||||||
closeSubscribers(subscribers)
|
closeSubscribers(subscribers, execTelemetryOutcomeFinished)
|
||||||
|
|
||||||
|
if session.telemetry != nil {
|
||||||
|
session.telemetry.finish(execTelemetryOutcomeFromError(runErr), runErr)
|
||||||
|
}
|
||||||
|
|
||||||
session.doneOnce.Do(func() {
|
session.doneOnce.Do(func() {
|
||||||
close(session.done)
|
close(session.done)
|
||||||
|
|
@ -680,9 +727,9 @@ func (session *execSession) takeSubscribersLocked() []*execSessionSubscriber {
|
||||||
return subscribers
|
return subscribers
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeSubscribers(subscribers []*execSessionSubscriber) {
|
func closeSubscribers(subscribers []*execSessionSubscriber, outcome string) {
|
||||||
for _, subscriber := range subscribers {
|
for _, subscriber := range subscribers {
|
||||||
subscriber.close()
|
subscriber.close(outcome)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -690,7 +737,7 @@ func (session *execSession) dropSubscriber(subscriber *execSessionSubscriber) {
|
||||||
session.mu.Lock()
|
session.mu.Lock()
|
||||||
defer session.mu.Unlock()
|
defer session.mu.Unlock()
|
||||||
|
|
||||||
session.detachLocked(subscriber)
|
session.detachLocked(subscriber, execTelemetryOutcomeDropped)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneExecFrame(frame *execstream.Frame) *execstream.Frame {
|
func cloneExecFrame(frame *execstream.Frame) *execstream.Frame {
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ func TestExecSessionHistoryReplayAndAck(t *testing.T) {
|
||||||
Exit: &execstream.Exit{Code: 7},
|
Exit: &execstream.Exit{Code: 7},
|
||||||
})
|
})
|
||||||
|
|
||||||
subscriber, err := session.attach()
|
subscriber, err := session.attach(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
session.sendHistory(subscriber, 0)
|
session.sendHistory(subscriber, 0)
|
||||||
|
|
@ -237,7 +237,7 @@ func TestExecSessionHistoryReplayStreamsPastSubscriberBuffer(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber, err := session.attach()
|
subscriber, err := session.attach(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
@ -271,7 +271,7 @@ func TestExecSessionDetachKeepsProcessAlive(t *testing.T) {
|
||||||
session := newManualExecSessionForTest(execSessionKey{vmName: "vm", sessionID: "session"}, registry)
|
session := newManualExecSessionForTest(execSessionKey{vmName: "vm", sessionID: "session"}, registry)
|
||||||
exec := session.exec.(*fakeExec)
|
exec := session.exec.(*fakeExec)
|
||||||
|
|
||||||
subscriber, err := session.attach()
|
subscriber, err := session.attach(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
session.detach(subscriber)
|
session.detach(subscriber)
|
||||||
|
|
@ -286,7 +286,7 @@ func TestLegacyExecSessionDetachStopsProcess(t *testing.T) {
|
||||||
session.policy = legacyExecSessionPolicy
|
session.policy = legacyExecSessionPolicy
|
||||||
exec := session.exec.(*fakeExec)
|
exec := session.exec.(*fakeExec)
|
||||||
|
|
||||||
subscriber, err := session.attach()
|
subscriber, err := session.attach(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
session.detach(subscriber)
|
session.detach(subscriber)
|
||||||
|
|
@ -323,7 +323,7 @@ func TestExecSessionCloseIfUnusedKeepsAttachedSession(t *testing.T) {
|
||||||
registry := newExecSessionRegistry()
|
registry := newExecSessionRegistry()
|
||||||
session := newManualExecSessionForTest(execSessionKey{vmName: "vm", sessionID: "session"}, registry)
|
session := newManualExecSessionForTest(execSessionKey{vmName: "vm", sessionID: "session"}, registry)
|
||||||
|
|
||||||
_, err := session.attach()
|
_, err := session.attach(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
session.closeIfUnused()
|
session.closeIfUnused()
|
||||||
|
|
@ -353,7 +353,7 @@ func TestExecSessionFinishedEntryExpiresAfterTTL(t *testing.T) {
|
||||||
session.retentionTTL = 10 * time.Millisecond
|
session.retentionTTL = 10 * time.Millisecond
|
||||||
registry.sessions[key] = session
|
registry.sessions[key] = session
|
||||||
|
|
||||||
session.markFinished()
|
session.markFinished(nil)
|
||||||
|
|
||||||
require.Eventually(t, func() bool {
|
require.Eventually(t, func() bool {
|
||||||
_, ok := registry.get(key)
|
_, ok := registry.get(key)
|
||||||
|
|
@ -366,7 +366,7 @@ func TestExecSessionFinishKeepsReconnectableSubscriberOpen(t *testing.T) {
|
||||||
registry := newExecSessionRegistry()
|
registry := newExecSessionRegistry()
|
||||||
session := newManualExecSessionForTest(execSessionKey{vmName: "vm", sessionID: "session"}, registry)
|
session := newManualExecSessionForTest(execSessionKey{vmName: "vm", sessionID: "session"}, registry)
|
||||||
|
|
||||||
subscriber, err := session.attach()
|
subscriber, err := session.attach(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
session.recordFrame(&execstream.Frame{Type: execstream.FrameTypeStdout, Data: []byte("out")})
|
session.recordFrame(&execstream.Frame{Type: execstream.FrameTypeStdout, Data: []byte("out")})
|
||||||
|
|
@ -374,7 +374,7 @@ func TestExecSessionFinishKeepsReconnectableSubscriberOpen(t *testing.T) {
|
||||||
Type: execstream.FrameTypeExit,
|
Type: execstream.FrameTypeExit,
|
||||||
Exit: &execstream.Exit{Code: 0},
|
Exit: &execstream.Exit{Code: 0},
|
||||||
})
|
})
|
||||||
session.markFinished()
|
session.markFinished(nil)
|
||||||
|
|
||||||
require.Equal(t, execstream.FrameTypeStdout, (<-subscriber.frames).Type)
|
require.Equal(t, execstream.FrameTypeStdout, (<-subscriber.frames).Type)
|
||||||
require.Equal(t, execstream.FrameTypeExit, (<-subscriber.frames).Type)
|
require.Equal(t, execstream.FrameTypeExit, (<-subscriber.frames).Type)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,411 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cirruslabs/orchard/internal/opentelemetry"
|
||||||
|
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/log"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
execTelemetryEventSessionStarted = "session_started"
|
||||||
|
execTelemetryEventAttached = "attached"
|
||||||
|
execTelemetryEventDetached = "detached"
|
||||||
|
execTelemetryEventFinished = "finished"
|
||||||
|
execTelemetryEventFailed = "failed"
|
||||||
|
|
||||||
|
execTelemetryModeLegacy = "legacy"
|
||||||
|
execTelemetryModeReconnectable = "reconnectable"
|
||||||
|
|
||||||
|
execTelemetryAttachmentInitial = "initial"
|
||||||
|
execTelemetryAttachmentReconnect = "reconnect"
|
||||||
|
|
||||||
|
execTelemetryOutcomeSuccess = "success"
|
||||||
|
execTelemetryOutcomeError = "error"
|
||||||
|
execTelemetryOutcomeCanceled = "canceled"
|
||||||
|
execTelemetryOutcomeClosed = "closed"
|
||||||
|
execTelemetryOutcomeDetached = "detached"
|
||||||
|
execTelemetryOutcomeDropped = "dropped"
|
||||||
|
execTelemetryOutcomeFinished = "finished"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execTelemetry struct {
|
||||||
|
tracer trace.Tracer
|
||||||
|
logger log.Logger
|
||||||
|
|
||||||
|
setupAttempts metric.Int64Counter
|
||||||
|
sessionsStarted metric.Int64Counter
|
||||||
|
sessionsFinished metric.Int64Counter
|
||||||
|
activeSessions metric.Int64UpDownCounter
|
||||||
|
attachments metric.Int64Counter
|
||||||
|
setupDuration metric.Float64Histogram
|
||||||
|
sessionDuration metric.Float64Histogram
|
||||||
|
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (telemetry *execTelemetry) startSetup(
|
||||||
|
ctx context.Context,
|
||||||
|
key execSessionKey,
|
||||||
|
spec execSessionSpec,
|
||||||
|
) *execSetupTelemetry {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := newExecTelemetryMetadata(key, spec)
|
||||||
|
ctx, span := telemetry.tracer.Start(ctx, "orchard.exec.setup",
|
||||||
|
trace.WithAttributes(metadata.spanAttributes()...))
|
||||||
|
|
||||||
|
return &execSetupTelemetry{
|
||||||
|
telemetry: telemetry,
|
||||||
|
metadata: metadata,
|
||||||
|
ctx: ctx,
|
||||||
|
span: span,
|
||||||
|
startedAt: telemetry.now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (telemetry *execTelemetry) emitLog(
|
||||||
|
ctx context.Context,
|
||||||
|
event string,
|
||||||
|
metadata execTelemetryMetadata,
|
||||||
|
extra ...log.KeyValue,
|
||||||
|
) {
|
||||||
|
record := log.Record{}
|
||||||
|
record.SetTimestamp(telemetry.now())
|
||||||
|
record.SetObservedTimestamp(telemetry.now())
|
||||||
|
record.SetSeverity(log.SeverityInfo)
|
||||||
|
record.SetBody(log.StringValue(event))
|
||||||
|
record.AddAttributes(metadata.logAttributes(extra...)...)
|
||||||
|
telemetry.logger.Emit(ctx, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDefaultExecTelemetry() (*execTelemetry, error) {
|
||||||
|
return newExecTelemetry(
|
||||||
|
opentelemetry.DefaultMeter,
|
||||||
|
opentelemetry.DefaultTracer,
|
||||||
|
opentelemetry.DefaultLogger,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecTelemetry(
|
||||||
|
meter metric.Meter,
|
||||||
|
tracer trace.Tracer,
|
||||||
|
logger log.Logger,
|
||||||
|
) (*execTelemetry, error) {
|
||||||
|
telemetry := &execTelemetry{
|
||||||
|
tracer: tracer,
|
||||||
|
logger: logger,
|
||||||
|
now: time.Now,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
telemetry.setupAttempts, err = meter.Int64Counter("org.cirruslabs.orchard.controller.exec.setup_attempts")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.sessionsStarted, err = meter.Int64Counter("org.cirruslabs.orchard.controller.exec.sessions_started")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.sessionsFinished, err = meter.Int64Counter("org.cirruslabs.orchard.controller.exec.sessions_finished")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.activeSessions, err = meter.Int64UpDownCounter("org.cirruslabs.orchard.controller.exec.active_sessions")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.attachments, err = meter.Int64Counter("org.cirruslabs.orchard.controller.exec.attachments")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.setupDuration, err = meter.Float64Histogram("org.cirruslabs.orchard.controller.exec.setup_time")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetry.sessionDuration, err = meter.Float64Histogram("org.cirruslabs.orchard.controller.exec.session_time")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return telemetry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type execTelemetryMetadata struct {
|
||||||
|
vmName string
|
||||||
|
vmUID string
|
||||||
|
sessionID string
|
||||||
|
worker string
|
||||||
|
mode string
|
||||||
|
tty bool
|
||||||
|
interactive bool
|
||||||
|
commandHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecTelemetryMetadata(key execSessionKey, spec execSessionSpec) execTelemetryMetadata {
|
||||||
|
mode := execTelemetryModeLegacy
|
||||||
|
if key.sessionID != "" {
|
||||||
|
mode = execTelemetryModeReconnectable
|
||||||
|
}
|
||||||
|
|
||||||
|
return execTelemetryMetadata{
|
||||||
|
vmName: key.vmName,
|
||||||
|
sessionID: key.sessionID,
|
||||||
|
mode: mode,
|
||||||
|
tty: spec.tty,
|
||||||
|
interactive: spec.interactive,
|
||||||
|
commandHash: execCommandHash(spec.command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (metadata *execTelemetryMetadata) setVM(vm *v1.VM) {
|
||||||
|
metadata.vmUID = vm.UID
|
||||||
|
metadata.worker = vm.Worker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (metadata execTelemetryMetadata) spanAttributes(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||||
|
attributes := []attribute.KeyValue{
|
||||||
|
attribute.String("vm_name", metadata.vmName),
|
||||||
|
attribute.String("vm_uid", metadata.vmUID),
|
||||||
|
attribute.String("session_id", metadata.sessionID),
|
||||||
|
attribute.String("mode", metadata.mode),
|
||||||
|
attribute.Bool("tty", metadata.tty),
|
||||||
|
attribute.Bool("interactive", metadata.interactive),
|
||||||
|
attribute.String("worker", metadata.worker),
|
||||||
|
attribute.String("command_sha256", metadata.commandHash),
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(attributes, extra...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (metadata execTelemetryMetadata) metricAttributes(extra ...attribute.KeyValue) []attribute.KeyValue {
|
||||||
|
attributes := []attribute.KeyValue{
|
||||||
|
attribute.String("mode", metadata.mode),
|
||||||
|
attribute.Bool("tty", metadata.tty),
|
||||||
|
attribute.Bool("interactive", metadata.interactive),
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(attributes, extra...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (metadata execTelemetryMetadata) logAttributes(extra ...log.KeyValue) []log.KeyValue {
|
||||||
|
attributes := []log.KeyValue{
|
||||||
|
log.String("vm_name", metadata.vmName),
|
||||||
|
log.String("vm_uid", metadata.vmUID),
|
||||||
|
log.String("session_id", metadata.sessionID),
|
||||||
|
log.String("mode", metadata.mode),
|
||||||
|
log.Bool("tty", metadata.tty),
|
||||||
|
log.Bool("interactive", metadata.interactive),
|
||||||
|
log.String("worker", metadata.worker),
|
||||||
|
log.String("command_sha256", metadata.commandHash),
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(attributes, extra...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type execSetupTelemetry struct {
|
||||||
|
telemetry *execTelemetry
|
||||||
|
metadata execTelemetryMetadata
|
||||||
|
ctx context.Context
|
||||||
|
span trace.Span
|
||||||
|
startedAt time.Time
|
||||||
|
|
||||||
|
finishOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (setup *execSetupTelemetry) setVM(vm *v1.VM) {
|
||||||
|
setup.metadata.setVM(vm)
|
||||||
|
setup.span.SetAttributes(setup.metadata.spanAttributes()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (setup *execSetupTelemetry) startSession() (context.Context, *execSessionTelemetry) {
|
||||||
|
sessionCtx, span := setup.telemetry.tracer.Start(
|
||||||
|
context.WithoutCancel(setup.ctx),
|
||||||
|
"orchard.exec.session",
|
||||||
|
trace.WithAttributes(setup.metadata.spanAttributes()...),
|
||||||
|
)
|
||||||
|
|
||||||
|
return sessionCtx, &execSessionTelemetry{
|
||||||
|
telemetry: setup.telemetry,
|
||||||
|
metadata: setup.metadata,
|
||||||
|
ctx: sessionCtx,
|
||||||
|
span: span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (setup *execSetupTelemetry) finish(err error) {
|
||||||
|
setup.finishOnce.Do(func() {
|
||||||
|
outcome := execTelemetryOutcomeFromError(err)
|
||||||
|
setup.telemetry.setupAttempts.Add(setup.ctx, 1,
|
||||||
|
metric.WithAttributes(setup.metadata.metricAttributes(attribute.String("outcome", outcome))...))
|
||||||
|
setup.telemetry.setupDuration.Record(setup.ctx,
|
||||||
|
setup.telemetry.now().Sub(setup.startedAt).Seconds(),
|
||||||
|
metric.WithAttributes(setup.metadata.metricAttributes(attribute.String("outcome", outcome))...))
|
||||||
|
|
||||||
|
setup.span.SetAttributes(attribute.String("outcome", outcome))
|
||||||
|
if err != nil {
|
||||||
|
setup.span.RecordError(err)
|
||||||
|
setup.span.SetStatus(codes.Error, err.Error())
|
||||||
|
setup.telemetry.emitLog(setup.ctx, execTelemetryEventFailed, setup.metadata,
|
||||||
|
log.String("outcome", outcome))
|
||||||
|
}
|
||||||
|
setup.span.End()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type execSessionTelemetry struct {
|
||||||
|
telemetry *execTelemetry
|
||||||
|
metadata execTelemetryMetadata
|
||||||
|
ctx context.Context
|
||||||
|
span trace.Span
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
started bool
|
||||||
|
startedAt time.Time
|
||||||
|
|
||||||
|
finishOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *execSessionTelemetry) start() {
|
||||||
|
session.mu.Lock()
|
||||||
|
if session.started {
|
||||||
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.started = true
|
||||||
|
session.startedAt = session.telemetry.now()
|
||||||
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
session.telemetry.sessionsStarted.Add(session.ctx, 1,
|
||||||
|
metric.WithAttributes(session.metadata.metricAttributes()...))
|
||||||
|
session.telemetry.activeSessions.Add(session.ctx, 1,
|
||||||
|
metric.WithAttributes(session.metadata.metricAttributes()...))
|
||||||
|
session.telemetry.emitLog(session.ctx, execTelemetryEventSessionStarted, session.metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *execSessionTelemetry) attach(
|
||||||
|
requestCtx context.Context,
|
||||||
|
kind string,
|
||||||
|
) *execAttachmentTelemetry {
|
||||||
|
var links []trace.Link
|
||||||
|
if spanContext := trace.SpanContextFromContext(requestCtx); spanContext.IsValid() {
|
||||||
|
links = append(links, trace.Link{SpanContext: spanContext})
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, span := session.telemetry.tracer.Start(
|
||||||
|
session.ctx,
|
||||||
|
"orchard.exec.attachment",
|
||||||
|
trace.WithAttributes(session.metadata.spanAttributes(attribute.String("attachment_kind", kind))...),
|
||||||
|
trace.WithLinks(links...),
|
||||||
|
)
|
||||||
|
|
||||||
|
session.telemetry.attachments.Add(ctx, 1,
|
||||||
|
metric.WithAttributes(session.metadata.metricAttributes(attribute.String("attachment_kind", kind))...))
|
||||||
|
session.telemetry.emitLog(ctx, execTelemetryEventAttached, session.metadata,
|
||||||
|
log.String("attachment_kind", kind))
|
||||||
|
|
||||||
|
return &execAttachmentTelemetry{
|
||||||
|
session: session,
|
||||||
|
ctx: ctx,
|
||||||
|
span: span,
|
||||||
|
kind: kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *execSessionTelemetry) fail(err error) {
|
||||||
|
if err == nil || errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.telemetry.emitLog(session.ctx, execTelemetryEventFailed, session.metadata,
|
||||||
|
log.String("outcome", execTelemetryOutcomeError))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *execSessionTelemetry) finish(outcome string, err error) {
|
||||||
|
session.finishOnce.Do(func() {
|
||||||
|
session.mu.Lock()
|
||||||
|
started := session.started
|
||||||
|
startedAt := session.startedAt
|
||||||
|
session.mu.Unlock()
|
||||||
|
|
||||||
|
session.span.SetAttributes(attribute.String("outcome", outcome))
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
session.span.RecordError(err)
|
||||||
|
session.span.SetStatus(codes.Error, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if started {
|
||||||
|
session.telemetry.sessionsFinished.Add(session.ctx, 1,
|
||||||
|
metric.WithAttributes(session.metadata.metricAttributes(attribute.String("outcome", outcome))...))
|
||||||
|
session.telemetry.activeSessions.Add(session.ctx, -1,
|
||||||
|
metric.WithAttributes(session.metadata.metricAttributes()...))
|
||||||
|
session.telemetry.sessionDuration.Record(session.ctx,
|
||||||
|
session.telemetry.now().Sub(startedAt).Seconds(),
|
||||||
|
metric.WithAttributes(session.metadata.metricAttributes(attribute.String("outcome", outcome))...))
|
||||||
|
session.telemetry.emitLog(session.ctx, execTelemetryEventFinished, session.metadata,
|
||||||
|
log.String("outcome", outcome))
|
||||||
|
}
|
||||||
|
|
||||||
|
session.span.End()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type execAttachmentTelemetry struct {
|
||||||
|
session *execSessionTelemetry
|
||||||
|
ctx context.Context
|
||||||
|
span trace.Span
|
||||||
|
kind string
|
||||||
|
|
||||||
|
finishOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attachment *execAttachmentTelemetry) finish(outcome string) {
|
||||||
|
attachment.finishOnce.Do(func() {
|
||||||
|
attachment.span.SetAttributes(attribute.String("outcome", outcome))
|
||||||
|
if outcome != execTelemetryOutcomeFinished {
|
||||||
|
attachment.session.telemetry.emitLog(attachment.ctx, execTelemetryEventDetached,
|
||||||
|
attachment.session.metadata,
|
||||||
|
log.String("attachment_kind", attachment.kind),
|
||||||
|
log.String("outcome", outcome))
|
||||||
|
}
|
||||||
|
attachment.span.End()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func execCommandHash(command string) string {
|
||||||
|
sum := sha256.Sum256([]byte(command))
|
||||||
|
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func execTelemetryOutcomeFromError(err error) string {
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return execTelemetryOutcomeSuccess
|
||||||
|
case errors.Is(err, context.Canceled):
|
||||||
|
return execTelemetryOutcomeCanceled
|
||||||
|
default:
|
||||||
|
return execTelemetryOutcomeError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,496 @@
|
||||||
|
//nolint:testpackage // telemetry tests exercise controller internals directly
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cirruslabs/orchard/internal/execstream"
|
||||||
|
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
otellog "go.opentelemetry.io/otel/log"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type execTelemetryHarness struct {
|
||||||
|
telemetry *execTelemetry
|
||||||
|
logExporter *memoryLogExporter
|
||||||
|
metricReader *sdkmetric.ManualReader
|
||||||
|
tracerProvider *sdktrace.TracerProvider
|
||||||
|
spanRecorder *tracetest.SpanRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExecTelemetryHarness(t *testing.T) *execTelemetryHarness {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
logExporter := &memoryLogExporter{}
|
||||||
|
loggerProvider := sdklog.NewLoggerProvider(
|
||||||
|
sdklog.WithProcessor(sdklog.NewSimpleProcessor(logExporter)),
|
||||||
|
)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, loggerProvider.Shutdown(context.Background()))
|
||||||
|
})
|
||||||
|
|
||||||
|
metricReader := sdkmetric.NewManualReader()
|
||||||
|
meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(metricReader))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, meterProvider.Shutdown(context.Background()))
|
||||||
|
})
|
||||||
|
|
||||||
|
spanRecorder := tracetest.NewSpanRecorder()
|
||||||
|
tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(spanRecorder))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, tracerProvider.Shutdown(context.Background()))
|
||||||
|
})
|
||||||
|
|
||||||
|
telemetry, err := newExecTelemetry(
|
||||||
|
meterProvider.Meter("exec-telemetry-test"),
|
||||||
|
tracerProvider.Tracer("exec-telemetry-test"),
|
||||||
|
loggerProvider.Logger("exec-telemetry-test"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &execTelemetryHarness{
|
||||||
|
telemetry: telemetry,
|
||||||
|
logExporter: logExporter,
|
||||||
|
metricReader: metricReader,
|
||||||
|
tracerProvider: tracerProvider,
|
||||||
|
spanRecorder: spanRecorder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type memoryLogExporter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
records []sdklog.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
func (exporter *memoryLogExporter) Export(_ context.Context, records []sdklog.Record) error {
|
||||||
|
exporter.mu.Lock()
|
||||||
|
defer exporter.mu.Unlock()
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
exporter.records = append(exporter.records, record.Clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (exporter *memoryLogExporter) Shutdown(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (exporter *memoryLogExporter) ForceFlush(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (exporter *memoryLogExporter) snapshot() []sdklog.Record {
|
||||||
|
exporter.mu.Lock()
|
||||||
|
defer exporter.mu.Unlock()
|
||||||
|
|
||||||
|
records := make([]sdklog.Record, 0, len(exporter.records))
|
||||||
|
for _, record := range exporter.records {
|
||||||
|
records = append(records, record.Clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecTelemetryCapturesReconnectableSessionAcrossSignals(t *testing.T) {
|
||||||
|
harness := newExecTelemetryHarness(t)
|
||||||
|
spec := execSessionSpec{
|
||||||
|
command: "printf secret",
|
||||||
|
interactive: true,
|
||||||
|
tty: true,
|
||||||
|
}
|
||||||
|
key := execSessionKey{vmName: "vm-1", sessionID: "session-1"}
|
||||||
|
vm := &v1.VM{
|
||||||
|
Meta: v1.Meta{Name: "vm-1"},
|
||||||
|
UID: "uid-1",
|
||||||
|
Worker: "worker-1",
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTelemetry := harness.telemetry.startSetup(context.Background(), key, spec)
|
||||||
|
setupTelemetry.setVM(vm)
|
||||||
|
sessionTraceContext, sessionTelemetry := setupTelemetry.startSession()
|
||||||
|
setupTelemetry.finish(nil)
|
||||||
|
|
||||||
|
runRelease := make(chan struct{})
|
||||||
|
sessionContext, sessionContextCancel := context.WithCancel(sessionTraceContext)
|
||||||
|
session := newExecSessionWithContextAndSpec(
|
||||||
|
sessionContext,
|
||||||
|
sessionContextCancel,
|
||||||
|
key,
|
||||||
|
spec,
|
||||||
|
spec.command,
|
||||||
|
&fakeExec{
|
||||||
|
run: func(context.Context, string, chan<- *execstream.Frame) error {
|
||||||
|
<-runRelease
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
time.Minute,
|
||||||
|
reconnectableExecSessionPolicy,
|
||||||
|
sessionTelemetry,
|
||||||
|
)
|
||||||
|
|
||||||
|
firstSubscriber, err := session.attach(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
session.start()
|
||||||
|
session.detach(firstSubscriber)
|
||||||
|
|
||||||
|
secondSubscriber, err := session.attach(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
close(runRelease)
|
||||||
|
<-session.done
|
||||||
|
session.detach(secondSubscriber)
|
||||||
|
|
||||||
|
require.NoError(t, harness.tracerProvider.ForceFlush(context.Background()))
|
||||||
|
|
||||||
|
spans := harness.spanRecorder.Ended()
|
||||||
|
setupSpan := findSpanByName(t, spans, "orchard.exec.setup")
|
||||||
|
sessionSpan := findSpanByName(t, spans, "orchard.exec.session")
|
||||||
|
attachmentSpans := findSpansByName(spans, "orchard.exec.attachment")
|
||||||
|
require.Len(t, attachmentSpans, 2)
|
||||||
|
require.Equal(t, setupSpan.SpanContext().SpanID(), sessionSpan.Parent().SpanID())
|
||||||
|
for _, attachmentSpan := range attachmentSpans {
|
||||||
|
require.Equal(t, sessionSpan.SpanContext().SpanID(), attachmentSpan.Parent().SpanID())
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := harness.logExporter.snapshot()
|
||||||
|
require.Equal(t, []string{
|
||||||
|
execTelemetryEventAttached,
|
||||||
|
execTelemetryEventSessionStarted,
|
||||||
|
execTelemetryEventDetached,
|
||||||
|
execTelemetryEventAttached,
|
||||||
|
execTelemetryEventFinished,
|
||||||
|
execTelemetryEventDetached,
|
||||||
|
}, logBodies(logs))
|
||||||
|
for _, record := range logs {
|
||||||
|
require.Equal(t, sessionSpan.SpanContext().TraceID(), record.TraceID())
|
||||||
|
require.NotContains(t, record.Body().AsString(), spec.command)
|
||||||
|
require.NotContains(t, logAttributes(record), "command")
|
||||||
|
require.Equal(t, execCommandHash(spec.command), logAttributes(record)["command_sha256"])
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics := collectMetrics(t, harness.metricReader)
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.setup_attempts", nil))
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.sessions_started", nil))
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.sessions_finished",
|
||||||
|
map[string]string{"outcome": execTelemetryOutcomeSuccess}))
|
||||||
|
require.EqualValues(t, 0, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.active_sessions", nil))
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.attachments",
|
||||||
|
map[string]string{"attachment_kind": execTelemetryAttachmentInitial}))
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.attachments",
|
||||||
|
map[string]string{"attachment_kind": execTelemetryAttachmentReconnect}))
|
||||||
|
require.EqualValues(t, 1, metricHistogramCount(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.setup_time"))
|
||||||
|
require.EqualValues(t, 1, metricHistogramCount(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.session_time"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecTelemetryRecordsSetupFailure(t *testing.T) {
|
||||||
|
harness := newExecTelemetryHarness(t)
|
||||||
|
setupTelemetry := harness.telemetry.startSetup(context.Background(),
|
||||||
|
execSessionKey{vmName: "vm-1"},
|
||||||
|
execSessionSpec{command: "echo hello"})
|
||||||
|
|
||||||
|
setupErr := errors.New("setup failed")
|
||||||
|
setupTelemetry.finish(setupErr)
|
||||||
|
require.NoError(t, harness.tracerProvider.ForceFlush(context.Background()))
|
||||||
|
|
||||||
|
setupSpan := findSpanByName(t, harness.spanRecorder.Ended(), "orchard.exec.setup")
|
||||||
|
require.Equal(t, codes.Error, setupSpan.Status().Code)
|
||||||
|
require.Equal(t, setupErr.Error(), setupSpan.Status().Description)
|
||||||
|
|
||||||
|
logs := harness.logExporter.snapshot()
|
||||||
|
require.Equal(t, []string{execTelemetryEventFailed}, logBodies(logs))
|
||||||
|
|
||||||
|
metrics := collectMetrics(t, harness.metricReader)
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.setup_attempts",
|
||||||
|
map[string]string{"outcome": execTelemetryOutcomeError}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecTelemetryRecordsLegacyDetachAsCanceledSession(t *testing.T) {
|
||||||
|
harness := newExecTelemetryHarness(t)
|
||||||
|
spec := execSessionSpec{command: "sleep forever"}
|
||||||
|
key := execSessionKey{vmName: "vm-1"}
|
||||||
|
|
||||||
|
setupTelemetry := harness.telemetry.startSetup(context.Background(), key, spec)
|
||||||
|
setupTelemetry.setVM(&v1.VM{Meta: v1.Meta{Name: "vm-1"}, UID: "uid-1", Worker: "worker-1"})
|
||||||
|
sessionTraceContext, sessionTelemetry := setupTelemetry.startSession()
|
||||||
|
setupTelemetry.finish(nil)
|
||||||
|
|
||||||
|
sessionContext, sessionContextCancel := context.WithCancel(sessionTraceContext)
|
||||||
|
session := newExecSessionWithContextAndSpec(
|
||||||
|
sessionContext,
|
||||||
|
sessionContextCancel,
|
||||||
|
key,
|
||||||
|
spec,
|
||||||
|
spec.command,
|
||||||
|
&fakeExec{
|
||||||
|
run: func(ctx context.Context, _ string, _ chan<- *execstream.Frame) error {
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
time.Minute,
|
||||||
|
legacyExecSessionPolicy,
|
||||||
|
sessionTelemetry,
|
||||||
|
)
|
||||||
|
|
||||||
|
subscriber, err := session.attach(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
session.start()
|
||||||
|
session.detach(subscriber)
|
||||||
|
<-session.done
|
||||||
|
|
||||||
|
metrics := collectMetrics(t, harness.metricReader)
|
||||||
|
require.EqualValues(t, 1, metricInt64Value(t, metrics,
|
||||||
|
"org.cirruslabs.orchard.controller.exec.sessions_finished",
|
||||||
|
map[string]string{"outcome": execTelemetryOutcomeCanceled}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecTelemetryRecordsSessionFailure(t *testing.T) {
|
||||||
|
harness := newExecTelemetryHarness(t)
|
||||||
|
spec := execSessionSpec{command: "exit 1"}
|
||||||
|
key := execSessionKey{vmName: "vm-1"}
|
||||||
|
|
||||||
|
setupTelemetry := harness.telemetry.startSetup(context.Background(), key, spec)
|
||||||
|
setupTelemetry.setVM(&v1.VM{Meta: v1.Meta{Name: "vm-1"}, UID: "uid-1", Worker: "worker-1"})
|
||||||
|
sessionTraceContext, sessionTelemetry := setupTelemetry.startSession()
|
||||||
|
setupTelemetry.finish(nil)
|
||||||
|
|
||||||
|
sessionContext, sessionContextCancel := context.WithCancel(sessionTraceContext)
|
||||||
|
session := newExecSessionWithContextAndSpec(
|
||||||
|
sessionContext,
|
||||||
|
sessionContextCancel,
|
||||||
|
key,
|
||||||
|
spec,
|
||||||
|
spec.command,
|
||||||
|
&fakeExec{
|
||||||
|
run: func(context.Context, string, chan<- *execstream.Frame) error {
|
||||||
|
return errors.New("command failed")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
time.Minute,
|
||||||
|
legacyExecSessionPolicy,
|
||||||
|
sessionTelemetry,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := session.attach(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
session.start()
|
||||||
|
<-session.done
|
||||||
|
require.NoError(t, harness.tracerProvider.ForceFlush(context.Background()))
|
||||||
|
|
||||||
|
require.Contains(t, logBodies(harness.logExporter.snapshot()), execTelemetryEventFailed)
|
||||||
|
sessionSpan := findSpanByName(t, harness.spanRecorder.Ended(), "orchard.exec.session")
|
||||||
|
require.Equal(t, codes.Error, sessionSpan.Status().Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecCommandHashIsStableAndDoesNotExposeRawCommand(t *testing.T) {
|
||||||
|
const command = "printf secret"
|
||||||
|
|
||||||
|
require.Equal(t, execCommandHash(command), execCommandHash(command))
|
||||||
|
require.NotEqual(t, command, execCommandHash(command))
|
||||||
|
require.NotEqual(t, execCommandHash(command), execCommandHash(command+"!"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecTelemetryMetricsAvoidHighCardinalityAttributes(t *testing.T) {
|
||||||
|
harness := newExecTelemetryHarness(t)
|
||||||
|
setupTelemetry := harness.telemetry.startSetup(context.Background(),
|
||||||
|
execSessionKey{vmName: "vm-1", sessionID: "session-1"},
|
||||||
|
execSessionSpec{command: "printf secret", interactive: true, tty: true})
|
||||||
|
setupTelemetry.setVM(&v1.VM{Meta: v1.Meta{Name: "vm-1"}, UID: "uid-1", Worker: "worker-1"})
|
||||||
|
setupTelemetry.finish(nil)
|
||||||
|
|
||||||
|
metrics := collectMetrics(t, harness.metricReader)
|
||||||
|
for _, metric := range flattenMetrics(metrics) {
|
||||||
|
for _, attributes := range metricAttributeSets(metric) {
|
||||||
|
require.NotContains(t, attributes, "vm_name")
|
||||||
|
require.NotContains(t, attributes, "vm_uid")
|
||||||
|
require.NotContains(t, attributes, "session_id")
|
||||||
|
require.NotContains(t, attributes, "command_sha256")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSpanByName(
|
||||||
|
t *testing.T,
|
||||||
|
spans []sdktrace.ReadOnlySpan,
|
||||||
|
name string,
|
||||||
|
) sdktrace.ReadOnlySpan {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
matches := findSpansByName(spans, name)
|
||||||
|
require.Len(t, matches, 1)
|
||||||
|
|
||||||
|
return matches[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSpansByName(spans []sdktrace.ReadOnlySpan, name string) []sdktrace.ReadOnlySpan {
|
||||||
|
var matches []sdktrace.ReadOnlySpan
|
||||||
|
for _, span := range spans {
|
||||||
|
if span.Name() == name {
|
||||||
|
matches = append(matches, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
func logBodies(records []sdklog.Record) []string {
|
||||||
|
result := make([]string, 0, len(records))
|
||||||
|
for _, record := range records {
|
||||||
|
result = append(result, record.Body().AsString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func logAttributes(record sdklog.Record) map[string]string {
|
||||||
|
result := map[string]string{}
|
||||||
|
record.WalkAttributes(func(attribute otellog.KeyValue) bool {
|
||||||
|
result[attribute.Key] = attribute.Value.AsString()
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectMetrics(t *testing.T, reader *sdkmetric.ManualReader) metricdata.ResourceMetrics {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var metrics metricdata.ResourceMetrics
|
||||||
|
require.NoError(t, reader.Collect(context.Background(), &metrics))
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenMetrics(metrics metricdata.ResourceMetrics) []metricdata.Metrics {
|
||||||
|
var result []metricdata.Metrics
|
||||||
|
for _, scopeMetrics := range metrics.ScopeMetrics {
|
||||||
|
result = append(result, scopeMetrics.Metrics...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func metricInt64Value(
|
||||||
|
t *testing.T,
|
||||||
|
metrics metricdata.ResourceMetrics,
|
||||||
|
name string,
|
||||||
|
requiredAttributes map[string]string,
|
||||||
|
) int64 {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, metric := range flattenMetrics(metrics) {
|
||||||
|
if metric.Name != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sum, ok := metric.Data.(metricdata.Sum[int64])
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
for _, dataPoint := range sum.DataPoints {
|
||||||
|
if attributesContain(dataPoint.Attributes.ToSlice(), requiredAttributes) {
|
||||||
|
return dataPoint.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("metric %q with attributes %v not found", name, requiredAttributes)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func metricHistogramCount(t *testing.T, metrics metricdata.ResourceMetrics, name string) uint64 {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, metric := range flattenMetrics(metrics) {
|
||||||
|
if metric.Name != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
histogram, ok := metric.Data.(metricdata.Histogram[float64])
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, histogram.DataPoints, 1)
|
||||||
|
|
||||||
|
return histogram.DataPoints[0].Count
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("metric %q not found", name)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func metricAttributeSets(metric metricdata.Metrics) []map[string]struct{} {
|
||||||
|
var result []map[string]struct{}
|
||||||
|
|
||||||
|
switch data := metric.Data.(type) {
|
||||||
|
case metricdata.Sum[int64]:
|
||||||
|
for _, dataPoint := range data.DataPoints {
|
||||||
|
result = append(result, attributeSet(dataPoint.Attributes.ToSlice()))
|
||||||
|
}
|
||||||
|
case metricdata.Histogram[float64]:
|
||||||
|
for _, dataPoint := range data.DataPoints {
|
||||||
|
result = append(result, attributeSet(dataPoint.Attributes.ToSlice()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func attributeSet(attributes []attribute.KeyValue) map[string]struct{} {
|
||||||
|
result := make(map[string]struct{}, len(attributes))
|
||||||
|
for _, attr := range attributes {
|
||||||
|
result[string(attr.Key)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func attributesContain(attributes []attribute.KeyValue, required map[string]string) bool {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := map[string]string{}
|
||||||
|
for _, attr := range attributes {
|
||||||
|
actual[string(attr.Key)] = attr.Value.AsString()
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range required {
|
||||||
|
if actual[key] != value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -2,15 +2,25 @@ package opentelemetry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||||
|
otellogglobal "go.opentelemetry.io/otel/log/global"
|
||||||
|
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||||||
"go.opentelemetry.io/otel/sdk/metric"
|
"go.opentelemetry.io/otel/sdk/metric"
|
||||||
"os"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
DefaultLogger = otellogglobal.Logger("")
|
||||||
DefaultMeter = otel.Meter("")
|
DefaultMeter = otel.Meter("")
|
||||||
|
DefaultTracer = otel.Tracer("")
|
||||||
)
|
)
|
||||||
|
|
||||||
func Configure(ctx context.Context) error {
|
func Configure(ctx context.Context) error {
|
||||||
|
|
@ -30,6 +40,12 @@ func Configure(ctx context.Context) error {
|
||||||
if err := setupMeterProvider(ctx); err != nil {
|
if err := setupMeterProvider(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := setupTracerProvider(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := setupLoggerProvider(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -54,3 +70,45 @@ func setupMeterProvider(ctx context.Context) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupTracerProvider(ctx context.Context) error {
|
||||||
|
httpExporter, err := otlptracehttp.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcExporter, err := otlptracegrpc.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tracerProvider := sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithBatcher(httpExporter),
|
||||||
|
sdktrace.WithBatcher(grpcExporter),
|
||||||
|
)
|
||||||
|
|
||||||
|
otel.SetTracerProvider(tracerProvider)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLoggerProvider(ctx context.Context) error {
|
||||||
|
httpExporter, err := otlploghttp.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcExporter, err := otlploggrpc.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loggerProvider := sdklog.NewLoggerProvider(
|
||||||
|
sdklog.WithProcessor(sdklog.NewBatchProcessor(httpExporter)),
|
||||||
|
sdklog.WithProcessor(sdklog.NewBatchProcessor(grpcExporter)),
|
||||||
|
)
|
||||||
|
|
||||||
|
otellogglobal.SetLoggerProvider(loggerProvider)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue