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

Add metadata attributes #59

Merged
merged 12 commits into from
Jan 31, 2023
Merged

Add metadata attributes #59

merged 12 commits into from
Jan 31, 2023

Conversation

joshcarp
Copy link
Contributor

@joshcarp joshcarp commented Jan 16, 2023

Adds metadata attributes to all types.
Metadata attributes are only defined in the spec for trace span attributes.
The ones defined are:

  • rpc.<protocol>.request.metadata.<normalised key>
  • rpc.<protocol>.response.metadata.<normalised key>
    Where;
  • The <normalised key> are specified via the WithTraceMetadataAttributes(requestKeys, responseKeys []string) Option.
  • protocol is one of grpc, grpc_web or buf_connect
  • "normalised" is <key> being the normalized gRPC Metadata key (lowercase, with - characters replaced by _), the value being the metadata values. as per the spec. This is implemented here
  • The value formatting is in the format rpc.request.metadata.my_custom_metadata_attribute=["1.2.3.4", "1.2.3.5"], which is implemented here

Closes: https://github.com/bufbuild/connect-opentelemetry-go/issues/52

@joshcarp joshcarp force-pushed the josh/metadata-attributes branch from e60f841 to ab9d879 Compare January 17, 2023 17:05
@joshcarp joshcarp force-pushed the josh/metadata-attributes branch from ab9d879 to 17a3e65 Compare January 17, 2023 19:53
attributes.go Outdated Show resolved Hide resolved
attributes.go Outdated Show resolved Hide resolved
@joshcarp joshcarp marked this pull request as ready for review January 17, 2023 20:00
Copy link
Member

@akshayjshah akshayjshah left a comment

Choose a reason for hiding this comment

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

Generally seems like a good idea, especially if the Java & JS otel-grpc integrations do this. A few suggestions:

  • Let's make sure we structure the options roughly the same as Java & JS. I'd expect both an allowlist and a denylist, but please do a little research and comment here with how the other implementations do it.
  • No matter how we address the previous point, we need space for the allow/deny/etc config to evolve without breaking back compat. Let's separate WithTraceRequestMetadata and WithTraceResponseMetadata and have each take a config struct (so we have space to add more config struct fields over time).
  • Please model the allowlists as sets instead of slices, so we have O(1) membership checks.

@akshayjshah
Copy link
Member

  • No matter how we address the previous point, we need space for the allow/deny/etc config to evolve without breaking back compat. Let's separate WithTraceRequestMetadata and WithTraceResponseMetadata and have each take a config struct (so we have space to add more config struct fields over time).

On further thought, mixing config structs and functional options seems bad. LMK what your research into the OTel-gRPC integrations in Java and JS turns up and we can decide how to proceed.

attributes.go Outdated Show resolved Hide resolved
@joshcarp
Copy link
Contributor Author

joshcarp commented Jan 18, 2023

java

@joshcarp
Copy link
Contributor Author

on grpc java it's done as follows:

  /** Sets which metadata request values should be captured as span attributes on client spans. */
  @CanIgnoreReturnValue
  public GrpcTelemetryBuilder setCapturedClientRequestMetadata(
      List<String> capturedClientRequestMetadata) {
    this.capturedClientRequestMetadata = capturedClientRequestMetadata;
    return this;
  }

  /** Sets which metadata request values should be captured as span attributes on server spans. */
  @CanIgnoreReturnValue
  public GrpcTelemetryBuilder setCapturedServerRequestMetadata(
      List<String> capturedServerRequestMetadata) {
    this.capturedServerRequestMetadata = capturedServerRequestMetadata;
    return this;
  }

and constructing this:

  @Override
  protected ManagedChannelBuilder<?> configureClient(ManagedChannelBuilder<?> client) {
    return client.intercept(
        GrpcTelemetry.builder(testing.getOpenTelemetry())
            .setCapturedClientRequestMetadata(
                Collections.singletonList(CLIENT_REQUEST_METADATA_KEY))
            .build()
            .newClientInterceptor());

getting the metadata:

  List<String> metadataValue(GrpcRequest request, String key) {
    if (request.getMetadata() == null) {
      return Collections.emptyList();
    }

    if (key == null || key.isEmpty()) {
      return Collections.emptyList();
    }

    Iterable<String> values =
        request.getMetadata().getAll(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));

    if (values == null) {
      return Collections.emptyList();
    }

    return StreamSupport.stream(values.spliterator(), false).collect(Collectors.toList());
  }

