2
0

span.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. package telemetry
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "runtime"
  8. "strings"
  9. "time"
  10. "github.com/google/uuid"
  11. "github.com/porter-dev/porter/api/types"
  12. "github.com/porter-dev/porter/internal/models"
  13. "go.opentelemetry.io/otel"
  14. "go.opentelemetry.io/otel/attribute"
  15. "go.opentelemetry.io/otel/codes"
  16. "go.opentelemetry.io/otel/trace"
  17. )
  18. // AttributeKey helps enforce consistent naming conventions for attribute key
  19. type AttributeKey string
  20. // NewSpan is a convenience function for creating a new span, with a Porter-namespaced name to avoid conflicts.
  21. // Any commonly used variables in the context, will be added to the span such as clusterID, projectID.
  22. // When using this function, make sure to call `defer span.End()` immediately after, to avoid lost spans.
  23. func NewSpan(ctx context.Context, name string) (context.Context, trace.Span) {
  24. ctx, span := otel.Tracer("").Start(ctx, prefixSpanKey(name))
  25. AddKnownContextVariablesToSpan(ctx, span)
  26. return ctx, span
  27. }
  28. // AddKnownContextVariablesToSpan adds known commonly read context variables to a span
  29. func AddKnownContextVariablesToSpan(ctx context.Context, span trace.Span) {
  30. user, ok := ctx.Value(types.UserScope).(*models.User)
  31. if ok {
  32. WithAttributes(span, AttributeKV{Key: "user-id", Value: user.ID})
  33. }
  34. cluster, ok := ctx.Value(types.ClusterScope).(*models.Cluster)
  35. if ok {
  36. WithAttributes(span, AttributeKV{Key: "cluster-id", Value: cluster.ID})
  37. }
  38. if project, ok := ctx.Value(types.ProjectScope).(*models.Project); ok {
  39. WithAttributes(span, AttributeKV{Key: "project-id", Value: project.ID})
  40. }
  41. if deploymentTarget, ok := ctx.Value(types.DeploymentTargetScope).(types.DeploymentTarget); ok {
  42. WithAttributes(span, AttributeKV{Key: "deployment-target-id", Value: deploymentTarget.ID})
  43. }
  44. }
  45. // AttributeKV is a wrapper for otel attributes KV
  46. type AttributeKV struct {
  47. Key AttributeKey
  48. Value any
  49. }
  50. // WithAttributes is a convenience function for adding attributes to a given span
  51. // This will also add the namespaced prefix to the keys
  52. func WithAttributes(span trace.Span, attrs ...AttributeKV) {
  53. for _, attr := range attrs {
  54. if attr.Key != "" {
  55. switch val := attr.Value.(type) {
  56. case uuid.UUID:
  57. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val.String()))
  58. case string:
  59. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val))
  60. case []string:
  61. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), strings.Join(val, ", ")))
  62. case sql.NullString:
  63. if val.Valid {
  64. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val.String))
  65. } else {
  66. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), "NULL"))
  67. }
  68. case int:
  69. span.SetAttributes(attribute.Int(prefixSpanKey(string(attr.Key)), val))
  70. case int64:
  71. span.SetAttributes(attribute.Int64(prefixSpanKey(string(attr.Key)), val))
  72. case int32:
  73. span.SetAttributes(attribute.Int64(prefixSpanKey(string(attr.Key)), int64(val)))
  74. case uint:
  75. span.SetAttributes(attribute.Int(prefixSpanKey(string(attr.Key)), int(val)))
  76. case float64:
  77. span.SetAttributes(attribute.Float64(prefixSpanKey(string(attr.Key)), val))
  78. case bool:
  79. span.SetAttributes(attribute.Bool(prefixSpanKey(string(attr.Key)), val))
  80. case time.Time:
  81. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val.String()))
  82. zone, offset := val.Zone()
  83. span.SetAttributes(attribute.String(prefixSpanKey(fmt.Sprintf("%s-timezone", string(attr.Key))), zone))
  84. span.SetAttributes(attribute.Int(prefixSpanKey(fmt.Sprintf("%s-offset", string(attr.Key))), offset))
  85. }
  86. }
  87. }
  88. }
  89. // Error adds the given error message and related context to a span
  90. // If message is empty, the span status_message will be set to the error
  91. // It is advised to let the raw error be set at err, and message to be a human
  92. // readable string
  93. func Error(_ context.Context, span trace.Span, err error, message string) error {
  94. if message != "" {
  95. WithAttributes(span, AttributeKV{Key: "message", Value: message})
  96. }
  97. if err == nil {
  98. err = errors.New(message)
  99. }
  100. WithAttributes(span, AttributeKV{Key: "error-message", Value: err.Error()}) // exact same as `status_message` but gives a more consolidated view when searching by porter.run/
  101. _, fn, line, _ := runtime.Caller(1)
  102. stackTraceLocation := fmt.Sprintf("%s:%d", fn, line)
  103. WithAttributes(span, AttributeKV{Key: "stack-trace-location", Value: stackTraceLocation})
  104. span.SetStatus(codes.Error, err.Error())
  105. span.RecordError(err)
  106. return err
  107. }
  108. func prefixSpanKey(name string) string {
  109. return fmt.Sprintf("porter.run/%s", name)
  110. }