diff --git a/Package.swift b/Package.swift index 1a1ef2ba..fda563f7 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,5 @@ // swift-tools-version:5.9 +import CompilerPluginSupport import PackageDescription let package = Package( @@ -6,9 +7,11 @@ let package = Package( products: [ .library(name: "Instrumentation", targets: ["Instrumentation"]), .library(name: "Tracing", targets: ["Tracing"]), + .library(name: "TracingMacros", targets: ["TracingMacros"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-service-context.git", from: "1.1.0") + .package(url: "https://github.com/apple/swift-service-context.git", from: "1.1.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), ], targets: [ // ==== -------------------------------------------------------------------------------------------------------- @@ -43,6 +46,33 @@ let package = Package( .target(name: "Tracing") ] ), + + // ==== -------------------------------------------------------------------------------------------------------- + // MARK: TracingMacros + + .target( + name: "TracingMacros", + dependencies: [ + .target(name: "Tracing"), + .target(name: "TracingMacrosImplementation"), + ] + ), + .macro( + name: "TracingMacrosImplementation", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + ] + ), + .testTarget( + name: "TracingMacrosTests", + dependencies: [ + .target(name: "Tracing"), + .target(name: "TracingMacros"), + .target(name: "TracingMacrosImplementation"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), ] ) diff --git a/Sources/TracingMacros/Docs.docc/TracedOperationName.md b/Sources/TracingMacros/Docs.docc/TracedOperationName.md new file mode 100644 index 00000000..2ebbee81 --- /dev/null +++ b/Sources/TracingMacros/Docs.docc/TracedOperationName.md @@ -0,0 +1,38 @@ +# ``TracingMacros/TracedOperationName`` + +### Examples + +The default behavior is to use the base name of the function, but you can +explicitly specify this as well. This creates a span named `"preheatOven"`: +```swift +@Traced(.baseName) +func preheatOven(temperature: Int) +``` + +You can request the full name of the function as the span name, this +creates a span named `"preheatOven(temperature:)"`: +```swift +@Traced(.fullName) +func preheatOven(temperature: Int) +``` + +And it is also initializable with a string literal for fully custom names, +this creates a span explicitly named `"preheat oven"`: +```swift +@Traced("preheat oven") +func preheatOven(temperature: Int) +``` +And if you need to load an existing string value as a name, you can use +`.string(someString)` to adapt it. + + +## Topics + +### Create Operation Names +- ``baseName`` +- ``fullName`` +- ``string(_:)`` +- ``init(stringLiteral:)`` + +### Convert an Operation Name to a String +- ``operationName(baseName:fullName:)`` diff --git a/Sources/TracingMacros/Docs.docc/index.md b/Sources/TracingMacros/Docs.docc/index.md new file mode 100644 index 00000000..536d3615 --- /dev/null +++ b/Sources/TracingMacros/Docs.docc/index.md @@ -0,0 +1,19 @@ +# ``TracingMacros`` + +Macro helpers for Tracing. + +## Overview + +The TracingMacros module provides optional macros to make it easier to write traced code. + +The ``Traced(_:context:ofKind:span:)`` macro lets you avoid the extra indentation that comes with +adopting traced code, and avoids having to keep the throws/try and async/await +in-sync with the body. You can just attach `@Traced` to a function and get +started. + +## Topics + +### Tracing functions +- ``Traced(_:context:ofKind:span:)`` +- ``TracedOperationName`` + diff --git a/Sources/TracingMacros/TracedMacro.swift b/Sources/TracingMacros/TracedMacro.swift new file mode 100644 index 00000000..8df7dbaf --- /dev/null +++ b/Sources/TracingMacros/TracedMacro.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing open source project +// +// Copyright (c) 2020-2024 Apple Inc. and the Swift Distributed Tracing project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Distributed Tracing project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +@_exported import ServiceContextModule +import Tracing + +/// A span name for a traced operation, either derived from the function name or explicitly specified. +/// +/// When using the ``Traced(_:context:ofKind:span:)`` macro, you can use this to customize the span name. +public struct TracedOperationName: ExpressibleByStringLiteral { + @usableFromInline + let value: Name + + @usableFromInline + enum Name { + case baseName + case fullName + case string(String) + } + + internal init(value: Name) { + self.value = value + } + + /// Use a literal string as an operation name. + public init(stringLiteral: String) { + value = .string(stringLiteral) + } + + /// Use the base name of the attached function. + /// + /// For `func preheatOven(temperature: Int)` this is `"preheatOven"`. + public static let baseName = TracedOperationName(value: .baseName) + + /// Use the full name of the attached function. + /// + /// For `func preheatOven(temperature: Int)` this is `"preheatOven(temperature:)"`. + /// This is provided by the `#function` macro. + public static let fullName = TracedOperationName(value: .fullName) + + /// Use an explicitly specified operation name. + public static func string(_ text: String) -> Self { + .init(value: .string(text)) + } + + /// Helper logic to support the `Traced` macro turning this operation name into a string. + /// Provided as an inference guide. + /// + /// - Parameters: + /// - baseName: The value to use for the ``baseName`` case. Must be + /// specified explicitly because there's no equivalent of `#function`. + /// - fullName: The value to use for the ``fullName`` case. + @inlinable + @_documentation(visibility: internal) + public static func _getOperationName(_ name: Self, baseName: String, fullName: String = #function) -> String { + switch name.value { + case .baseName: baseName + case .fullName: fullName + case let .string(text): text + } + } +} + +#if compiler(>=6.0) +/// Instrument a function to place the entire body inside a span. +/// +/// This macro is equivalent to calling ``withSpan`` in the body, but saves an +/// indentation level and duplication. It introduces a `span` variable into the +/// body of the function which can be used to add attributes to the span. +/// +/// Parameters are passed directly to ``withSpan`` where applicable, +/// and omitting the parameters from the macro omit them from the call, falling +/// back to the default. +/// +/// - Parameters: +/// - operationName: The name of the operation being traced. +/// - context: The `ServiceContext` providing information on where to start the new ``Span``. +/// - kind: The ``SpanKind`` of the new ``Span``. +/// - spanName: The name of the span variable to introduce in the function. Pass `"_"` to omit it. +@attached(body) +public macro Traced( + _ operationName: TracedOperationName = .baseName, + context: ServiceContext? = nil, + ofKind kind: SpanKind? = nil, + span spanName: String = "span" +) = #externalMacro(module: "TracingMacrosImplementation", type: "TracedMacro") +#endif diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift new file mode 100644 index 00000000..31c699f8 --- /dev/null +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -0,0 +1,159 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing open source project +// +// Copyright (c) 2020-2024 Apple Inc. and the Swift Distributed Tracing project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Distributed Tracing project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +#if compiler(>=6.0) +public struct TracedMacro: BodyMacro { + public static func expansion( + of node: AttributeSyntax, + providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, + in context: some MacroExpansionContext + ) throws -> [CodeBlockItemSyntax] { + guard let function = declaration.as(FunctionDeclSyntax.self), + let body = function.body + else { + throw MacroExpansionErrorMessage("expected a function with a body") + } + + // Construct a withSpan call matching the invocation of the @Traced macro + let (operationName, context, kind, spanName) = try extractArguments(from: node) + let baseNameExpr = ExprSyntax(StringLiteralExprSyntax(content: function.name.text)) + + var withSpanCall = FunctionCallExprSyntax("withSpan()" as ExprSyntax)! + let operationNameExpr: ExprSyntax + if let operationName { + if operationName.is(StringLiteralExprSyntax.self) { + operationNameExpr = operationName + } else { + operationNameExpr = "TracedOperationName._getOperationName(\(operationName), baseName: \(baseNameExpr))" + } + } else { + operationNameExpr = baseNameExpr + } + withSpanCall.arguments.append(LabeledExprSyntax(expression: operationNameExpr)) + func appendComma() { + withSpanCall.arguments[withSpanCall.arguments.index(before: withSpanCall.arguments.endIndex)] + .trailingComma = .commaToken() + } + if let context { + appendComma() + withSpanCall.arguments.append(LabeledExprSyntax(label: "context", expression: context)) + } + if let kind { + appendComma() + withSpanCall.arguments.append(LabeledExprSyntax(label: "ofKind", expression: kind)) + } + + // Introduce a span identifier in scope + var spanIdentifier: TokenSyntax = "span" + if let spanName { + spanIdentifier = .identifier(spanName) + } + + // We want to explicitly specify the closure effect specifiers in order + // to avoid warnings about unused try/await expressions. + // We might as well explicitly specify the closure return type to help type inference. + + let asyncClause = function.signature.effectSpecifiers?.asyncSpecifier + let returnClause = function.signature.returnClause + var throwsClause = function.signature.effectSpecifiers?.throwsClause + // You aren't allowed to apply "rethrows" as a closure effect + // specifier, so we have to convert this to a "throws" effect + if throwsClause?.throwsSpecifier.tokenKind == .keyword(.rethrows) { + throwsClause?.throwsSpecifier = .keyword(.throws) + } + var withSpanExpr: ExprSyntax = """ + \(withSpanCall) { \(spanIdentifier) \(asyncClause)\(throwsClause)\(returnClause)in \(body.statements) } + """ + + // Apply a try / await as necessary to adapt the withSpan expression + + if function.signature.effectSpecifiers?.asyncSpecifier != nil { + withSpanExpr = "await \(withSpanExpr)" + } + + if function.signature.effectSpecifiers?.throwsClause != nil { + withSpanExpr = "try \(withSpanExpr)" + } + + return ["\(withSpanExpr)"] + } + + static func extractArguments( + from node: AttributeSyntax + ) throws -> ( + operationName: ExprSyntax?, + context: ExprSyntax?, + kind: ExprSyntax?, + spanName: String? + ) { + // If there are no arguments, we don't have to do any of these bindings + guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) else { + return (nil, nil, nil, nil) + } + + func getArgument(label: String) -> ExprSyntax? { + arguments.first(where: { $0.label?.identifier?.name == label })?.expression + } + + // The operation name is the first argument if it's unlabeled + var operationName: ExprSyntax? + if let firstArgument = arguments.first, firstArgument.label == nil { + operationName = firstArgument.expression + } + + let context = getArgument(label: "context") + let kind = getArgument(label: "ofKind") + var spanName: String? + let spanNameExpr = getArgument(label: "span") + if let spanNameExpr { + guard let stringLiteral = spanNameExpr.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + let segment = stringLiteral.segments.first, + let segmentText = segment.as(StringSegmentSyntax.self) + else { + throw MacroExpansionErrorMessage("span name must be a simple string literal") + } + let text = segmentText.content.text + let isValidIdentifier = DeclReferenceExprSyntax("\(raw: text)" as ExprSyntax)?.hasError == false + let isValidWildcard = text == "_" + guard isValidIdentifier || isValidWildcard else { + throw MacroExpansionErrorMessage("'\(text)' is not a valid parameter name") + } + spanName = text + } + return ( + operationName: operationName, + context: context, + kind: kind, + spanName: spanName + ) + } + +} +#endif + +@main +struct TracingMacroPlugin: CompilerPlugin { + #if compiler(>=6.0) + let providingMacros: [Macro.Type] = [ + TracedMacro.self + ] + #else + let providingMacros: [Macro.Type] = [] + #endif +} diff --git a/Tests/TracingMacrosTests/TracedTests.swift b/Tests/TracingMacrosTests/TracedTests.swift new file mode 100644 index 00000000..94cc3345 --- /dev/null +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -0,0 +1,539 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Distributed Tracing open source project +// +// Copyright (c) 2020-2024 Apple Inc. and the Swift Distributed Tracing project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift Distributed Tracing project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntaxMacrosTestSupport +import Tracing +import TracingMacros +import TracingMacrosImplementation +import XCTest + +final class TracedMacroTests: XCTestCase { + #if compiler(>=6.0) + func test_tracedMacro_requires_body() { + assertMacroExpansion( + """ + @Traced + func funcWithoutBody() + """, + expandedSource: """ + func funcWithoutBody() + """, + diagnostics: [ + .init(message: "expected a function with a body", line: 1, column: 1) + ], + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_sync_nothrow() { + assertMacroExpansion( + """ + @Traced + func syncNonthrowingExample(param: Int) { + print(param) + } + """, + expandedSource: """ + func syncNonthrowingExample(param: Int) { + withSpan("syncNonthrowingExample") { span in + print(param) + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_sync_throws() { + assertMacroExpansion( + """ + @Traced + func syncThrowingExample(param: Int) throws { + struct ExampleError: Error { + } + throw ExampleError() + } + """, + expandedSource: """ + func syncThrowingExample(param: Int) throws { + try withSpan("syncThrowingExample") { span throws in + struct ExampleError: Error { + } + throw ExampleError() + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_sync_rethrows() { + assertMacroExpansion( + """ + @Traced + func syncRethrowingExample(body: () throws -> Int) rethrows -> Int { + print("Starting") + let result = try body() + print("Ending") + return result + } + """, + expandedSource: """ + func syncRethrowingExample(body: () throws -> Int) rethrows -> Int { + try withSpan("syncRethrowingExample") { span throws -> Int in + print("Starting") + let result = try body() + print("Ending") + return result + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_async_nothrow() { + assertMacroExpansion( + """ + @Traced + func asyncNonthrowingExample(param: Int) async { + print(param) + } + """, + expandedSource: """ + func asyncNonthrowingExample(param: Int) async { + await withSpan("asyncNonthrowingExample") { span async in + print(param) + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_async_throws() { + assertMacroExpansion( + """ + @Traced + func asyncThrowingExample(param: Int) async throws { + try await Task.sleep(for: .seconds(1)) + print("Hello") + } + """, + expandedSource: """ + func asyncThrowingExample(param: Int) async throws { + try await withSpan("asyncThrowingExample") { span async throws in + try await Task.sleep(for: .seconds(1)) + print("Hello") + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_async_rethrows() { + assertMacroExpansion( + """ + @Traced + func asyncRethrowingExample(body: () async throws -> Int) async rethrows -> Int { + try? await Task.sleep(for: .seconds(1)) + let result = try await body() + span.attributes["result"] = result + return result + } + """, + expandedSource: """ + func asyncRethrowingExample(body: () async throws -> Int) async rethrows -> Int { + try await withSpan("asyncRethrowingExample") { span async throws -> Int in + try? await Task.sleep(for: .seconds(1)) + let result = try await body() + span.attributes["result"] = result + return result + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + // Testing that this expands correctly, but not including this as a + // compile-test because withSpan doesn't currently support typed throws. + func test_tracedMacro_async_typed_throws() { + assertMacroExpansion( + """ + @Traced + func asyncTypedThrowingExample(body: () async throws(Err) -> Int) async throws(Err) -> Int { + try? await Task.sleep(for: .seconds(1)) + let result = try await body() + span.attributes["result"] = result + return result + } + """, + expandedSource: """ + func asyncTypedThrowingExample(body: () async throws(Err) -> Int) async throws(Err) -> Int { + try await withSpan("asyncTypedThrowingExample") { span async throws(Err) -> Int in + try? await Task.sleep(for: .seconds(1)) + let result = try await body() + span.attributes["result"] = result + return result + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_accessSpan() { + assertMacroExpansion( + """ + @Traced + func example(param: Int) { + span.attributes["param"] = param + } + """, + expandedSource: """ + func example(param: Int) { + withSpan("example") { span in + span.attributes["param"] = param + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_specifyOperationName() { + assertMacroExpansion( + """ + @Traced("example but with a custom operationName") + func example(param: Int) { + span.attributes["param"] = param + } + """, + expandedSource: """ + func example(param: Int) { + withSpan("example but with a custom operationName") { span in + span.attributes["param"] = param + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + + assertMacroExpansion( + """ + let globalName = "example" + + @Traced(.string(globalName)) + func example(param: Int) { + span.attributes["param"] = param + } + """, + expandedSource: """ + let globalName = "example" + func example(param: Int) { + withSpan(TracedOperationName._getOperationName(.string(globalName), baseName: "example")) { span in + span.attributes["param"] = param + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + + assertMacroExpansion( + """ + @Traced(.baseName) + func useBaseName(param: Int) { + span.attributes["param"] = param + } + """, + expandedSource: """ + func useBaseName(param: Int) { + withSpan(TracedOperationName._getOperationName(.baseName, baseName: "useBaseName")) { span in + span.attributes["param"] = param + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + + assertMacroExpansion( + """ + @Traced(.fullName) + func useFullName(param: Int) { + span.attributes["param"] = param + } + """, + expandedSource: """ + func useFullName(param: Int) { + withSpan(TracedOperationName._getOperationName(.fullName, baseName: "useFullName")) { span in + span.attributes["param"] = param + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_specifyContext() { + assertMacroExpansion( + """ + @Traced(context: .topLevel) + func example() { + print("Hello") + } + """, + expandedSource: """ + func example() { + withSpan("example", context: .topLevel) { span in + print("Hello") + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_specifyKind() { + assertMacroExpansion( + """ + @Traced(ofKind: .client) + func example() { + print("Hello") + } + """, + expandedSource: """ + func example() { + withSpan("example", ofKind: .client) { span in + print("Hello") + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_specifySpanBindingName() { + assertMacroExpansion( + """ + @Traced(span: "customSpan") + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, + expandedSource: """ + func example(span: String) throws { + try withSpan("example") { customSpan throws in + customSpan.attributes["span"] = span + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + + assertMacroExpansion( + """ + @Traced(span: "_") + func example(span: String) { + print(span) + } + """, + expandedSource: """ + func example(span: String) { + withSpan("example") { _ in + print(span) + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_specifySpanBindingName_invalid() { + assertMacroExpansion( + """ + @Traced(span: 1) + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, + expandedSource: """ + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, + diagnostics: [ + .init(message: "span name must be a simple string literal", line: 1, column: 1) + ], + macros: ["Traced": TracedMacro.self] + ) + + assertMacroExpansion( + """ + @Traced(span: "invalid name") + func example(span: String) throws { + customSpan.attributes["span"] = span + } + + @Traced(span: "123") + func example2(span: String) throws { + customSpan.attributes["span"] = span + } + """, + expandedSource: """ + func example(span: String) throws { + customSpan.attributes["span"] = span + } + func example2(span: String) throws { + customSpan.attributes["span"] = span + } + """, + diagnostics: [ + .init(message: "'invalid name' is not a valid parameter name", line: 1, column: 1), + .init(message: "'123' is not a valid parameter name", line: 6, column: 1), + ], + macros: ["Traced": TracedMacro.self] + ) + + assertMacroExpansion( + """ + @Traced(span: "Hello \\(1)") + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, + expandedSource: """ + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, + diagnostics: [ + .init(message: "span name must be a simple string literal", line: 1, column: 1) + ], + macros: ["Traced": TracedMacro.self] + ) + } + + func test_tracedMacro_multipleMacroParameters() { + assertMacroExpansion( + """ + @Traced("custom span name", context: .topLevel, ofKind: .client, span: "customSpan") + func example(span: Int) { + customSpan.attributes["span"] = span + 1 + } + """, + expandedSource: """ + func example(span: Int) { + withSpan("custom span name", context: .topLevel, ofKind: .client) { customSpan in + customSpan.attributes["span"] = span + 1 + } + } + """, + macros: ["Traced": TracedMacro.self] + ) + } + + #endif + + func test_operationNameBehavior() { + XCTAssertEqual( + TracedOperationName._getOperationName("example custom", baseName: "test_operationNameBehavior"), + "example custom" + ) + XCTAssertEqual( + TracedOperationName._getOperationName(.string("example literal"), baseName: "test_operationNameBehavior"), + "example literal" + ) + XCTAssertEqual( + TracedOperationName._getOperationName(.baseName, baseName: "test_operationNameBehavior"), + "test_operationNameBehavior" + ) + XCTAssertEqual( + TracedOperationName._getOperationName(.fullName, baseName: "test_operationNameBehavior"), + "test_operationNameBehavior()" + ) + } +} + +#if compiler(>=6.0) + +// MARK: Compile tests + +@Traced +func syncNonthrowingExample(param: Int) { + print(param) +} + +@Traced +func syncThrowingExample(param: Int) throws { + struct ExampleError: Error {} + throw ExampleError() +} + +@Traced +func syncRethrowingExample(body: () throws -> Int) rethrows -> Int { + print("Starting") + let result = try body() + print("Ending") + return result +} + +@Traced +func asyncNonthrowingExample(param: Int) async { + print(param) +} + +@Traced +func asyncThrowingExample(param: Int) async throws { + try await Task.sleep(for: .seconds(1)) + print("Hello") +} + +@Traced +func asyncRethrowingExample(body: () async throws -> Int) async rethrows -> Int { + try? await Task.sleep(for: .seconds(1)) + let result = try await body() + span.attributes["result"] = result + return result +} + +@Traced +func example(param: Int) { + span.attributes["param"] = param +} + +let globalName = "example" + +@Traced(.string(globalName)) +func withDynamicOperationName(param: Int) { + span.attributes["param"] = param +} + +@Traced(.baseName) +func useBaseName(param: Int) { + span.attributes["param"] = param +} + +@Traced(.fullName) +func useFullName(param: Int) { + span.attributes["param"] = param +} + +@Traced("custom span name", context: .topLevel, ofKind: .client, span: "customSpan") +func exampleWithParams(span: Int) { + customSpan.attributes["span"] = span + 1 +} + +#endif