calling the metadata function:

    for (String key : capturedRequestMetadata) {
      List<String> value = getter.metadataValue(request, key);
      if (!value.isEmpty()) {
        attributes.put(requestAttributeKey(key), value);
      }
    }

This is basically the same way that this PR implements it:

  • slices as keys for allowed request and allowed response metadata
  • set them as a field on the interceptor
  • in the request if there are any matches between the allowlist then add them + format the keys/values

@joshcarp
Copy link
Contributor Author

joshcarp commented Jan 18, 2023

Please model the allowlists as sets instead of slices, so we have O(1) membership checks.

Question about this; there is never a case where a single lookup is done; in each request the keys of metadataReqKeys and metadataResKeys are checked against http.Header, because every key needs to be looked up it's the same as if it were a map.

The only benefit of having a map is that duplicate keys aren't logged, but that could be done in the option, keeping the option parameter a []string, then remove duplicates, then continue with a []string

EDIT: plus it also makes testing awkward because it's non deterministic to iterate through a map

@joshcarp joshcarp requested a review from akshayjshah January 19, 2023 21:52
attributes.go Outdated Show resolved Hide resolved
attributes.go Outdated Show resolved Hide resolved
interceptor.go Outdated
// metadata until the send function is called. This means that traces are created without the request
// metadata attributes, so samplers don't have access to metadata attributes in streaming clients.
span.SetAttributes(headerAttributes(protocol, requestKey, conn.RequestHeader(), i.config.metadataReqKeys)...)
span.SetAttributes(headerAttributes(protocol, responseKey, conn.ResponseHeader(), i.config.metadataResKeys)...)
Copy link
Member

Choose a reason for hiding this comment

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

Seems like we ought to move span creation to the first call to Send or the first call to Close, whichever happens first? That'll also make the RPC duration match network activity more closely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Completed through createSpanOnce that gets called in both close and send: 1f41e4c

option.go Show resolved Hide resolved
@joshcarp joshcarp requested a review from akshayjshah January 23, 2023 16:04
return &streamingClientInterceptor{
StreamingClientConn: conn,
onClose: func() {
createSpanOnce.Do(createSpan)
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't look to me like this is thread-safe. onClose is called from CloseResponse, which must be safe to call concurrently with Send. (This is documented on the StreamingClientConn interface.) Using a sync.Once guarantees that createSpan is only called once, but it does not guarantee that the span has actually been created - it's possible for a call to CloseResponse to be in the midst of creating the span while a concurrent call to Send skips over the Once and attempts to use the nonexistent span.

Let's pair program this afternoon and try to fix this up!

@joshcarp joshcarp merged commit 15b7c28 into main Jan 31, 2023
@joshcarp joshcarp deleted the josh/metadata-attributes branch January 31, 2023 21:58
akshayjshah pushed a commit that referenced this pull request Jul 26, 2023
Adds metadata attributes to all types. 
Metadata attributes are _only_ defined in the spec for trace span
attributes.
The ones defined are:
- `rpc.<protocol>.request.metadata.<normalised key>`
- `rpc.<protocol>.response.metadata.<normalised key>`
Where; 
- The `<normalised key>` are specified via the
`WithTraceMetadataAttributes(requestKeys, responseKeys []string)`
Option.
- protocol is one of `grpc`, `grpc_web` or `buf_connect` 
- "normalised" is `<key> being the normalized gRPC Metadata key
(lowercase, with - characters replaced by _), the value being the
metadata values.` as per the
[spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md#grpc-request-and-response-metadata).
This is implemented
[here](https://github.com/bufbuild/connect-opentelemetry-go/pull/59/files#r1072737068)
- The value formatting is in the format
`rpc.request.metadata.my_custom_metadata_attribute=["1.2.3.4",
"1.2.3.5"]`, which is implemented
[here](https://github.com/bufbuild/connect-opentelemetry-go/pull/59/files#r1072738606)

Closes: https://github.com/bufbuild/connect-opentelemetry-go/issues/52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

gRPC Request and Response Metadata
2 participants