Skip to content
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

Conform client's tracing interceptor to OTel's conventions #25

Merged
merged 16 commits into from
Jan 23, 2025

Conversation

gjcairo
Copy link
Collaborator

@gjcairo gjcairo commented Jan 16, 2025

This PR conforms the client tracing interceptor to follow recommendations/conventions laid out in both:

@gjcairo gjcairo added the 🆕 semver/minor Adds new public API. label Jan 16, 2025
@gjcairo gjcairo requested a review from glbrntt January 16, 2025 16:55
@gjcairo gjcairo added ⚠️ semver/major Breaks existing public API. and removed 🆕 semver/minor Adds new public API. labels Jan 16, 2025
@gjcairo
Copy link
Collaborator Author

gjcairo commented Jan 16, 2025

This API break is expected, as the interceptor has been renamed:

💔 API breakage: struct ClientTracingInterceptor has been removed

@gjcairo gjcairo changed the title Conform tracing interceptor to OTel's conventions Conform client's tracing interceptor to OTel's conventions Jan 17, 2025
@gjcairo gjcairo force-pushed the otel-client-interceptor branch from 1d1f018 to 2929500 Compare January 17, 2025 17:57
@gjcairo gjcairo force-pushed the otel-client-interceptor branch from 48530eb to dd7bc23 Compare January 20, 2025 16:14
@gjcairo gjcairo requested a review from glbrntt January 20, 2025 18:02
Sources/GRPCInterceptors/HookedAsyncSequence.swift Outdated Show resolved Hide resolved
Sources/GRPCInterceptors/HookedAsyncSequence.swift Outdated Show resolved Hide resolved
Sources/GRPCInterceptors/HookedAsyncSequence.swift Outdated Show resolved Hide resolved
Comment on lines 94 to 97
case .other(let address):
// We can't nicely format the span attributes to contain the appropriate information here,
// so include the whole thing as part of the server address.
self.attributes.rpc.serverAddress = address
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure about this. We set serverAddress above to serverHostname which is a value explicitly passed in by the user so I don't think we should be overriding it here.

I think my lean is to ignore the remote peer if we can't parse it. (Meaning PeerAddress should have a failable init rather than other.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually this is a mistake - I think we should be setting network.peer.address. I agree the server address should stay as the hostname. The port is only required if the transport supports it, which we don't know at this stage so we can just leave it blank.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This one didn't get fixed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I've changed it to set "network.peer.address" instead

self.attributes.rpc.serverPort = port

case .unixDomainSocket(let path):
self.attributes.rpc.networkType = "unix"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looking at the docs https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/, it seems like network type should be either "ipv4"/"ipv6" and that "unix" should be a transport.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, so should we leave the network type empty when using UDS?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also unsure about setting the network transport here, since we're getting it as an argument when initialising the interceptor.

Copy link
Collaborator

Choose a reason for hiding this comment

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

network.type has the following list of well-known values. If one of them applies, then the respective value MUST be used; otherwise, a custom value MAY be used.

We don't have a well-known value here (they list "ipv4" and "ipv6") only. We could use a custom value but not sure that would actually add useful information (given we'd set "network.transport" to "unix") so I think we shouldn't set it.

Copy link
Collaborator Author

@gjcairo gjcairo Jan 21, 2025

Choose a reason for hiding this comment

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

Also unsure about setting the network transport here, since we're getting it as an argument when initialising the interceptor.

What about this? IMO, we shouldn't set it, as it's the same argument as we don't want to override the server address; we don't want to override the network transport either.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, forgot to reply to that one. I agree we shouldn't set it; let's leave that one up to the user.

@gjcairo gjcairo requested a review from glbrntt January 21, 2025 11:12
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

I think a few things got missed but this is definitely much closer to where it needs to be!

Tests/GRPCInterceptorsTests/PeerAddressTests.swift Outdated Show resolved Hide resolved
self.attributes.rpc.serverPort = port

case .unixDomainSocket(let path):
self.attributes.rpc.networkType = "unix"
Copy link
Collaborator

Choose a reason for hiding this comment

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

network.type has the following list of well-known values. If one of them applies, then the respective value MUST be used; otherwise, a custom value MAY be used.

We don't have a well-known value here (they list "ipv4" and "ipv6") only. We could use a custom value but not sure that would actually add useful information (given we'd set "network.transport" to "unix") so I think we shouldn't set it.

