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 mandatory rpc and grpc tags for grpc integration #2620

Merged
merged 4 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ AllCops:
- 'lib/datadog/profiling/pprof/pprof_pb.rb'
- 'spec/**/**/interesting_backtrace_helper.rb' # This file needs quite a few bizarre code patterns by design
- 'vendor/bundle/**/*'
- 'spec/datadog/tracing/contrib/grpc/support/gen/**/*.rb' # Skip protoc autogenerated code
NewCops: disable # Don't allow new cops to be enabled implicitly.
SuggestExtensions: false # Stop pushing suggestions constantly.

Expand Down
14 changes: 14 additions & 0 deletions docs/DevelopmentGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,17 @@ Datadog.configure do |c|
}
end
```

### Generating GRPC proto stubs for tests

If you modify any of the `.proto` files under `./spec/datadog/tracing/contrib/grpc/support/proto` used for
testing the `grpc` integration, you'll need to regenerate the Ruby code by running:

```
$ docker run \
--platform linux/amd64 \
-v ${PWD}:/app \
-w /app \
ruby:latest \
./spec/datadog/tracing/contrib/grpc/support/gen_proto.sh
```
5 changes: 5 additions & 0 deletions lib/datadog/tracing/contrib/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ module RPC
TAG_SYSTEM = 'rpc.system'
TAG_SERVICE = 'rpc.service'
TAG_METHOD = 'rpc.method'

module GRPC
TAG_STATUS_CODE = 'rpc.grpc.status_code'
TAG_FULL_METHOD = 'rpc.grpc.full_method'
end
end

module Messaging
Expand Down
39 changes: 22 additions & 17 deletions lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative '../../analytics'
require_relative '../ext'
require_relative '../../ext'
require_relative '../formatting'

module Datadog
module Tracing
Expand All @@ -16,30 +17,39 @@ module DatadogInterceptor
# sending the request to the server.
class Client < Base
def trace(keywords)
keywords[:metadata] ||= {}
formatter = GRPC::Formatting::FullMethodStringFormatter.new(keywords[:method])

options = {
span_type: Tracing::Metadata::Ext::HTTP::TYPE_OUTBOUND,
service: service_name, # Maintain client-side service name configuration
resource: format_resource(keywords[:method])
resource: formatter.resource_name
}

Tracing.trace(Ext::SPAN_CLIENT, **options) do |span, trace|
annotate!(trace, span, keywords[:metadata], keywords[:call])

yield
annotate!(trace, span, keywords, formatter)

begin
yield
rescue StandardError => e
code = e.is_a?(::GRPC::BadStatus) ? e.code : ::GRPC::Core::StatusCodes::UNKNOWN
span.set_tag(Contrib::Ext::RPC::GRPC::TAG_STATUS_CODE, code)

raise
else
span.set_tag(Contrib::Ext::RPC::GRPC::TAG_STATUS_CODE, ::GRPC::Core::StatusCodes::OK)
end
end
end

private

def annotate!(trace, span, metadata, call)
span.set_tags(metadata)
def annotate!(trace, span, keywords, formatter)
metadata = keywords[:metadata] || {}
call = keywords[:call]

span.set_tag(Contrib::Ext::RPC::TAG_SYSTEM, Ext::TAG_SYSTEM)
span.set_tags(metadata)
Copy link
Member

Choose a reason for hiding this comment

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

Is this setting all gRPC metadata key/value pairs as span tags?
Isn't metadata arbitrary user data, meaning it could have PII?
If so, we can't simply tag it like this. Is this a requirement from some internal doc?

Also, the keys will become Datadog tag names, we should make sure they don't conflict our tag namespace, likely by namespacing them to metadata.METADATA_KEY = METADATA_VALUE.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @marcotc 👋 !
This code was already here before my changes (I just moved the lines around)
I think the concerns that you raise make sense but I think they would be out of scope of this PR. If you wanna discuss this offline please let me know!


span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_CLIENT)

span.set_tag(Tracing::Metadata::Ext::TAG_COMPONENT, Ext::TAG_COMPONENT)
span.set_tag(Tracing::Metadata::Ext::TAG_OPERATION, Ext::TAG_OPERATION_CLIENT)

Expand All @@ -48,6 +58,9 @@ def annotate!(trace, span, metadata, call)
span.set_tag(Tracing::Metadata::Ext::TAG_PEER_SERVICE, span.service)
end

span.set_tag(Contrib::Ext::RPC::TAG_SYSTEM, Ext::TAG_SYSTEM)
span.set_tag(Contrib::Ext::RPC::GRPC::TAG_FULL_METHOD, formatter.grpc_full_method)

host, _port = find_host_port(call)
span.set_tag(Tracing::Metadata::Ext::TAG_PEER_HOSTNAME, host) if host

Expand All @@ -62,14 +75,6 @@ def annotate!(trace, span, metadata, call)
Datadog.logger.debug("GRPC client trace failed: #{e}")
end

def format_resource(proto_method)
proto_method
.downcase
.split('/')
.reject(&:empty?)
.join('.')
end

def find_deadline(call)
return unless call.respond_to?(:deadline) && call.deadline.is_a?(Time)

Expand Down
41 changes: 21 additions & 20 deletions lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative '../../analytics'
require_relative '../ext'
require_relative '../../ext'
require_relative '../formatting'

module Datadog
module Tracing
Expand All @@ -17,25 +18,31 @@ module DatadogInterceptor
# its tracing context with a parent client-side context
class Server < Base
def trace(keywords)
method = keywords[:method]
formatter = GRPC::Formatting::MethodObjectFormatter.new(keywords[:method])

options = {
span_type: Tracing::Metadata::Ext::HTTP::TYPE_INBOUND,
service: service_name, # TODO: Remove server-side service name configuration
resource: format_resource(method),
resource: formatter.resource_name,
on_error: error_handler
}
metadata = keywords[:call].metadata

set_distributed_context!(metadata)

Tracing.trace(Ext::SPAN_SERVICE, **options) do |span|
span.set_tag(Contrib::Ext::RPC::TAG_SYSTEM, Ext::TAG_SYSTEM)
span.set_tag(Contrib::Ext::RPC::TAG_SERVICE, method.owner.to_s)
span.set_tag(Contrib::Ext::RPC::TAG_METHOD, method.name)
annotate!(span, metadata, formatter)

annotate!(span, metadata)
begin
yield
rescue StandardError => e
code = e.is_a?(::GRPC::BadStatus) ? e.code : ::GRPC::Core::StatusCodes::UNKNOWN
span.set_tag(Contrib::Ext::RPC::GRPC::TAG_STATUS_CODE, code)

yield
raise
else
span.set_tag(Contrib::Ext::RPC::GRPC::TAG_STATUS_CODE, ::GRPC::Core::StatusCodes::OK)
end
end
end

Expand All @@ -49,7 +56,7 @@ def set_distributed_context!(metadata)
)
end

def annotate!(span, metadata)
def annotate!(span, metadata, formatter)
metadata.each do |header, value|
# Datadog propagation headers are considered internal implementation detail.
next if header.to_s.start_with?(Tracing::Distributed::Datadog::TAGS_PREFIX)
Expand All @@ -58,27 +65,21 @@ def annotate!(span, metadata)
end

span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_SERVER)

span.set_tag(Tracing::Metadata::Ext::TAG_COMPONENT, Ext::TAG_COMPONENT)
span.set_tag(Tracing::Metadata::Ext::TAG_OPERATION, Ext::TAG_OPERATION_SERVICE)

span.set_tag(Contrib::Ext::RPC::TAG_SYSTEM, Ext::TAG_SYSTEM)
span.set_tag(Contrib::Ext::RPC::TAG_SERVICE, formatter.legacy_grpc_service)
span.set_tag(Contrib::Ext::RPC::TAG_METHOD, formatter.legacy_grpc_method)
span.set_tag(Contrib::Ext::RPC::GRPC::TAG_FULL_METHOD, formatter.grpc_full_method)

# Set analytics sample rate
Contrib::Analytics.set_sample_rate(span, analytics_sample_rate) if analytics_enabled?

# Measure service stats
Contrib::Analytics.set_measured(span)
rescue StandardError => e
Datadog.logger.debug("GRPC client trace failed: #{e}")
end

def format_resource(proto_method)
proto_method
.owner
.to_s
.downcase
.split('::')
.<<(proto_method.name)
.join('.')
Datadog.logger.debug("GRPC server trace failed: #{e}")
end
end
end
Expand Down
114 changes: 114 additions & 0 deletions lib/datadog/tracing/contrib/grpc/formatting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# frozen_string_literal: true

module Datadog
module Tracing
module Contrib
module GRPC
module Formatting
VALUE_UNKNOWN = 'unknown'

# A class to extract GRPC span attributes from the GRPC implementing class method object.
class MethodObjectFormatter
# grpc_full_method is a string containing all the rpc method information (from the Protobuf definition)
# in a single string with the following format: /$package.$service/$method
attr_reader :grpc_full_method

# legacy_grpc_service is built using the Ruby GRPC service implementation package and class name instead
# of the rpc interface representation from Protobuf. It's kept for compatibility.
attr_reader :legacy_grpc_service

# legacy_grpc_method is built using the Ruby GRPC service implementation method name instead of the rpc
# interface representation from Protobuf. It's kept for compatibility.
attr_reader :legacy_grpc_method

# resource_name is used for the span resource name.
attr_reader :resource_name

def initialize(grpc_method_object)
@grpc_full_method = format_full_method(grpc_method_object)
@resource_name = format_resource_name(grpc_method_object)
@legacy_grpc_method = extract_legacy_grpc_method(grpc_method_object)
@legacy_grpc_service = extract_legacy_grpc_service(grpc_method_object)
end

private

def format_full_method(grpc_method_object)
service = extract_grpc_service(grpc_method_object)
method = extract_grpc_method(grpc_method_object)
"/#{service}/#{method}"
end

def extract_grpc_service(grpc_method_object)
owner = grpc_method_object.owner
return VALUE_UNKNOWN unless owner.instance_variable_defined?(:@service_name)

# Ruby protoc generated code includes this variable which directly contains the value from the original
# protobuf definition
owner.service_name.to_s
end

# extract_grpc_method attempts to find the original method name from the Protobuf file definition,
# since grpc gem forces the implementation method name to be in snake_case.
def extract_grpc_method(grpc_method_object)
owner = grpc_method_object.owner

return VALUE_UNKNOWN unless owner.instance_variable_defined?(:@rpc_descs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to invoke instance_variable_defined? instead of leveraging public method rpc_descs ?

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 added this check because for the case of the previous grpc Ruby class (manual) implementation this was not defined, and this is added by the protoc Ruby generated one.

Even though I just checked the grpc gem fails to start if you provide a class without this or this having no items.

Would you prefer to remove this check? (in that case I think we probably need to update some unit tests that are relying on a non-autogenerated grpc test service class if I recall correctly)


method, = owner.rpc_descs.find do |k, _|
Copy link
Contributor

Choose a reason for hiding this comment

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

Array#find would be iterating through the entire collection until the result is found. Do you know how large would the collection be? Is there better way to get the value?

Copy link
Contributor Author

@rarguelloF rarguelloF Apr 4, 2023

Choose a reason for hiding this comment

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

Do you know how large would the collection be?

This collection would contain all the methods (or rpcs in gRPC terminology) in that service. I can imagine most services having a magnitude of 10s or at most 100s (even though I already consider this case being weird). I guess potentially it could be anything but it doesn't seem a realistic scenario to me.

Is there better way to get the value?

Let me try to give a little bit of extra context:

  • Assume you create an RPC service MyService with a method named MyMethod in your original .proto file - please note protobuf doesn't enforce any specific syntax, you could do my_method, MyMethod, myMethod, and so on.
  • What we have grpc_method_object.name.to_s is always the method name with underscore syntax: my_method, regardless of what you did in the previous step. This is enforced by the protoc autogenerated RPC service Ruby class (if you name it myMethod or MyMethod` you will get an exception).
  • In this section of code, you don't know what was the original choice in step 1, but you have the underscore version my_method. So if you loop over all the available original method names for the service (inside owner.rpc_descs) and transform them to underscore using ::GRPC::GenericService.underscore and matches my_method, then you know that is method you are looking for.

So given this situation, I don't think there's an alternative to this, but if you think there's one please let me know 🙏

::GRPC::GenericService.underscore(k.to_s) == grpc_method_object.name.to_s
end

return VALUE_UNKNOWN if method.nil?

method.to_s
end

def extract_legacy_grpc_service(grpc_method_object)
grpc_method_object.owner.to_s
end

def extract_legacy_grpc_method(grpc_method_object)
grpc_method_object.name
end

def format_resource_name(grpc_method_object)
grpc_method_object
.owner
.to_s
.downcase
.split('::')
.<<(grpc_method_object.name)
.join('.')
end
end

# A class to extract GRPC span attributes from the full method string.
class FullMethodStringFormatter
# grpc_full_method is a string containing all the rpc method information (from the Protobuf definition)
# in a single string with the following format: /$package.$service/$method
attr_reader :grpc_full_method

# resource_name is used for the span resource name.
attr_reader :resource_name

def initialize(grpc_full_method)
@grpc_full_method = grpc_full_method
@resource_name = format_resource_name(grpc_full_method)
end

private

def format_resource_name(grpc_full_method)
grpc_full_method
.downcase
.split('/')
.reject(&:empty?)
.join('.')
end
end
end
end
end
end
end
42 changes: 42 additions & 0 deletions sig/datadog/tracing/contrib/grpc/formatting.rbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Datadog
module Tracing
module Contrib
module GRPC
module Formatting
VALUE_UNKNOWN: "unknown"
class MethodObjectFormatter
attr_reader grpc_full_method: String
attr_reader legacy_grpc_service: String
attr_reader legacy_grpc_method: String
attr_reader resource_name: String

def initialize: (untyped grpc_method_object) -> void

private

def format_full_method: (untyped grpc_method_object) -> ::String

def extract_grpc_service: (untyped grpc_method_object) -> String
def extract_grpc_method: (untyped grpc_method_object) -> String

def extract_legacy_grpc_service: (untyped grpc_method_object) -> String

def extract_legacy_grpc_method: (untyped grpc_method_object) -> String

def format_resource_name: (untyped grpc_method_object) -> String
end
class FullMethodStringFormatter
attr_reader grpc_full_method: String
attr_reader resource_name: String

def initialize: (String grpc_full_method) -> void

private

def format_resource_name: (String grpc_full_method) -> String
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
expect(span).to have_error_message('test error')
expect(span).to have_error_type('TestError')
expect(span).to have_error_stack(include('server_spec.rb'))
expect(span.get_tag('rpc.system')).to eq 'grpc'
expect(span.get_tag('rpc.system')).to eq('grpc')
expect(span.get_tag('span.kind')).to eq('server')
end
end
Expand Down
Loading