github.com/mailgun/holster/v4@v4.20.0/tracing/scope.go (about) 1 // Trace a code block as a scoped span. 2 // * Use instead of manual instrumentation: `tracer.Start()`/`span.End()`. 3 // * Must call `InitTracing()` first. 4 // * Automates start/end of span. 5 // * Tags file and line number where span started. 6 // * If function returned error: 7 // * Span is tagged as error. 8 // * Sets span attributes `otel.status_code` and `otel.status_description` 9 // with error details. 10 // * Logs error details to span. 11 12 package tracing 13 14 import ( 15 "context" 16 "runtime" 17 "strconv" 18 19 "github.com/mailgun/holster/v4/errors" 20 "go.opentelemetry.io/otel/attribute" 21 "go.opentelemetry.io/otel/codes" 22 "go.opentelemetry.io/otel/trace" 23 ) 24 25 type ScopeAction func(ctx context.Context) error 26 27 const ( 28 ErrorClassKey = "error.class" 29 ErrorTypeKey = "error.type" 30 ) 31 32 // StartScope start a scope with span named after fully qualified caller function. 33 func StartScope(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 34 return startScope(ctx, InfoLevel, 1, opts...) 35 } 36 37 // StartScopeDebug start a scope with span named after fully qualified caller function with 38 // debug log level. 39 func StartScopeDebug(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 40 return startScope(ctx, DebugLevel, 1, opts...) 41 } 42 43 // StartScopeInfo start a scope with span named after fully qualified caller function with 44 // info log level. 45 func StartScopeInfo(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 46 return startScope(ctx, InfoLevel, 1, opts...) 47 } 48 49 // StartScopeWarn start a scope with span named after fully qualified caller function with 50 // warning log level. 51 func StartScopeWarn(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 52 return startScope(ctx, WarnLevel, 1, opts...) 53 } 54 55 // StartScopeError start a scope with span named after fully qualified caller function with 56 // error log level. 57 func StartScopeError(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 58 return startScope(ctx, ErrorLevel, 1, opts...) 59 } 60 61 // Start a scope with user-provided span name. 62 func StartNamedScope(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 63 return startNamedScope(ctx, spanName, InfoLevel, 1, opts...) 64 } 65 66 // StartNamedScopeDebug start a scope with user-provided span name with debug log level. 67 func StartNamedScopeDebug(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 68 return startNamedScope(ctx, spanName, DebugLevel, 1, opts...) 69 } 70 71 // StartNamedScopeInfo start a scope with user-provided span name with info log level. 72 func StartNamedScopeInfo(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 73 return startNamedScope(ctx, spanName, InfoLevel, 1, opts...) 74 } 75 76 // StartNamedScopeWarn start a scope with user-provided span name with warning log level. 77 func StartNamedScopeWarn(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 78 return startNamedScope(ctx, spanName, WarnLevel, 1, opts...) 79 } 80 81 // StartNamedScopeError start a scope with user-provided span name with error log level. 82 func StartNamedScopeError(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 83 return startNamedScope(ctx, spanName, ErrorLevel, 1, opts...) 84 } 85 86 // BranchScope branch an existing scope with span named after fully qualified caller function. 87 func BranchScope(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 88 if !trace.SpanContextFromContext(ctx).IsValid() { 89 return ctx 90 } 91 return startScope(ctx, InfoLevel, 1, opts...) 92 } 93 94 // BranchScopeDebug branch an existing scope with span named after fully qualified caller function with 95 // debug log level. 96 func BranchScopeDebug(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 97 if !trace.SpanContextFromContext(ctx).IsValid() { 98 return ctx 99 } 100 return startScope(ctx, DebugLevel, 1, opts...) 101 } 102 103 // BranchScopeInfo branch an existing scope with span named after fully qualified caller function with 104 // info log level. 105 func BranchScopeInfo(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 106 if !trace.SpanContextFromContext(ctx).IsValid() { 107 return ctx 108 } 109 return startScope(ctx, InfoLevel, 1, opts...) 110 } 111 112 // BranchScopeWarn branch an existing scope with span named after fully qualified caller function with 113 // warn log level. 114 func BranchScopeWarn(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 115 if !trace.SpanContextFromContext(ctx).IsValid() { 116 return ctx 117 } 118 return startScope(ctx, WarnLevel, 1, opts...) 119 } 120 121 // BranchScopeError branch an existing scope with span named after fully qualified caller function with 122 // error log level. 123 func BranchScopeError(ctx context.Context, opts ...trace.SpanStartOption) context.Context { 124 if !trace.SpanContextFromContext(ctx).IsValid() { 125 return ctx 126 } 127 return startScope(ctx, ErrorLevel, 1, opts...) 128 } 129 130 // BranchNamedScope branch an existing scope with user-provided span name. 131 func BranchNamedScope(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 132 if !trace.SpanContextFromContext(ctx).IsValid() { 133 return ctx 134 } 135 return startNamedScope(ctx, spanName, InfoLevel, 1, opts...) 136 } 137 138 // BranchNamedScopeDebug branch an existing scope with user-provided span name with debug log level. 139 func BranchNamedScopeDebug(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 140 if !trace.SpanContextFromContext(ctx).IsValid() { 141 return ctx 142 } 143 return startNamedScope(ctx, spanName, DebugLevel, 1, opts...) 144 } 145 146 // BranchNamedScopeInfo branch an existing scope with user-provided span name with info log level. 147 func BranchNamedScopeInfo(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 148 if !trace.SpanContextFromContext(ctx).IsValid() { 149 return ctx 150 } 151 return startNamedScope(ctx, spanName, InfoLevel, 1, opts...) 152 } 153 154 // BranchNamedScopeWarn branch an existing scope with user-provided span name with warn log level. 155 func BranchNamedScopeWarn(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 156 if !trace.SpanContextFromContext(ctx).IsValid() { 157 return ctx 158 } 159 return startNamedScope(ctx, spanName, WarnLevel, 1, opts...) 160 } 161 162 // BranchNamedScopeError branch an existing scope with user-provided span name with error log level. 163 func BranchNamedScopeError(ctx context.Context, spanName string, opts ...trace.SpanStartOption) context.Context { 164 if !trace.SpanContextFromContext(ctx).IsValid() { 165 return ctx 166 } 167 return startNamedScope(ctx, spanName, ErrorLevel, 1, opts...) 168 } 169 170 // EndScope end scope created by `StartScope()`/`StartNamedScope()`. 171 // Logs error return value and ends span. 172 func EndScope(ctx context.Context, err error) { 173 span := trace.SpanFromContext(ctx) 174 175 // If scope returns an error, mark span with error. 176 if err != nil { 177 span.RecordError(err) 178 span.SetStatus(codes.Error, err.Error()) 179 180 if typedErr, ok := err.(*errors.TypedError); ok { 181 span.SetAttributes( 182 attribute.String(ErrorClassKey, typedErr.Class()), 183 attribute.String(ErrorTypeKey, typedErr.Type()), 184 ) 185 } 186 } 187 188 span.End() 189 } 190 191 var ( 192 // Deprecated: Use CallScope 193 Scope = CallScope 194 // Deprecated: Use CallScopeDebug 195 ScopeDebug = CallScopeDebug 196 // Deprecated: Use CallScopeInfo 197 ScopeInfo = CallScopeInfo 198 // Deprecated: Use CallScopeWarn 199 ScopeWarn = CallScopeWarn 200 // Deprecated: Use CallScopeError 201 ScopeError = CallScopeError 202 // Deprecated: Use CallScope 203 NamedScope = CallNamedScope 204 // Deprecated: Use CallNamedScopeDebug 205 NamedScopeDebug = CallNamedScopeDebug 206 // Deprecated: Use CallNamedScopeInfo 207 NamedScopeInfo = CallNamedScopeInfo 208 // Deprecated: Use CallNamedScopeWarn 209 NamedScopeWarn = CallNamedScopeWarn 210 // Deprecated: Use CallNamedScopeError 211 NamedScopeError = CallNamedScopeError 212 ) 213 214 // CallScope calls action function within a tracing span named after the calling 215 // function. 216 // Equivalent to wrapping a code block with `StartScope()`/`EndScope()`. 217 func CallScope(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 218 spanName, fileTag := getCallerSpanName(1) 219 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 220 err := action(ctx) 221 EndScope(ctx, err) 222 return err 223 } 224 225 // CallScopeDebug calls action function within a tracing span named after the calling 226 // function. Scope tagged with log level debug. 227 // Equivalent to wrapping a code block with `StartScope()`/`EndScope()`. 228 func CallScopeDebug(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 229 spanName, fileTag := getCallerSpanName(1) 230 ctx = startSpan(ctx, spanName, fileTag, DebugLevel, opts...) 231 err := action(ctx) 232 EndScope(ctx, err) 233 return err 234 } 235 236 // CallScopeInfo calls action function within a tracing span named after the calling 237 // function. Scope tagged with log level info. 238 // Equivalent to wrapping a code block with `StartScope()`/`EndScope()`. 239 func CallScopeInfo(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 240 spanName, fileTag := getCallerSpanName(1) 241 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 242 err := action(ctx) 243 EndScope(ctx, err) 244 return err 245 } 246 247 // CallScopeWarn calls action function within a tracing span named after the calling 248 // function. Scope tagged with log level warning. 249 // Equivalent to wrapping a code block with `StartScope()`/`EndScope()`. 250 func CallScopeWarn(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 251 spanName, fileTag := getCallerSpanName(1) 252 ctx = startSpan(ctx, spanName, fileTag, WarnLevel, opts...) 253 err := action(ctx) 254 EndScope(ctx, err) 255 return err 256 } 257 258 // CallScopeError calls action function within a tracing span named after the calling 259 // function. Scope tagged with log level error. 260 // Equivalent to wrapping a code block with `StartScope()`/`EndScope()`. 261 func CallScopeError(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 262 spanName, fileTag := getCallerSpanName(1) 263 ctx = startSpan(ctx, spanName, fileTag, ErrorLevel, opts...) 264 trace.SpanFromContext(ctx).SetAttributes(attribute.Bool("error", true)) 265 err := action(ctx) 266 EndScope(ctx, err) 267 return err 268 } 269 270 // CallNamedScope calls action function within a tracing span. 271 // Equivalent to wrapping a code block with `StartNamedScope()`/`EndScope()`. 272 func CallNamedScope(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 273 fileTag := getFileTag(1) 274 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 275 err := action(ctx) 276 EndScope(ctx, err) 277 return err 278 } 279 280 // CallNamedScopeDebug calls action function within a tracing span. Scope tagged 281 // with log level debug. 282 // Equivalent to wrapping a code block with `StartNamedScope()`/`EndScope()`. 283 func CallNamedScopeDebug(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 284 fileTag := getFileTag(1) 285 ctx = startSpan(ctx, spanName, fileTag, DebugLevel, opts...) 286 err := action(ctx) 287 EndScope(ctx, err) 288 return err 289 } 290 291 // CallNamedScopeInfo calls action function within a tracing span. Scope tagged 292 // with log level info. 293 // Equivalent to wrapping a code block with `StartNamedScope()`/`EndScope()`. 294 func CallNamedScopeInfo(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 295 fileTag := getFileTag(1) 296 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 297 err := action(ctx) 298 EndScope(ctx, err) 299 return err 300 } 301 302 // CallNamedScopeWarn calls action function within a tracing span. Scope tagged 303 // with log level warning. 304 // Equivalent to wrapping a code block with `StartNamedScope()`/`EndScope()`. 305 func CallNamedScopeWarn(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 306 fileTag := getFileTag(1) 307 ctx = startSpan(ctx, spanName, fileTag, WarnLevel, opts...) 308 err := action(ctx) 309 EndScope(ctx, err) 310 return err 311 } 312 313 // CallNamedScopeError calls action function within a tracing span. Scope tagged 314 // with log level error. 315 // Equivalent to wrapping a code block with `StartNamedScope()`/`EndScope()`. 316 func CallNamedScopeError(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 317 fileTag := getFileTag(1) 318 ctx = startSpan(ctx, spanName, fileTag, ErrorLevel, opts...) 319 trace.SpanFromContext(ctx).SetAttributes(attribute.Bool("error", true)) 320 err := action(ctx) 321 EndScope(ctx, err) 322 return err 323 } 324 325 // CallScopeBranch calls action function within a tracing span named after the 326 // calling function. 327 // Equivalent to wrapping a code block with `BranchScope()`/`EndScope()`. 328 func CallScopeBranch(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 329 if !trace.SpanContextFromContext(ctx).IsValid() { 330 return action(ctx) 331 } 332 spanName, fileTag := getCallerSpanName(1) 333 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 334 err := action(ctx) 335 EndScope(ctx, err) 336 return err 337 } 338 339 // CallScopeBranchDebug calls action function within a tracing span named after the 340 // calling function. Scope tagged with log level debug. 341 // Equivalent to wrapping a code block with `BranchScopeDebug()`/`EndScope()`. 342 func CallScopeBranchDebug(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 343 if !trace.SpanContextFromContext(ctx).IsValid() { 344 return action(ctx) 345 } 346 spanName, fileTag := getCallerSpanName(1) 347 ctx = startSpan(ctx, spanName, fileTag, DebugLevel, opts...) 348 err := action(ctx) 349 EndScope(ctx, err) 350 return err 351 } 352 353 // CallScopeBranchInfo calls action function within a tracing span named after the 354 // calling function. Scope tagged with log level info. 355 // Equivalent to wrapping a code block with `BranchScopeInfo()`/`EndScope()`. 356 func CallScopeBranchInfo(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 357 if !trace.SpanContextFromContext(ctx).IsValid() { 358 return action(ctx) 359 } 360 spanName, fileTag := getCallerSpanName(1) 361 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 362 err := action(ctx) 363 EndScope(ctx, err) 364 return err 365 } 366 367 // CallScopeBranchWarn calls action function within a tracing span named after the 368 // calling function. Scope tagged with log level warn. 369 // Equivalent to wrapping a code block with `BranchScopeWarn()`/`EndScope()`. 370 func CallScopeBranchWarn(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 371 if !trace.SpanContextFromContext(ctx).IsValid() { 372 return action(ctx) 373 } 374 spanName, fileTag := getCallerSpanName(1) 375 ctx = startSpan(ctx, spanName, fileTag, WarnLevel, opts...) 376 err := action(ctx) 377 EndScope(ctx, err) 378 return err 379 } 380 381 // CallScopeBranchError calls action function within a tracing span named after the 382 // calling function. Scope tagged with log level error. 383 // Equivalent to wrapping a code block with `BranchScopeError()`/`EndScope()`. 384 func CallScopeBranchError(ctx context.Context, action ScopeAction, opts ...trace.SpanStartOption) error { 385 if !trace.SpanContextFromContext(ctx).IsValid() { 386 return action(ctx) 387 } 388 spanName, fileTag := getCallerSpanName(1) 389 ctx = startSpan(ctx, spanName, fileTag, ErrorLevel, opts...) 390 err := action(ctx) 391 EndScope(ctx, err) 392 return err 393 } 394 395 // CallNamedScopeBranch calls action function within an existing tracing span. 396 // Equivalent to wrapping a code block with `BranchNamedScope()`/`EndScope()`. 397 func CallNamedScopeBranch(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 398 if !trace.SpanContextFromContext(ctx).IsValid() { 399 return action(ctx) 400 } 401 fileTag := getFileTag(1) 402 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 403 err := action(ctx) 404 EndScope(ctx, err) 405 return err 406 } 407 408 // CallNamedScopeBranchDebug calls action function within an existing tracing span. 409 // Scope tagged with log level debug. 410 // Equivalent to wrapping a code block with `BranchNamedScope()`/`EndScope()`. 411 func CallNamedScopeBranchDebug(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 412 if !trace.SpanContextFromContext(ctx).IsValid() { 413 return action(ctx) 414 } 415 fileTag := getFileTag(1) 416 ctx = startSpan(ctx, spanName, fileTag, DebugLevel, opts...) 417 err := action(ctx) 418 EndScope(ctx, err) 419 return err 420 } 421 422 // CallNamedScopeBranchInfo calls action function within an existing tracing span. 423 // Scope tagged with log level debug. 424 // Equivalent to wrapping a code block with `BranchNamedScope()`/`EndScope()`. 425 func CallNamedScopeBranchInfo(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 426 if !trace.SpanContextFromContext(ctx).IsValid() { 427 return action(ctx) 428 } 429 fileTag := getFileTag(1) 430 ctx = startSpan(ctx, spanName, fileTag, InfoLevel, opts...) 431 err := action(ctx) 432 EndScope(ctx, err) 433 return err 434 } 435 436 // CallNamedScopeBranchWarn calls action function within an existing tracing span. 437 // Scope tagged with log level debug. 438 // Equivalent to wrapping a code block with `BranchNamedScope()`/`EndScope()`. 439 func CallNamedScopeBranchWarn(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 440 if !trace.SpanContextFromContext(ctx).IsValid() { 441 return action(ctx) 442 } 443 fileTag := getFileTag(1) 444 ctx = startSpan(ctx, spanName, fileTag, WarnLevel, opts...) 445 err := action(ctx) 446 EndScope(ctx, err) 447 return err 448 } 449 450 // CallNamedScopeBranchError calls action function within an existing tracing span. 451 // Scope tagged with log level debug. 452 // Equivalent to wrapping a code block with `BranchNamedScope()`/`EndScope()`. 453 func CallNamedScopeBranchError(ctx context.Context, spanName string, action ScopeAction, opts ...trace.SpanStartOption) error { 454 if !trace.SpanContextFromContext(ctx).IsValid() { 455 return action(ctx) 456 } 457 fileTag := getFileTag(1) 458 ctx = startSpan(ctx, spanName, fileTag, ErrorLevel, opts...) 459 err := action(ctx) 460 EndScope(ctx, err) 461 return err 462 } 463 464 func startScope(ctx context.Context, level Level, skipCallers int, opts ...trace.SpanStartOption) context.Context { 465 if level <= ErrorLevel { 466 opts = append(opts, trace.WithAttributes( 467 attribute.Bool("error", true), 468 )) 469 } 470 471 spanName, fileTag := getCallerSpanName(skipCallers + 1) 472 return startSpan(ctx, spanName, fileTag, level, opts...) 473 } 474 475 func startNamedScope(ctx context.Context, spanName string, level Level, skipCallers int, opts ...trace.SpanStartOption) context.Context { 476 if level <= ErrorLevel { 477 opts = append(opts, trace.WithAttributes( 478 attribute.Bool("error", true), 479 )) 480 } 481 482 fileTag := getFileTag(skipCallers + 1) 483 return startSpan(ctx, spanName, fileTag, level, opts...) 484 } 485 486 func startSpan(ctx context.Context, spanName, fileTag string, level Level, opts ...trace.SpanStartOption) context.Context { 487 opts = append(opts, trace.WithAttributes( 488 attribute.String("file", fileTag), 489 )) 490 491 // Embed log level parameter as context value. 492 ctx = context.WithValue(ctx, logLevelCtxKey, level) 493 ctx, _ = Tracer().Start(ctx, spanName, opts...) 494 return ctx 495 } 496 497 // getCallerSpanName returns function and file name:line of the caller. 498 // 499 // Use skip=0 to get the caller of getCallerSpanName. 500 // 501 // Use skip=1 to get the caller of the caller(a getCallerSpanName() wrapper) and so on. 502 func getCallerSpanName(skip int) (spanName, fileTag string) { 503 pc, file, line, ok := runtime.Caller(skip + 1) 504 505 // Determine source file and line number. 506 if ok { 507 fileTag = file + ":" + strconv.Itoa(line) 508 spanName = runtime.FuncForPC(pc).Name() 509 } else { 510 // Rare condition. Probably a bug in caller. 511 fileTag = "unknown" 512 } 513 return 514 } 515 516 // getFileTag returns file name:line of the caller. 517 // 518 // Use skip=0 to get the caller of getFileTag. 519 // 520 // Use skip=1 to get the caller of the caller(a getFileTag() wrapper). 521 func getFileTag(skip int) string { 522 _, file, line, ok := runtime.Caller(skip + 1) 523 524 // Determine source file and line number. 525 if !ok { 526 // Rare condition. Probably a bug in caller. 527 return "unknown" 528 } 529 530 return file + ":" + strconv.Itoa(line) 531 }