From 82ea68df40917a2de5190fc3548dca23b7b6c366 Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Mon, 18 Nov 2024 00:12:18 -0800 Subject: [PATCH 1/8] Add the TracingMacros module and the @Traced macro This is defined as a separate product so that users who don't want to pay the compile-time cost of macros don't have to use it, you opt-in to that cost by depending on the TracingMacros module. The @Traced macro is only available in Swift 6.0 compilers since function body macros were introduced in Swift 6.0. This adds minimum OS versions for Apple platforms in order to be able to depend on SwiftSyntax. This applies to the whole package since there are no per-target platform specifications. Most notably: This raises the macOS minimum deployment target from the (implicit) 10.13 to 10.15. --- Package.swift | 39 +++++++- Sources/TracingMacros/TracedMacro.swift | 25 +++++ .../TracedMacro.swift | 50 ++++++++++ Tests/TracingMacrosTests/TracedTests.swift | 91 +++++++++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 Sources/TracingMacros/TracedMacro.swift create mode 100644 Sources/TracingMacrosImplementation/TracedMacro.swift create mode 100644 Tests/TracingMacrosTests/TracedTests.swift diff --git a/Package.swift b/Package.swift index b1713cb0..0b267c38 100644 --- a/Package.swift +++ b/Package.swift @@ -1,14 +1,24 @@ // swift-tools-version:5.9 import PackageDescription +import CompilerPluginSupport let package = Package( name: "swift-distributed-tracing", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + ], 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,5 +53,32 @@ 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/TracedMacro.swift b/Sources/TracingMacros/TracedMacro.swift new file mode 100644 index 00000000..5a9f0ca4 --- /dev/null +++ b/Sources/TracingMacros/TracedMacro.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +#if compiler(>=6.0) +@attached(body) +public macro Traced( + _ operationName: String? = nil, + context: ServiceContext? = nil, + ofKind kind: SpanKind? = nil, + span spanName: String? = nil +) = #externalMacro(module: "TracingMacrosImplementation", type: "TracedMacro") +#endif diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift new file mode 100644 index 00000000..9d2e0310 --- /dev/null +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// 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") + } + + let operationName = StringLiteralExprSyntax(content: function.name.text) + let withSpanCall: ExprSyntax = "withSpan(\(operationName))" + let withSpanExpr: ExprSyntax = "\(withSpanCall) { span in \(body.statements) }" + + return ["\(withSpanExpr)"] + } +} +#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..f4b844a5 --- /dev/null +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +import SwiftSyntaxMacrosTestSupport + +import Tracing +import TracingMacros +import TracingMacrosImplementation + +#if compiler(>=6.0) + +final class TracedMacroTests: XCTestCase { + 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_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] + ) + } +} + +// MARK: Compile tests + +@Traced +func syncNonthrowingExample(param: Int) { + print(param) +} + +@Traced +func example(param: Int) { + span.attributes["param"] = param +} + +#endif From 50446c1de800e2546948f833c72495bc6ea2a337 Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Mon, 18 Nov 2024 11:05:15 -0800 Subject: [PATCH 2/8] Support async/throws functions in the @Traced macro Based on the effects signature of the attached function we apply try/await as appropriate. But, if the function is async/throws but those effects aren't actually used in the function body, this causes a new warning because the closure isn't inferred to be async and/or throws. To avoid those warnings, we also apply matching effects specifiers to the withSpan closure. --- .../TracedMacro.swift | 29 ++- Tests/TracingMacrosTests/TracedTests.swift | 173 ++++++++++++++++++ 2 files changed, 201 insertions(+), 1 deletion(-) diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift index 9d2e0310..fc46a87b 100644 --- a/Sources/TracingMacrosImplementation/TracedMacro.swift +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -29,9 +29,36 @@ public struct TracedMacro: BodyMacro { throw MacroExpansionErrorMessage("expected a function with a body") } + // Construct a withSpan call matching the invocation of the @Traced macro + let operationName = StringLiteralExprSyntax(content: function.name.text) let withSpanCall: ExprSyntax = "withSpan(\(operationName))" - let withSpanExpr: ExprSyntax = "\(withSpanCall) { span in \(body.statements) }" + + // 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) { span \(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)"] } diff --git a/Tests/TracingMacrosTests/TracedTests.swift b/Tests/TracingMacrosTests/TracedTests.swift index f4b844a5..1af1c23c 100644 --- a/Tests/TracingMacrosTests/TracedTests.swift +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -56,6 +56,146 @@ final class TracedMacroTests: XCTestCase { ) } + 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( """ @@ -83,6 +223,39 @@ 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 From 101df6623449c93316deee089defbe343feadf35 Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Mon, 18 Nov 2024 22:28:20 -0800 Subject: [PATCH 3/8] Add support for parameters passed to the @Traced macro First, allow overriding the regular parameters of the withSpan call: setting the operationName, the context, and the kind. If it's not specified, it's not passed to the function, which means the function remains the source of truth for default arguments. Also allow overriding the span name, which controls the variable binding in the closure body. This is primarily useful for avoiding shadowing an outer "span" variable. --- .../TracedMacro.swift | 77 ++++++- Tests/TracingMacrosTests/TracedTests.swift | 202 ++++++++++++++++++ 2 files changed, 276 insertions(+), 3 deletions(-) diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift index fc46a87b..b9970fa3 100644 --- a/Sources/TracingMacrosImplementation/TracedMacro.swift +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -30,9 +30,28 @@ public struct TracedMacro: BodyMacro { } // Construct a withSpan call matching the invocation of the @Traced macro + let (operationName, context, kind, spanName) = try extractArguments(from: node) - let operationName = StringLiteralExprSyntax(content: function.name.text) - let withSpanCall: ExprSyntax = "withSpan(\(operationName))" + var withSpanCall = FunctionCallExprSyntax("withSpan()" as ExprSyntax)! + withSpanCall.arguments.append(LabeledExprSyntax( + expression: operationName ?? ExprSyntax(StringLiteralExprSyntax(content: function.name.text)))) + 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. @@ -47,7 +66,7 @@ public struct TracedMacro: BodyMacro { throwsClause?.throwsSpecifier = .keyword(.throws) } var withSpanExpr: ExprSyntax = """ - \(withSpanCall) { span \(asyncClause)\(throwsClause)\(returnClause)in \(body.statements) } + \(withSpanCall) { \(spanIdentifier) \(asyncClause)\(throwsClause)\(returnClause)in \(body.statements) } """ // Apply a try / await as necessary to adapt the withSpan expression @@ -62,6 +81,58 @@ public struct TracedMacro: BodyMacro { 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 diff --git a/Tests/TracingMacrosTests/TracedTests.swift b/Tests/TracingMacrosTests/TracedTests.swift index 1af1c23c..3152d5fa 100644 --- a/Tests/TracingMacrosTests/TracedTests.swift +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -214,6 +214,203 @@ final class TracedMacroTests: XCTestCase { 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(globalName) + func example(param: Int) { + span.attributes["param"] = param + } + """, + expandedSource: """ + let globalName = "example" + func example(param: Int) { + withSpan(globalName) { 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] + ) + } } // MARK: Compile tests @@ -261,4 +458,9 @@ func example(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 From 2cbfb9f1128bfdf4c796065d2f2a0f7204b2ca28 Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Fri, 22 Nov 2024 14:03:06 -0800 Subject: [PATCH 4/8] fixup! Add support for parameters passed to the @Traced macro --- Sources/TracingMacrosImplementation/TracedMacro.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift index b9970fa3..bf184fce 100644 --- a/Sources/TracingMacrosImplementation/TracedMacro.swift +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -129,7 +129,7 @@ public struct TracedMacro: BodyMacro { operationName: operationName, context: context, kind: kind, - spanName: spanName, + spanName: spanName ) } From f00afb1ad27057b95c7f5b1f57397ce63d72356c Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Fri, 22 Nov 2024 14:12:04 -0800 Subject: [PATCH 5/8] [WIP] Fixup formatting. Will do commit-by-commit post-review --- Package.swift | 2 +- .../TracedMacro.swift | 30 ++- Tests/TracingMacrosTests/TracedTests.swift | 223 +++++++++--------- 3 files changed, 129 insertions(+), 126 deletions(-) diff --git a/Package.swift b/Package.swift index 0b267c38..189b1577 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,6 @@ // swift-tools-version:5.9 -import PackageDescription import CompilerPluginSupport +import PackageDescription let package = Package( name: "swift-distributed-tracing", diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift index bf184fce..2776c9eb 100644 --- a/Sources/TracingMacrosImplementation/TracedMacro.swift +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -24,7 +24,7 @@ public struct TracedMacro: BodyMacro { in context: some MacroExpansionContext ) throws -> [CodeBlockItemSyntax] { guard let function = declaration.as(FunctionDeclSyntax.self), - let body = function.body + let body = function.body else { throw MacroExpansionErrorMessage("expected a function with a body") } @@ -33,10 +33,14 @@ public struct TracedMacro: BodyMacro { let (operationName, context, kind, spanName) = try extractArguments(from: node) var withSpanCall = FunctionCallExprSyntax("withSpan()" as ExprSyntax)! - withSpanCall.arguments.append(LabeledExprSyntax( - expression: operationName ?? ExprSyntax(StringLiteralExprSyntax(content: function.name.text)))) + withSpanCall.arguments.append( + LabeledExprSyntax( + expression: operationName ?? ExprSyntax(StringLiteralExprSyntax(content: function.name.text)) + ) + ) func appendComma() { - withSpanCall.arguments[withSpanCall.arguments.index(before: withSpanCall.arguments.endIndex)].trailingComma = .commaToken() + withSpanCall.arguments[withSpanCall.arguments.index(before: withSpanCall.arguments.endIndex)] + .trailingComma = .commaToken() } if let context { appendComma() @@ -66,8 +70,8 @@ public struct TracedMacro: BodyMacro { throwsClause?.throwsSpecifier = .keyword(.throws) } var withSpanExpr: ExprSyntax = """ - \(withSpanCall) { \(spanIdentifier) \(asyncClause)\(throwsClause)\(returnClause)in \(body.statements) } - """ + \(withSpanCall) { \(spanIdentifier) \(asyncClause)\(throwsClause)\(returnClause)in \(body.statements) } + """ // Apply a try / await as necessary to adapt the withSpan expression @@ -111,9 +115,9 @@ public struct TracedMacro: BodyMacro { 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) + 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") } @@ -138,11 +142,11 @@ public struct TracedMacro: BodyMacro { @main struct TracingMacroPlugin: CompilerPlugin { -#if compiler(>=6.0) + #if compiler(>=6.0) let providingMacros: [Macro.Type] = [ - TracedMacro.self, + TracedMacro.self ] -#else + #else let providingMacros: [Macro.Type] = [] -#endif + #endif } diff --git a/Tests/TracingMacrosTests/TracedTests.swift b/Tests/TracingMacrosTests/TracedTests.swift index 3152d5fa..94bff6cb 100644 --- a/Tests/TracingMacrosTests/TracedTests.swift +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -1,3 +1,7 @@ +import SwiftSyntaxMacrosTestSupport +import Tracing +import TracingMacros +import TracingMacrosImplementation //===----------------------------------------------------------------------===// // // This source file is part of the Swift Distributed Tracing open source project @@ -12,11 +16,6 @@ // //===----------------------------------------------------------------------===// import XCTest -import SwiftSyntaxMacrosTestSupport - -import Tracing -import TracingMacros -import TracingMacrosImplementation #if compiler(>=6.0) @@ -28,10 +27,10 @@ final class TracedMacroTests: XCTestCase { func funcWithoutBody() """, expandedSource: """ - func funcWithoutBody() - """, + func funcWithoutBody() + """, diagnostics: [ - .init(message: "expected a function with a body", line: 1, column: 1), + .init(message: "expected a function with a body", line: 1, column: 1) ], macros: ["Traced": TracedMacro.self] ) @@ -46,12 +45,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func syncNonthrowingExample(param: Int) { - withSpan("syncNonthrowingExample") { span in - print(param) + func syncNonthrowingExample(param: Int) { + withSpan("syncNonthrowingExample") { span in + print(param) + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -67,14 +66,14 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func syncThrowingExample(param: Int) throws { - try withSpan("syncThrowingExample") { span throws in - struct ExampleError: Error { + func syncThrowingExample(param: Int) throws { + try withSpan("syncThrowingExample") { span throws in + struct ExampleError: Error { + } + throw ExampleError() } - throw ExampleError() } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -91,15 +90,15 @@ final class TracedMacroTests: XCTestCase { } """, 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 + 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] ) } @@ -113,12 +112,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func asyncNonthrowingExample(param: Int) async { - await withSpan("asyncNonthrowingExample") { span async in - print(param) + func asyncNonthrowingExample(param: Int) async { + await withSpan("asyncNonthrowingExample") { span async in + print(param) + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -133,13 +132,13 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func asyncThrowingExample(param: Int) async throws { - try await withSpan("asyncThrowingExample") { span async throws in - try await Task.sleep(for: .seconds(1)) - print("Hello") + 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] ) } @@ -156,15 +155,15 @@ final class TracedMacroTests: XCTestCase { } """, 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 + 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] ) } @@ -183,15 +182,15 @@ final class TracedMacroTests: XCTestCase { } """, 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 + 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] ) } @@ -205,12 +204,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(param: Int) { - withSpan("example") { span in - span.attributes["param"] = param + func example(param: Int) { + withSpan("example") { span in + span.attributes["param"] = param + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -224,12 +223,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(param: Int) { - withSpan("example but with a custom operationName") { span in - span.attributes["param"] = param + func example(param: Int) { + withSpan("example but with a custom operationName") { span in + span.attributes["param"] = param + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) @@ -243,13 +242,13 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - let globalName = "example" - func example(param: Int) { - withSpan(globalName) { span in - span.attributes["param"] = param + let globalName = "example" + func example(param: Int) { + withSpan(globalName) { span in + span.attributes["param"] = param + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -263,12 +262,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example() { - withSpan("example", context: .topLevel) { span in - print("Hello") + func example() { + withSpan("example", context: .topLevel) { span in + print("Hello") + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -282,12 +281,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example() { - withSpan("example", ofKind: .client) { span in - print("Hello") + func example() { + withSpan("example", ofKind: .client) { span in + print("Hello") + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -301,12 +300,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(span: String) throws { - try withSpan("example") { customSpan throws in - customSpan.attributes["span"] = span + func example(span: String) throws { + try withSpan("example") { customSpan throws in + customSpan.attributes["span"] = span + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) @@ -318,12 +317,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(span: String) { - withSpan("example") { _ in - print(span) + func example(span: String) { + withSpan("example") { _ in + print(span) + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } @@ -337,12 +336,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(span: String) throws { - customSpan.attributes["span"] = span - } - """, + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, diagnostics: [ - .init(message: "span name must be a simple string literal", line: 1, column: 1), + .init(message: "span name must be a simple string literal", line: 1, column: 1) ], macros: ["Traced": TracedMacro.self] ) @@ -360,13 +359,13 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(span: String) throws { - customSpan.attributes["span"] = span - } - func example2(span: String) throws { - customSpan.attributes["span"] = span - } - """, + 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), @@ -382,12 +381,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(span: String) throws { - customSpan.attributes["span"] = span - } - """, + func example(span: String) throws { + customSpan.attributes["span"] = span + } + """, diagnostics: [ - .init(message: "span name must be a simple string literal", line: 1, column: 1), + .init(message: "span name must be a simple string literal", line: 1, column: 1) ], macros: ["Traced": TracedMacro.self] ) @@ -402,12 +401,12 @@ final class TracedMacroTests: XCTestCase { } """, expandedSource: """ - func example(span: Int) { - withSpan("custom span name", context: .topLevel, ofKind: .client) { customSpan in - customSpan.attributes["span"] = span + 1 + func example(span: Int) { + withSpan("custom span name", context: .topLevel, ofKind: .client) { customSpan in + customSpan.attributes["span"] = span + 1 + } } - } - """, + """, macros: ["Traced": TracedMacro.self] ) } From 8ae71001d1847d45d169d3fd502644afbdea360f Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Fri, 22 Nov 2024 14:38:25 -0800 Subject: [PATCH 6/8] [WIP] Fix license comment ordering --- Tests/TracingMacrosTests/TracedTests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/TracingMacrosTests/TracedTests.swift b/Tests/TracingMacrosTests/TracedTests.swift index 94bff6cb..ab7396b0 100644 --- a/Tests/TracingMacrosTests/TracedTests.swift +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -1,7 +1,3 @@ -import SwiftSyntaxMacrosTestSupport -import Tracing -import TracingMacros -import TracingMacrosImplementation //===----------------------------------------------------------------------===// // // This source file is part of the Swift Distributed Tracing open source project @@ -15,6 +11,11 @@ import TracingMacrosImplementation // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// + +import SwiftSyntaxMacrosTestSupport +import Tracing +import TracingMacros +import TracingMacrosImplementation import XCTest #if compiler(>=6.0) From 24364caf8d78e5135393fd5f9b354c0f7315097e Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Fri, 22 Nov 2024 16:12:57 -0800 Subject: [PATCH 7/8] Add documentation for `@Traced` --- Sources/TracingMacros/TracedMacro.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/TracingMacros/TracedMacro.swift b/Sources/TracingMacros/TracedMacro.swift index 5a9f0ca4..29323ced 100644 --- a/Sources/TracingMacros/TracedMacro.swift +++ b/Sources/TracingMacros/TracedMacro.swift @@ -15,11 +15,26 @@ import Tracing #if compiler(>=6.0) +/// Instrument a function to place the entire body inside a span. +/// +/// This macro is equivalent to calling ``/Tracing/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 ``/Tracing/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. Defaults to the name of the function. +/// - context: The `ServiceContext` providing information on where to start the new ``/Tracing/Span``. +/// - kind: The ``/Tracing/SpanKind`` of the new ``/Tracing/Span``. +/// - spanName: The name of the span variable to introduce in the function. Pass `"_"` to omit it. @attached(body) public macro Traced( _ operationName: String? = nil, context: ServiceContext? = nil, ofKind kind: SpanKind? = nil, - span spanName: String? = nil + span spanName: String = "span" ) = #externalMacro(module: "TracingMacrosImplementation", type: "TracedMacro") #endif From 8b3cfd3248998d9bc10c8356e331e76b113d379c Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Fri, 22 Nov 2024 18:17:16 -0800 Subject: [PATCH 8/8] Make the `@Traced` macro take a TracedOperationName **Motivation:** We want to allow richer customization of the operation name in the `@Traced` macro, in particular the difference between the different ways you can view a function name. **Modifications:** - Add a TracedOperationName type that represents the kinds of operation names we want to support. - Change the `@Traced` macro interface and expansion to use the new operation name type. **Result:** Now you can write `@Traced(.baseName)`, `@Traced(.fullName)` as well as `@Traced("custom name here")`, giving more flexibility between types of operation names. --- .../Docs.docc/TracedOperationName.md | 38 +++++++++ Sources/TracingMacros/Docs.docc/index.md | 19 +++++ Sources/TracingMacros/TracedMacro.swift | 69 ++++++++++++++-- .../TracedMacro.swift | 17 ++-- Tests/TracingMacrosTests/TracedTests.swift | 81 ++++++++++++++++++- 5 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 Sources/TracingMacros/Docs.docc/TracedOperationName.md create mode 100644 Sources/TracingMacros/Docs.docc/index.md 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 index 29323ced..8df7dbaf 100644 --- a/Sources/TracingMacros/TracedMacro.swift +++ b/Sources/TracingMacros/TracedMacro.swift @@ -14,25 +14,82 @@ @_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 ``/Tracing/withSpan`` in the body, but saves an +/// 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 ``/Tracing/withSpan`` where applicable, +/// 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. Defaults to the name of the function. -/// - context: The `ServiceContext` providing information on where to start the new ``/Tracing/Span``. -/// - kind: The ``/Tracing/SpanKind`` of the new ``/Tracing/Span``. +/// - 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: String? = nil, + _ operationName: TracedOperationName = .baseName, context: ServiceContext? = nil, ofKind kind: SpanKind? = nil, span spanName: String = "span" diff --git a/Sources/TracingMacrosImplementation/TracedMacro.swift b/Sources/TracingMacrosImplementation/TracedMacro.swift index 2776c9eb..31c699f8 100644 --- a/Sources/TracingMacrosImplementation/TracedMacro.swift +++ b/Sources/TracingMacrosImplementation/TracedMacro.swift @@ -31,13 +31,20 @@ public struct TracedMacro: BodyMacro { // 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)! - withSpanCall.arguments.append( - LabeledExprSyntax( - expression: operationName ?? ExprSyntax(StringLiteralExprSyntax(content: function.name.text)) - ) - ) + 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() diff --git a/Tests/TracingMacrosTests/TracedTests.swift b/Tests/TracingMacrosTests/TracedTests.swift index ab7396b0..94cc3345 100644 --- a/Tests/TracingMacrosTests/TracedTests.swift +++ b/Tests/TracingMacrosTests/TracedTests.swift @@ -18,9 +18,8 @@ import TracingMacros import TracingMacrosImplementation import XCTest -#if compiler(>=6.0) - final class TracedMacroTests: XCTestCase { + #if compiler(>=6.0) func test_tracedMacro_requires_body() { assertMacroExpansion( """ @@ -237,7 +236,7 @@ final class TracedMacroTests: XCTestCase { """ let globalName = "example" - @Traced(globalName) + @Traced(.string(globalName)) func example(param: Int) { span.attributes["param"] = param } @@ -245,7 +244,41 @@ final class TracedMacroTests: XCTestCase { expandedSource: """ let globalName = "example" func example(param: Int) { - withSpan(globalName) { span in + 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 } } @@ -411,8 +444,31 @@ final class TracedMacroTests: XCTestCase { 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 @@ -458,6 +514,23 @@ 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