orchard/internal/controller/exec_telemetry.go

412 lines
11 KiB
Go

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
}
}