span.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  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. func AddKnownContextVariablesToSpan(ctx context.Context, span trace.Span) {
  29. user, ok := ctx.Value(types.UserScope).(*models.User)
  30. if ok {
  31. WithAttributes(span, AttributeKV{Key: "user-id", Value: user.ID})
  32. }
  33. cluster, ok := ctx.Value(types.ClusterScope).(*models.Cluster)
  34. if ok {
  35. WithAttributes(span, AttributeKV{Key: "cluster-id", Value: cluster.ID})
  36. }
  37. if project, ok := ctx.Value(types.ProjectScope).(*models.Project); ok {
  38. WithAttributes(span, AttributeKV{Key: "project-id", Value: project.ID})
  39. }
  40. }
  41. // AttributeKV is a wrapper for otel attributes KV
  42. type AttributeKV struct {
  43. Key AttributeKey
  44. Value any
  45. }
  46. // WithAttributes is a convenience function for adding attributes to a given span
  47. // This will also add the namespaced prefix to the keys
  48. func WithAttributes(span trace.Span, attrs ...AttributeKV) {
  49. for _, attr := range attrs {
  50. if attr.Key != "" {
  51. switch val := attr.Value.(type) {
  52. case uuid.UUID:
  53. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val.String()))
  54. case string:
  55. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val))
  56. case []string:
  57. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), strings.Join(val, ", ")))
  58. case sql.NullString:
  59. if val.Valid {
  60. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val.String))
  61. } else {
  62. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), "NULL"))
  63. }
  64. case int:
  65. span.SetAttributes(attribute.Int(prefixSpanKey(string(attr.Key)), val))
  66. case int64:
  67. span.SetAttributes(attribute.Int64(prefixSpanKey(string(attr.Key)), val))
  68. case int32:
  69. span.SetAttributes(attribute.Int64(prefixSpanKey(string(attr.Key)), int64(val)))
  70. case uint:
  71. span.SetAttributes(attribute.Int(prefixSpanKey(string(attr.Key)), int(val)))
  72. case float64:
  73. span.SetAttributes(attribute.Float64(prefixSpanKey(string(attr.Key)), val))
  74. case bool:
  75. span.SetAttributes(attribute.Bool(prefixSpanKey(string(attr.Key)), val))
  76. case time.Time:
  77. span.SetAttributes(attribute.String(prefixSpanKey(string(attr.Key)), val.String()))
  78. zone, offset := val.Zone()
  79. span.SetAttributes(attribute.String(prefixSpanKey(fmt.Sprintf("%s-timezone", string(attr.Key))), zone))
  80. span.SetAttributes(attribute.Int(prefixSpanKey(fmt.Sprintf("%s-offset", string(attr.Key))), offset))
  81. }
  82. }
  83. }
  84. }
  85. // Error adds the given error message and related context to a span
  86. // If message is empty, the span status_message will be set to the error
  87. // It is advised to let the raw error be set at err, and message to be a human
  88. // readable string
  89. func Error(_ context.Context, span trace.Span, err error, message string) error {
  90. if message != "" {
  91. WithAttributes(span, AttributeKV{Key: "message", Value: message})
  92. }
  93. if err == nil {
  94. err = errors.New(message)
  95. }
  96. 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/
  97. _, fn, line, _ := runtime.Caller(1)
  98. stackTraceLocation := fmt.Sprintf("%s:%d", fn, line)
  99. WithAttributes(span, AttributeKV{Key: "stack-trace-location", Value: stackTraceLocation})
  100. span.SetStatus(codes.Error, err.Error())
  101. span.RecordError(err)
  102. return err
  103. }
  104. func prefixSpanKey(name string) string {
  105. return fmt.Sprintf("porter.run/%s", name)
  106. }