Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/unit-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
- name: Resolve Swift dependencies
run: swift package resolve
- name: Run Unit Tests
run: swift test --parallel
run: swift test --enable-all-traits
4 changes: 3 additions & 1 deletion Generator/Sources/FileRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ extension FileRenderer {
switch deprecated {
case let .renamed(renamed_to, note):
if let renamedTo = try? attributeIDToSwiftMemberPath(renamed_to) {
result.append(", renamed: \"\(renamedTo)\"")
// Swift disallows backticks in the `renamed` value
let sanitizedRenamedTo = renamedTo.replacingOccurrences(of: "`", with: "")
result.append(", renamed: \"\(sanitizedRenamedTo)\"")
}
if let note = note?.trimmingCharacters(in: .whitespacesAndNewlines) {
result.append(", message: \"\(note)\"")
Expand Down
26 changes: 23 additions & 3 deletions Generator/Sources/FileRenderers/OTelAttributeRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ struct OTelAttributeRenderer: FileRenderer {
func renderFile(_ namespace: Namespace) throws -> String {
try """
extension OTelAttribute {
\(renderNamespace(namespace, indent: 4, doccSymbolPrefix: ["OTelAttribute"]))
\(renderNamespace(namespace, parent: nil, indent: 4, doccSymbolPrefix: ["OTelAttribute"]))
}

"""
}

private func renderNamespace(
_ namespace: Namespace,
parent: Namespace?,
indent: Int,
doccSymbolPrefix: [String]
) throws -> String {
Expand All @@ -52,16 +53,27 @@ struct OTelAttributeRenderer: FileRenderer {
}.map { child in
try renderNamespace(
child,
parent: namespace,
indent: 4,
doccSymbolPrefix: doccSymbolPrefix
)
}
)

var result = "/// `\(namespace.id)` namespace"
let parentMarkedExperimental = parent?.containsNoStableAttributes ?? false
let shouldMarkExperimental = !parentMarkedExperimental && namespace.containsNoStableAttributes

var result = ""
if shouldMarkExperimental {
result.append("#if Experimental\n")
}
result.append("/// `\(namespace.id)` namespace")
result.append("\npublic enum \(namespace.memberName) {")
result.append("\n" + properties.joined(separator: "\n\n"))
result.append("\n}")
if shouldMarkExperimental {
result.append("\n#endif")
}
return result.indent(by: indent)
}

Expand All @@ -71,7 +83,12 @@ struct OTelAttributeRenderer: FileRenderer {
indent: Int,
doccSymbolPrefix: [String]
) throws -> String {
var result = renderDocs(attribute)
let shouldMarkExperimental = !namespace.containsNoStableAttributes && attribute.stability != .stable
var result = ""
if shouldMarkExperimental {
result.append("#if Experimental\n")
}
result.append(renderDocs(attribute))
if let deprecated = attribute.deprecated {
result.append("\n" + renderDeprecatedAttribute(deprecated))
}
Expand All @@ -85,6 +102,9 @@ struct OTelAttributeRenderer: FileRenderer {
result.append(
"\npublic static let \(attributeMemberName) = \"\(attribute.id)\""
)
if shouldMarkExperimental {
result.append("\n#endif")
}

return result.indent(by: indent)
}
Expand Down
52 changes: 39 additions & 13 deletions Generator/Sources/FileRenderers/SpanAttributeRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct SpanAttributeRenderer: FileRenderer {
import Tracing

extension SpanAttributes {
\(renderNamespace(namespace, indent: 4, doccSymbolPrefix: ["Tracing", "SpanAttributes"]))
\(renderNamespace(namespace, parent: nil, indent: 4, doccSymbolPrefix: ["Tracing", "SpanAttributes"]))
}

#endif
Expand Down Expand Up @@ -57,7 +57,7 @@ struct SpanAttributeRenderer: FileRenderer {

private func renderNamespace(
_ namespace: Namespace,
inSpanNamespace: Bool = false,
parent: Namespace?,
indent: Int,
doccSymbolPrefix: [String]
) throws -> String {
Expand All @@ -67,24 +67,31 @@ struct SpanAttributeRenderer: FileRenderer {
let standardAttributes = namespace.attributes.values.filter { !isTemplateType($0.type) }
let templateAttributes = namespace.attributes.values.filter { isTemplateType($0.type) }

var result = "/// `\(namespace.id)` namespace"
let parentMarkedExperimental = parent?.containsNoStableAttributes ?? false
let shouldMarkExperimental = !parentMarkedExperimental && namespace.containsNoStableAttributes

var result = ""
if shouldMarkExperimental {
result.append("#if Experimental\n")
}
result.append("/// `\(namespace.id)` namespace")
result.append(
"""

public var \(propertyName): \(structName) {
get {
.init(attributes: \(inSpanNamespace ? "self.attributes" : "self"))
.init(attributes: \(parent == nil ? "self" : "self.attributes"))
}
set {
\(inSpanNamespace ? "self.attributes" : "self") = newValue.attributes
\(parent == nil ? "self" : "self.attributes") = newValue.attributes
}
}

@dynamicMemberLookup
public struct \(structName): SpanAttributeNamespace {
public var attributes: SpanAttributes
public var attributes: Tracing.SpanAttributes

public init(attributes: SpanAttributes) {
public init(attributes: Tracing.SpanAttributes) {
self.attributes = attributes
}
"""
Expand Down Expand Up @@ -137,14 +144,17 @@ struct SpanAttributeRenderer: FileRenderer {
}.map { child in
try renderNamespace(
child,
inSpanNamespace: true,
parent: namespace,
indent: 4,
doccSymbolPrefix: doccSymbolPrefix + [structName]
)
}.joined(separator: "\n\n")
)
}
result.append("\n}")
if shouldMarkExperimental {
result.append("\n#endif")
}
return result.indent(by: indent)
}

Expand All @@ -163,7 +173,13 @@ struct SpanAttributeRenderer: FileRenderer {
.replacingOccurrences(of: "`", with: "")
context.doccSymbolReferences[attribute.id, default: [:]]["Span Attributes"] = symbolReference

var result = renderDocs(attribute)
let shouldMarkExperimental = !namespace.containsNoStableAttributes && attribute.stability != .stable

var result = ""
if shouldMarkExperimental {
result.append("#if Experimental\n")
}
result.append(renderDocs(attribute))
if let deprecated = attribute.deprecated {
result.append("\n" + renderDeprecatedAttribute(deprecated))
}
Expand Down Expand Up @@ -232,6 +248,9 @@ struct SpanAttributeRenderer: FileRenderer {
} else {
throw SpanAttributeRendererError.invalidStandardAttributeType(attribute.type)
}
if shouldMarkExperimental {
result.append("\n#endif")
}

return result.indent(by: indent)
}
Expand Down Expand Up @@ -269,9 +288,13 @@ struct SpanAttributeRenderer: FileRenderer {
throw SpanAttributeRendererError.invalidTemplateAttributeType(attribute.type)
}

// getTemplateType
let shouldMarkExperimental = !namespace.containsNoStableAttributes && attribute.stability != .stable

var result = renderDocs(attribute)
var result = ""
if shouldMarkExperimental {
result.append("#if Experimental\n")
}
result.append(renderDocs(attribute))
result.append(
"""

Expand All @@ -285,9 +308,9 @@ struct SpanAttributeRenderer: FileRenderer {
}

public struct \(structName) {
public var attributes: SpanAttributes
public var attributes: Tracing.SpanAttributes

public init(attributes: SpanAttributes) {
public init(attributes: Tracing.SpanAttributes) {
self.attributes = attributes
}

Expand All @@ -314,6 +337,9 @@ struct SpanAttributeRenderer: FileRenderer {
}
"""
)
if shouldMarkExperimental {
result.append("\n#endif")
}
return result.indent(by: indent)
}

Expand Down
14 changes: 9 additions & 5 deletions Generator/Sources/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,6 @@ struct Generator: AsyncParsableCommand {
semConvModelsDirectory: semConvModelsDirectory
)

// Filter out attributes that are not stable
parsedAttributes = parsedAttributes.filter { id, attribute in
attribute.stability == .stable
}

// Create semconv namespace tree
let namespaceTree = try constructNamespaceTree(attributes: parsedAttributes)

Expand Down Expand Up @@ -373,6 +368,15 @@ class Namespace {
var memberName: String {
nameGenerator.swiftMemberName(for: name)
}

lazy var containsNoStableAttributes: Bool = {
attributes.allSatisfy {
$0.1.stability != .stable
}
&& subNamespaces.values.allSatisfy {
$0.containsNoStableAttributes
}
}()
}

let generatedFileHeader = """
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ let package = Package(
],
traits: [
.trait(name: "Tracing"),
.default(enabledTraits: ["Tracing"]),
.trait(name: "Experimental"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created the experimental trait as discussed in #27, but what do you think about using "Unstable" instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll get to a detailed review later. One thing I thought about is that just having the trait without additional markers on the unstable attributes may be a bit too little because you don't immediately see whether you'd even need the trait based on the attributes used. One option I thought about is to prefix them all with experimental/unstable, so it's immediately obvious from the call-side. What do you think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that. Plus we should add a clear note in the Readme explaining that if you use the experimental trait, you must use .upToNextMinor as they might break between minor versions.

Copy link
Contributor Author

@NeedleInAJayStack NeedleInAJayStack Oct 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, definitely a good concern. The only thing I'd want to be careful of is to abide by the Development section of the stability guide, in particular:

OpenTelemetry clients MUST NOT be designed in a manner that breaks existing users when a signal transitions from Development to Stable. This would punish users of the release candidate, and hinder adoption.

I believe that this would disallow any symbol-prefixing or namespacing (since abc becoming stable would transition the symbol from experimentalAbc -> abc, and break existing users).

Two approaches that come to mind and are compatible with the spec:

  1. Using @available attributes to provide compiler warnings on non-stable attributes. This may be too intrusive, as some projects consider any compiler warnings to be a code smell.
  2. Adjusting documentation to more obviously flag non-stable attributes as mentioned in the issue. This may not be intrusive enough, as a user might not view the docs before using.

What do you guys think?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe doc comments are enough, combined with the package trait.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks for linking to the spec. I agree that it makes sense to follow it here. As you suggested option 1 with @available checks seems too intrusive because it would basically "punish" users of the unstable API always, not just when upgrading to a release that graduated some of the unstable attributes.

So overall I agree that the best option is to make it very clear in the documentation of individual attributes and in the trait documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I've added a big, bolded **UNSTABLE** to the beginning of each attribute documentation here: aadf494

.default(enabledTraits: [
"Tracing"
]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0")
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ logger[metadataKey: .init(name: OTelAttribute.http.request.method)] = "POST"
logger[metadataKey: .init(name: OTelAttribute.http.response.statusCode)] = "200"
```

### Unstable Instrumentations

[Unstable](https://opentelemetry.io/docs/specs/otel/versioning-and-stability/#semantic-conventions-stability) attributes are available, but are gated behind a non-default `Experimental` trait. To use them, you must explicitly include the this trait in your `Package.swift`.

**Be aware that unstable attributes may experience breaking changes on minor version updates of this package, so use with caution!** If this breakage is unacceptable, but non-standard tags are still required, a version dependency range that only allows patch updates should be used.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add an explicit instruction to use .upToNextMinor, so that at least we promise not to break them between patch versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought - I was going to do that, but it looks like .upToNextMinor is deprecated, so I went with explaining the equivalent range restriction. Is that right that it's deprecated?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right one, on Range, not deprecated https://developer.apple.com/documentation/swift/range/uptonextminor(from:)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh awesome, thanks for linking that! I've adjusted the documentation in 30eb17f to recommend that upToNextMinor is used.


## Contributing

### Generation
Expand Down
Loading