Comment on lines 94 to 97
case .other(let address):
// We can't nicely format the span attributes to contain the appropriate information here,
// so include the whole thing as part of the server address.
self.attributes.rpc.serverAddress = address
Copy link
Collaborator

Choose a reason for hiding this comment

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

This one didn't get fixed.

Comment on lines 101 to 106
guard addressComponents.count == 3, let port = Int(addressComponents[2]) else {
// This is some unexpected/unknown format, so we have no way of splitting it up nicely.
self = .other(address)
return
}
self = .ipv4(address: String(addressComponents[1]), port: port)
Copy link
Collaborator

Choose a reason for hiding this comment

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

A previous comment got missed: #25 (comment)

Copy link
Collaborator Author

@gjcairo gjcairo Jan 21, 2025

Choose a reason for hiding this comment

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

This is also fixed with the changes I brought from the server PR

Comment on lines 40 to 44
@Test("Unrecognised addresses are correctly parsed")
func testOther() {
let address = PeerAddress("in-process:1234")
#expect(address == .other("in-process:1234"))
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm still not sure whether we should include addresses we can't parse. I think my lean is to not include them because we're then more likely to catch parsing bugs (I think a user is more likely to report an issue if the information is missing rather than not quite appearing as it should).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, I'm not sure about this. I feel like silently skipping information is worse than showing it in a not-ideal way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I agree: we don't know what the value is so we should treat it with a bit of suspicion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't feel that strongly about this so I don't mind not setting it, but what is the worst that could happen if we did set it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can think of a few things:

  • If, for some reason, the string is very dynamic (i.e. changes for each RPC) then we could pollute the users telemetry collection service with thousands of potentially useless values.
  • A very long could cause issues if the collector doesn't handle long strings well.

In practice these both seem pretty unlikely, but they are possible.

IMO the more compelling reason to not set it is that users are more likely to complain if the value is missing and then we can figure out why parsing failed so we can set it correctly.

I also find it weird that we can return .other when we know it's ipv4/ipv6 but we can't parse it, because it doesn't meet our expectations of what that address type should be.

@gjcairo gjcairo requested a review from glbrntt January 21, 2025 19:59
@gjcairo gjcairo force-pushed the otel-client-interceptor branch from 41aa5fe to 3275f72 Compare January 21, 2025 20:00
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

This basically looks good; just the one nit around "[" and resolving how to handle that.

// At this point, we are looking at an address with format: [<address>]:<port>
// We drop the first character ('[') and split by ']:' to keep two components: the address
// and the port.
let ipv6AddressComponents = addressComponents[1].dropFirst().split(separator: "]:")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should be a bit stricter here and check that we actually have a "[".

We also have plenty of allocations here which are mostly avoidable (which I'm fine to punt on for now), but could you file an issue? The idea would be to avoid split and all the extra String conversions by using the UTF8 view.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#30

Comment on lines 40 to 44
@Test("Unrecognised addresses are correctly parsed")
func testOther() {
let address = PeerAddress("in-process:1234")
#expect(address == .other("in-process:1234"))
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I can think of a few things:

  • If, for some reason, the string is very dynamic (i.e. changes for each RPC) then we could pollute the users telemetry collection service with thousands of potentially useless values.
  • A very long could cause issues if the collector doesn't handle long strings well.

In practice these both seem pretty unlikely, but they are possible.

IMO the more compelling reason to not set it is that users are more likely to complain if the value is missing and then we can figure out why parsing failed so we can set it correctly.

I also find it weird that we can return .other when we know it's ipv4/ipv6 but we can't parse it, because it doesn't meet our expectations of what that address type should be.

@gjcairo gjcairo requested a review from glbrntt January 22, 2025 16:37
@glbrntt glbrntt enabled auto-merge (squash) January 23, 2025 13:36
@glbrntt glbrntt disabled auto-merge January 23, 2025 13:45
@glbrntt glbrntt merged commit ee4936a into grpc:main Jan 23, 2025
20 of 21 checks passed
@gjcairo gjcairo deleted the otel-client-interceptor branch January 23, 2025 13:46
glbrntt added a commit that referenced this pull request Jan 24, 2025
This PR brings the same changes made to the client in
#25 to the server
interceptor, conforming it to follow recommendations/conventions laid
out in both:
- https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans
- https://opentelemetry.io/docs/specs/semconv/rpc/grpc/

---------

Co-authored-by: George Barnett <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⚠️ semver/major Breaks existing public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants