Skip to content

Revive TracingMacros and @Traced #178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
32 changes: 31 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
// swift-tools-version:5.9
import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "swift-distributed-tracing",
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: [
// ==== --------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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"),
]
),
]
)

Expand Down
38 changes: 38 additions & 0 deletions Sources/TracingMacros/Docs.docc/TracedOperationName.md
Original file line number Diff line number Diff line change
@@ -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:)``
19 changes: 19 additions & 0 deletions Sources/TracingMacros/Docs.docc/index.md
Original file line number Diff line number Diff line change
@@ -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``

97 changes: 97 additions & 0 deletions Sources/TracingMacros/TracedMacro.swift
Original file line number Diff line number Diff line change
@@ -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
159 changes: 159 additions & 0 deletions Sources/TracingMacrosImplementation/TracedMacro.swift
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading