Skip to content

Add simple method tracing API#5294

Merged
lloeki merged 35 commits into
masterfrom
lloeki/method-tracing-api
Feb 6, 2026
Merged

Add simple method tracing API#5294
lloeki merged 35 commits into
masterfrom
lloeki/method-tracing-api

Conversation

@lloeki

@lloeki lloeki commented Jan 30, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Add a simple method tracing API

Motivation:

A long overdue and oft-requested feature #1197

Change log entry

Add simple method tracing API

Additional Notes:

  • This uses Graft
  • The intent is to bootstrap usage and kickstart implementation of an instrumentation API

Related existing work:

Things that these language define or allow setting:

  • resource name (.Net, Java, Python, Node, PHP)
  • (operation) name (.Net, Java, Python (as name), Node (as name), PHP (as name))
  • no parent (Java) (creates new trace)
  • service (Python, Node, PHP)
  • tags (PHP)
  • span type (Python, Node, PHP)

How to test the change?

  • CI
  • play with it

@github-actions

github-actions Bot commented Jan 30, 2026

Copy link
Copy Markdown

👋 Hey @lloeki, please fill "Change log entry" section in the pull request description.

If changes need to be present in CHANGELOG.md you can state it this way

**Change log entry**

Yes. A brief summary to be placed into the CHANGELOG.md

(possible answers Yes/Yep/Yeah)

Or you can opt out like that

**Change log entry**

None.

(possible answers No/Nope/None)

Visited at: 2026-02-03 11:52:41 UTC

@lloeki
lloeki force-pushed the lloeki/method-tracing-api branch 3 times, most recently from 9e88d86 to e51e2d4 Compare January 30, 2026 16:38
@pr-commenter

pr-commenter Bot commented Jan 30, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-02-06 10:42:46

Comparing candidate commit 47df115 in PR branch lloeki/method-tracing-api with baseline commit e18737e in branch master.

Found 1 performance improvements and 0 performance regressions! Performance is the same for 43 metrics, 2 unstable metrics.

scenario:tracing - Propagation - Datadog

  • 🟩 throughput [+2594.668op/s; +2664.584op/s] or [+8.972%; +9.213%]

@lloeki
lloeki force-pushed the lloeki/method-tracing-api branch 2 times, most recently from 4ff2738 to e3b58b9 Compare February 2, 2026 14:59
Comment thread lib/datadog/kit/tracing/method_tracer.rb
Comment thread lib/datadog/kit/tracing/method_tracer.rb Outdated
Comment thread lib/datadog/kit/tracing/method_tracer.rb
Comment thread lib/datadog/kit/tracing/method_tracer.rb Outdated
@lloeki

lloeki commented Feb 2, 2026

Copy link
Copy Markdown
Member Author

Strange CI failures, apparently unrelated, like:

Datadog::Core::Metrics::Client
  is expected to have attributes {statsd: #<Double "statsd">}
  #count
    when #statsd is nil
      is expected not to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 0 times
    when #statsd is a Datadog::Statsd
      which raises an error
        behaves like logs an error without raising
          is expected not to raise Exception
      and given a block
        that yields args
          is expected to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
        that does not yield args
          behaves like logs an error without raising
            is expected not to raise Exception
      and given no options
        is expected to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
      and given options
        that are empty
          is expected to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
        that contain :tags
          is expected to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0", "foo", "bar"]}) 1 time
          which are frozen
            is expected to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0", "foo", "bar"]}) 1 time
        that are frozen
          is expected to have received count(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
  #time
    when #statsd is a Datadog::Statsd
      and given no options
        is expected to have received distribution(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
      and given a block
        is expected to yield control
        which raises an error
          is expected to raise #<RuntimeError: RuntimeError>
      which raises an error
        is expected not to raise Exception
      and given options
        that are frozen
          is expected to have received distribution(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
        that contain :tags
          is expected to have received distribution(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0", "foo", "bar"]}) 1 time
          which are frozen
            is expected to have received distribution(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0", "foo", "bar"]}) 1 time
        that are empty
          is expected to have received distribution(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 1 time
    when #statsd is nil
      is expected not to have received distribution(:foo, kind of Numeric, {tags: ["language:ruby", "language-interpreter:ruby-x86_64-linux", "language-version:4.0.1", "tracer-version:2.28.0"]}) 0 times
  #configure
    given options including
      :statsd
        is expected to change `metrics.statsd` from #<Double "statsd"> to #<InstanceDouble(Datadog::Statsd) (anonymous)>
      :enabled
        as true
          is expected to equal true
        as false
          is expected to equal false
  #default_statsd_client
    is expected to equal #<InstanceDouble(Datadog::Statsd) (anonymous)> (FAILED - 116)
    with Datadog::Statsd not loaded
      loads Datadog::Statsd library (FAILED - 117)
  #send_stats?
    when disabled
      example at ./spec/datadog/core/metrics/client_spec.rb:352 (FAILED - 118)
    when enabled
      and Statsd
        is initialized
          example at ./spec/datadog/core/metrics/client_spec.rb:362 (FAILED - 119)
        is nil
          example at ./spec/datadog/core/metrics/client_spec.rb:368 (FAILED - 120)
  #gauge
    when #statsd is nil
      example at ./spec/datadog/core/metrics/client_spec.rb:543 (FAILED - 121)
    when #statsd is a Datadog::Statsd
      which raises an error
        behaves like logs an error without raising
          example at ./spec/datadog/core/metrics/client_spec.rb:19 (FAILED - 122)
      and given no options
        example at ./spec/datadog/core/metrics/client_spec.rb:568 (FAILED - 123)
      and given a block
        that yields args
          example at ./spec/datadog/core/metrics/client_spec.rb:561 (FAILED - 124)
        that does not yield args
          behaves like logs an error without raising
            example at ./spec/datadog/core/metrics/client_spec.rb:19 (FAILED - 125)
      and given options
        that contain :tags
          example at ./spec/datadog/core/metrics/client_spec.rb:590 (FAILED - 126)
          which are frozen
            example at ./spec/datadog/core/metrics/client_spec.rb:595 (FAILED - 127)
        that are empty
          example at ./spec/datadog/core/metrics/client_spec.rb:577 (FAILED - 128)
        that are frozen
          example at ./spec/datadog/core/metrics/client_spec.rb:583 (FAILED - 129)

and summaries:

  1046) Tracer integration tests rule sampler with rule matching span with matching tags behaves like flushed trace 
        Got 2 failures:
        Shared Example Group: "flushed trace" called from ./spec/datadog/tracing/integration_spec.rb:353

        1046.1) Failure/Error: unused_statsd.each(&:close)
                  #<InstanceDouble(Datadog::Statsd) (anonymous)> was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.

@lloeki
lloeki force-pushed the lloeki/method-tracing-api branch 2 times, most recently from 3ec50d5 to cdafd12 Compare February 3, 2026 10:29
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage
Patch Coverage: 98.40%
Overall Coverage: 95.17% (+0.00%)

View detailed report

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 47df115 | Docs | Datadog PR Page | Was this helpful? Give us feedback!

@lloeki
lloeki marked this pull request as ready for review February 3, 2026 12:06
@lloeki
lloeki requested a review from a team as a code owner February 3, 2026 12:06
@lloeki

lloeki commented Feb 3, 2026

Copy link
Copy Markdown
Member Author

Steep hits a:

2026-02-03 11:10:05.984: FATAL: [Steep 1.10.0] [typecheck:typecheck@1] [background] [#typecheck_source(path=lib/datadog/core/logger.rb)] [#type_check_file(lib/datadog/core/logger.rb@datadog)] [synthesize:(3:1)] [synthesize:(5:1)] [synthesize:(6:3)] [synthesize:(11:5)] [synthesize:(12:7)] [synthesize:(20:7)] [synthesize:(21:9)] [synthesize:(29:9)] [synthesize:(30:11)] [synthesize:(31:13)]
Unexpected error: #<NoMethodError: undefined method `selector' for an instance of Parser::Source::Map::Keyword>

EDIT: soutaro/steep#2015

@Strech Strech left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it looks simple and effective 👏🏼

Some ideas:

  1. Improve slightly documentation in the comments (like YARD for arguments)
  2. Maybe some areas of wrong-usage should be covered in tests
  3. If possible avoid using eval, but only if it doesn't make it much more complicated

P.S it's a non-blocking suggestions

Comment thread lib/datadog/kit/tracing/method_tracer.rb Outdated
Comment thread lib/datadog/kit/tracing/method_tracer.rb
Comment thread lib/datadog/kit/tracing/method_tracer.rb
Comment thread lib/datadog/kit/tracing/method_tracer.rb
Comment thread lib/datadog/kit/tracing/method_tracer.rb Outdated
Comment thread lib/datadog/kit/tracing/method_tracer.rb Outdated
@p-datadog

Copy link
Copy Markdown
Member

I support method_missing handlers in DI instrumentation. This PR does not explicitly give reasons for rejecting them. I think this is a common use case and if it's not supported probably the reason should be written down for the customers who attempt it.

@lloeki

lloeki commented Feb 4, 2026

Copy link
Copy Markdown
Member Author

I support method_missing handlers in DI instrumentation. This PR does not explicitly give reasons for rejecting them. I think this is a common use case and if it's not supported probably the reason should be written down for the customers who attempt it.

@p-datadog I agree this is interesting to support.

The reason why it wasn't is that the initial use case described was this DSL:

class Foo
  extend Datadog::Kit::Tracing::MethodTracer

  def foo = 42
  trace_method :foo
end

A corresponding usage with method_missing would be:

class Foo
  extend Datadog::Kit::Tracing::MethodTracer

  def method_missing(meth, *args, &block)
    case meth
    when :foo then 42
    else super
    end
  end

  trace_method :foo
  trace_method :bar
end

Since these methods are dynamic (virtual? is that a better term?), they "exist" only at runtime, and can only be checked for via respond_to_missing?, so even then they can't be checked for before an instance exists.

Therefore they can't be checked for existence at the class level, which is where the DSL operates.

With the current implementation that enforces the check, both trace_method :foo (handled by method_missing) and trace_method :bar (entirely unhandled) would raise an exception.

I am considering an implementation where the user can simply relax that existence (+visibility) check, since the check simply can't be done at the class level. Visibility doesn't matter anyway as method_missing is for public methods: one could theoretically implement private- an protected-like virtual methods but they would really need to go out of their way examining the caller bindings and raising appropriately but I feel that's overkill to address). In that case the hook module's super will simply forward the message up the ancestor chain where the method does not exist and so fall back to invoke method_missing.

With that check relaxed, both trace_method :foo (handled by method_missing) and trace_method :bar (entirely unhandled) would stop raising an exception.

A side effect is that a bar method would start existing, and when called, traced, but then raise the standard NoMethodError exception. The behaviour is thus correct, although a) there's an extra span being generated and b) respond_to?(:bar) is true. But then there's also user error at the start, it's just that we can't really guard against it; at least the span error field will contain the exact location of the error and help pinpoint the problem.

An alternative considered was to hook on method_missing instead and check for the meth value, but then things like ActiveRecord's magic that dynamically defines methods upon first invocation of method_missing would make it stop working since subsequent invocations would be on a defined method (and thus forever skip our method_missing).

So... the other reason why is that it's more complex to handle and I focused on laying down the basics ;)

@lloeki

lloeki commented Feb 4, 2026

Copy link
Copy Markdown
Member Author

Dug into steep with:

$ find lib -name '*.rb' | while read -r f; do echo -n "#### $f"; out=$(steep check "$f" 2>&1); rc=$?; if [[ $rc -ne 0 || "$out" == *FATAL* ]]; then echo -e "\rFAIL $f"; echo "$f" >> steep.failed; else echo -e "\r  OK $f"; fi; done
-----8<----
$ cat steep.failed 
lib/datadog/kit/appsec/events/v2.rb
lib/datadog/tracing/contrib/rack/route_inference.rb
lib/datadog/core/logger.rb

Steep failures:

$ steep check lib/datadog/core/logger.rb; echo "=> exit: $?"
2026-02-04 15:29:23.361: FATAL: [Steep 1.10.0] [typecheck:typecheck@6] [background] [#typecheck_source(path=lib/datadog/core/logger.rb)] [#type_check_file(lib/datadog/core/logger.rb@datadog)] [synthesize:(3:1)] [synthesize:(5:1)] [synthesize:(6:3)] [synthesize:(11:5)] [synthesize:(12:7)] [synthesize:(20:7)] [synthesize:(21:9)] [synthesize:(29:9)] [synthesize:(30:11)] [synthesize:(31:13)]
Unexpected error: #<NoMethodError: undefined method 'selector' for an instance of Parser::Source::Map::Keyword>
-----8<----
=> exit: 0
$ steep check lib/datadog/tracing/contrib/rack/route_inference.rb; echo "=> exit: $?"
2026-02-04 15:27:49.863: FATAL: [Steep 1.10.0] [typecheck:typecheck@6] [background] [#typecheck_source(path=lib/datadog/tracing/contrib/rack/route_inference.rb)] [#type_check_file(lib/datadog/tracing/contrib/rack/route_inference.rb@datadog)] [synthesize:(3:1)] [synthesize:(4:3)] [synthesize:(5:5)] [synthesize:(6:7)] [synthesize:(9:9)] [synthesize:(10:11)] [synthesize:(27:11)] [synthesize:(28:13)] [synthesize:(28:13)] [synthesize:(30:13)]
Unexpected error: #<ArgumentError: wrong number of arguments (given 0, expected 3)>
-----8<----
=> exit: 0
$ steep check lib/datadog/kit/appsec/events/v2.rb; echo "=> exit: $?"
2026-02-04 15:31:18.321: FATAL: [Steep 1.10.0] [typecheck:typecheck@7] [background] [#typecheck_source(path=lib/datadog/kit/appsec/events/v2.rb)] [#type_check_file(lib/datadog/kit/appsec/events/v2.rb@datadog)] [synthesize:(3:1)] [synthesize:(6:1)] [synthesize:(7:3)] [synthesize:(8:5)] [synthesize:(9:7)] [synthesize:(11:9)] [synthesize:(12:11)] [synthesize:(22:11)] [synthesize:(44:13)] [synthesize:(44:13)] [synthesize:(45:15)] [synthesize:(48:15)] [synthesize:(48:18)] [synthesize:(48:18)]
Unexpected error: #<RuntimeError: Unknown name for build_instance: Tracing::TraceSegment>
-----8<----
=> exit: 1

The latter has these type failures:

lib/datadog/kit/appsec/events/v2.rb:133:14: [error] The method cannot return a value of type `(::Datadog::Tracing::TraceSegment | nil)` because declared as type `(Tracing::TraceSegment | Tracing::TraceOperation | nil)`
│   (::Datadog::Tracing::TraceSegment | nil) <: (Tracing::TraceSegment | Tracing::TraceOperation | nil)
│     ::Datadog::Tracing::TraceSegment <: (Tracing::TraceSegment | Tracing::TraceOperation | nil)
│       ::Datadog::Tracing::TraceSegment <: Tracing::TraceSegment
│         ::Object <: Tracing::TraceSegment
│           ::BasicObject <: Tracing::TraceSegment
│
│ Diagnostic ID: Ruby::ReturnTypeMismatch
│
└               return Datadog::Tracing.active_trace unless Datadog::AppSec.active_context
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

lib/datadog/kit/appsec/events/v2.rb:132:16: [error] Cannot allow method body have type `(::Datadog::Tracing::TraceOperation | nil)` because declared as type `(Tracing::TraceSegment | Tracing::TraceOperation | nil)`
│   (::Datadog::Tracing::TraceOperation | nil) <: (Tracing::TraceSegment | Tracing::TraceOperation | nil)
│     ::Datadog::Tracing::TraceOperation <: (Tracing::TraceSegment | Tracing::TraceOperation | nil)
│       ::Datadog::Tracing::TraceOperation <: Tracing::TraceSegment
│         ::Object <: Tracing::TraceSegment
│           ::BasicObject <: Tracing::TraceSegment
│
│ Diagnostic ID: Ruby::MethodBodyTypeMismatch
│
└             def service_entry_trace
                  ~~~~~~~~~~~~~~~~~~~

lib/datadog/kit/appsec/events/v2.rb:141:14: [error] The method cannot return a value of type `(::Datadog::Tracing::SpanOperation | nil)` because declared as type `(Tracing::SpanOperation | nil)`
│   (::Datadog::Tracing::SpanOperation | nil) <: (Tracing::SpanOperation | nil)
│     ::Datadog::Tracing::SpanOperation <: (Tracing::SpanOperation | nil)
│       ::Datadog::Tracing::SpanOperation <: Tracing::SpanOperation
│         ::Object <: Tracing::SpanOperation
│           ::BasicObject <: Tracing::SpanOperation
│
│ Diagnostic ID: Ruby::ReturnTypeMismatch
│
└               return Datadog::Tracing.active_span unless Datadog::AppSec.active_context
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

lib/datadog/kit/appsec/events/v2.rb:140:16: [error] Cannot allow method body have type `(::Datadog::Tracing::SpanOperation | nil)` because declared as type `(Tracing::SpanOperation | nil)`
│   (::Datadog::Tracing::SpanOperation | nil) <: (Tracing::SpanOperation | nil)
│     ::Datadog::Tracing::SpanOperation <: (Tracing::SpanOperation | nil)
│       ::Datadog::Tracing::SpanOperation <: Tracing::SpanOperation
│         ::Object <: Tracing::SpanOperation
│           ::BasicObject <: Tracing::SpanOperation
│
│ Diagnostic ID: Ruby::MethodBodyTypeMismatch
│
└             def service_entry_span
                  ~~~~~~~~~~~~~~~~~~

Detected 4 problems from 1 file

@lloeki
lloeki requested a review from a team as a code owner February 4, 2026 14:55
@github-actions

github-actions Bot commented Feb 4, 2026

Copy link
Copy Markdown

Typing analysis

Ignored files

This PR introduces 4 ignored files. It decreases the percentage of typed files from 42.11% to 41.73% (-0.38%).

Ignored files (+4-0)Introduced:
lib/datadog/open_feature/resolution_details.rb
lib/datadog/kit/appsec/events/v2.rb
lib/datadog/core/logger.rb
lib/datadog/tracing/contrib/rack/route_inference.rb

Note: Ignored files are excluded from the next sections.

Untyped methods

This PR clears 2 untyped methods and 4 partially typed methods. It increases the percentage of typed methods from 59.57% to 59.65% (+0.08%).

Untyped methods (+0-2)Cleared:
sig/datadog/core/logger.rbs:6
└── def initialize: (*untyped args) ?{ () -> untyped } -> void
sig/datadog/core/logger.rbs:8
└── def add: (untyped severity, ?untyped? message, ?untyped? progname) { () -> untyped } -> untyped
Partially typed methods (+0-4)Cleared:
sig/datadog/kit/appsec/events/v2.rbs:37
└── def self.set_span_tags: (Tracing::SpanOperation span, ::Hash[::Symbol, untyped] tags, namespace: ::String) -> void
sig/datadog/open_feature/resolution_details.rbs:24
└── def self.new: (
        ?value: untyped,
        ?reason: ::String?,
        ?variant: ::String?,
        ?error_code: ::String?,
        ?error_message: ::String?,
        ?flag_metadata: ::Hash[::String, untyped]?,
        ?allocation_key: ::String?,
        ?extra_logging: ::Hash[::String, untyped]?,
        ?log?: bool?,
        ?error?: bool?
      ) -> instance
sig/datadog/open_feature/resolution_details.rbs:37
└── def self.build_error: (
        value: untyped,
        error_code: ::String,
        error_message: ::String,
        ?reason: ::String
      ) -> instance
sig/datadog/tracing/contrib/rack/route_inference.rbs:16
└── def self?.read_or_infer: (Hash[::String, untyped] request_env) -> ::String?

Untyped other declarations

This PR clears 1 untyped other declaration and 2 partially typed other declarations. It decreases the percentage of typed other declarations from 76.42% to 76.22% (-0.2%).

Untyped other declarations (+0-1)Cleared:
sig/datadog/open_feature/resolution_details.rbs:4
└── attr_accessor value: untyped
Partially typed other declarations (+0-2)Cleared:
sig/datadog/open_feature/resolution_details.rbs:14
└── attr_accessor flag_metadata: ::Hash[::String, untyped]?
sig/datadog/open_feature/resolution_details.rbs:18
└── attr_accessor extra_logging: ::Hash[::String, untyped]?

@lloeki

lloeki commented Feb 4, 2026

Copy link
Copy Markdown
Member Author
$ steep check lib/datadog/core/logger.rb
Unexpected error: #<NoMethodError: undefined method 'selector' for an instance of Parser::Source::Map::Keyword>

Traced down to this line

      class InsufficientPositionalArguments < Base
        attr_reader :node
        attr_reader :params

        def initialize(node:, params:)
          send = case node.type
                 when :send, :csend
                   node
                 when :block, :numblock
                   node.children[0]
                 end

          loc = if send
                  send.loc.selector.with(end_pos: send.loc.expression.end_pos) # steep:ignore NoMethod
                else
                  node.loc.expression
                end

          super(node: node, location: loc)
          @params = params
        end

        def header_line
          "More positional arguments are required"
        end
      end

node:

s(:block,
  s(:zsuper),
  s(:args),
  s(:dstr,
    s(:str, "["),
    s(:begin,
      s(:send,
        s(:self), :progname)),
    s(:str, "] "),
    s(:begin,
      s(:lvar, :where)),
    s(:begin,
      s(:yield))))

params:

#<Steep::Interface::Function::Params:0x0000000129f13f18 @positional_params=#<Steep::Interface::Function::Params::PositionalParams:0x0000000129f13f68 @head=#<Steep::Interface::Function::Params::PositionalParams::Required:0x0000000129f13f90 @type=#<Steep::AST::Types::Any:0x0000000125554e90 @fvs=Set[]>>, @tail=#<Steep::Interface::Function::Params::PositionalParams:0x0000000129f14008 @head=#<Steep::Interface::Function::Params::PositionalParams::Optional:0x0000000129f14030 @type=#<Steep::AST::Types::Any:0x0000000125554e90 @fvs=Set[]>>, @tail=#<Steep::Interface::Function::Params::PositionalParams:0x0000000129f14058 @head=#<Steep::Interface::Function::Params::PositionalParams::Optional:0x0000000129f14080 @type=#<Steep::AST::Types::Any:0x0000000125554e90 @fvs=Set[]>>, @tail=nil>>>, @keyword_params=#<Steep::Interface::Function::Params::KeywordParams:0x0000000129f13f40 @requireds={}, @optionals={}, @rest=nil>>

send:

#<Parser::Source::Map::Keyword:0x0000000129a53668 @keyword=#<Parser::Source::Range lib/datadog/core/logger.rb 853...858>, @begin=nil, @end=nil, @expression=#<Parser::Source::Range lib/datadog/core/logger.rb 853...858>, @node=s(:zsuper)>

So the problem is InsufficientPositionalArguments on a :zsuper.

Lots of super in there, but only one without positional args... and voilà:

diff --git a/lib/datadog/core/logger.rb b/lib/datadog/core/logger.rb
index c551660f98..14b2b704ad 100644
--- a/lib/datadog/core/logger.rb
+++ b/lib/datadog/core/logger.rb
@@ -28,7 +28,7 @@ module Datadog
 
         if message.nil?
           if block
-            super do
+            super(severity) do
               "[#{self.progname}] #{where}#{yield}"
             end
           else

Will report details properly upstream.

@lloeki
lloeki force-pushed the lloeki/method-tracing-api branch from 658bf11 to 71268e6 Compare February 5, 2026 12:27
@greghuels
greghuels removed their request for review February 5, 2026 12:53

@mabdinur mabdinur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

left some questions, overall LGTM thanks for picking this up 😄

Comment thread lib/datadog/kit/tracing/method_tracer.rb
Comment thread lib/datadog/kit/tracing/method_tracer.rb

@p-datadog p-datadog left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I approve based on my previous comments.

@lloeki
lloeki requested a review from marcotc February 5, 2026 21:31
@lloeki
lloeki merged commit e3b6fb5 into master Feb 6, 2026
618 checks passed
@lloeki
lloeki deleted the lloeki/method-tracing-api branch February 6, 2026 11:36
@github-actions github-actions Bot added this to the 2.29.0 milestone Feb 6, 2026
#
# def foo; 'hello'; end
#
# trace_method :foo, span_name: 'optional_span_name'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It would be nice to also see an example of how to add tracing to a class method.

@lloeki lloeki Feb 9, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair point.

Given that:

  • "class methods" don't exist! They are merely methods of instances of the class Class
  • but we wouldn't want a given method to be hooked for every single class!

... one would straight up hook onto that instance's singleton, i.e MyClass.singleton_class:

class MyClass
  class << self
    def foo; 'hello'; end
  end
end

Datadog::Kit::Tracing::MethodTracer.trace_method(MyClass.singleton_class, :foo, span_name: 'MyClass.foo')

Alternatively, this works too:

class MyClass
  class << self
    extend Datadog::Kit::Tracing::MethodTracer

    def foo; 'hello'; end
    trace_method :foo, span_name: 'MyClass.foo'
  end
end

Since this singleton class is a dynamic class, span_name must be provided.

A convenience DSL-ish class-context trace_class_method (or maybe better: trace_singleton_class_method could be added:

def trace_singleton_class_method(method_name, span_name: nil, dynamic: false)
  span_name ||= "#{name}.#{method_name}" if respond_to?(:name)

  Datadog::Kit::Tracing::MethodTracer.trace_method(self.singleton_class, :foo, span_name: span_name, dynamic: false)

I will open opened an issue to track that.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A convenience DSL-ish class-context trace_class_method (or maybe better: trace_singleton_class_method could be added

👍🏼 Yes, I was looking for something along those lines. (It also matches the API that we suggested way back in #670).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Involves Datadog core libraries integrations Involves tracing integrations tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants