Skip to content

Commit e0c1a24

Browse files
authored
Merge pull request #4858 from DataDog/ivoanjo/libdatadog-ddsketch
[NO-TICKET] RFC: Bindings for libdatadog ddsketch API
2 parents 97c91f0 + 8ae9432 commit e0c1a24

File tree

11 files changed

+435
-56
lines changed

11 files changed

+435
-56
lines changed

Matrixfile

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,12 @@
1111
'custom_cop' => {
1212
'' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby'
1313
},
14-
'crashtracking' => {
14+
'core_with_libdatadog_api' => {
1515
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby',
1616
},
1717
'error_tracking' => {
1818
'' => '❌ 2.5 / ❌ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby',
1919
},
20-
'process_discovery' => {
21-
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby'
22-
},
23-
'stable_config' => {
24-
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ❌ jruby'
25-
},
2620
'appsec:main' => {
2721
'' => '✅ 2.5 / ✅ 2.6 / ✅ 2.7 / ✅ 3.0 / ✅ 3.1 / ✅ 3.2 / ✅ 3.3 / ✅ 3.4 / ✅ 3.5 / ✅ jruby'
2822
},

Rakefile

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ Dir.glob('tasks/*.rake').each { |r| import r }
2222

2323
TEST_METADATA = eval(File.read('Matrixfile')).freeze # rubocop:disable Security/Eval
2424

25+
CORE_WITH_LIBDATADOG_API = [
26+
'spec/datadog/core/crashtracking/**/*_spec.rb',
27+
'spec/datadog/core/process_discovery_spec.rb',
28+
'spec/datadog/core/configuration/stable_config_spec.rb',
29+
'spec/datadog/core/ddsketch_spec.rb',
30+
].freeze
31+
2532
# rubocop:disable Metrics/BlockLength
2633
namespace :test do
2734
desc 'Run all tests'
@@ -70,13 +77,14 @@ namespace :spec do
7077
:graphql, :graphql_unified_trace_patcher, :graphql_trace_patcher, :graphql_tracing_patcher,
7178
:rails, :railsredis, :railsredis_activesupport, :railsactivejob,
7279
:elasticsearch, :http, :redis, :sidekiq, :sinatra, :hanami, :hanami_autoinstrument,
73-
:profiling, :crashtracking, :error_tracking, :process_discovery, :stable_config]
80+
:profiling, :core_with_libdatadog_api, :error_tracking]
7481

7582
desc '' # "Explicitly hiding from `rake -T`"
7683
RSpec::Core::RakeTask.new(:main) do |t, args|
7784
t.pattern = 'spec/**/*_spec.rb'
7885
t.exclude_pattern = 'spec/**/{appsec/integration,contrib,benchmark,redis,auto_instrument,opentelemetry,profiling,crashtracking,error_tracking,rubocop}/**/*_spec.rb,' \
79-
' spec/**/{auto_instrument,opentelemetry,process_discovery,stable_config}_spec.rb, spec/datadog/gem_packaging_spec.rb'
86+
' spec/**/{auto_instrument,opentelemetry,process_discovery,stable_config,ddsketch}_spec.rb,' \
87+
' spec/datadog/gem_packaging_spec.rb'
8088
t.rspec_opts = args.to_a.join(' ')
8189
end
8290

@@ -202,27 +210,8 @@ namespace :spec do
202210
end
203211

204212
# rubocop:disable Style/MultilineBlockChain
205-
RSpec::Core::RakeTask.new(:crashtracking) do |t, args|
206-
t.pattern = 'spec/datadog/core/crashtracking/**/*_spec.rb'
207-
t.rspec_opts = args.to_a.join(' ')
208-
end.tap do |t|
209-
Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"])
210-
end
211-
# rubocop:enable Style/MultilineBlockChain
212-
213-
# rubocop:disable Style/MultilineBlockChain
214-
RSpec::Core::RakeTask.new(:process_discovery) do |t, args|
215-
t.pattern = 'spec/datadog/core/process_discovery_spec.rb'
216-
t.rspec_opts = args.to_a.join(' ')
217-
end.tap do |t|
218-
Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"])
219-
end
220-
# rubocop:enable Style/MultilineBlockChain
221-
222-
# rubocop:disable Style/MultilineBlockChain
223-
desc '' # "Explicitly hiding from `rake -T`"
224-
RSpec::Core::RakeTask.new(:stable_config) do |t, args|
225-
t.pattern = 'spec/datadog/core/configuration/stable_config_spec.rb'
213+
RSpec::Core::RakeTask.new(:core_with_libdatadog_api) do |t, args|
214+
t.pattern = CORE_WITH_LIBDATADOG_API.join(', ')
226215
t.rspec_opts = args.to_a.join(' ')
227216
end.tap do |t|
228217
Rake::Task[t.name].enhance(["compile:libdatadog_api.#{RUBY_VERSION[/\d+.\d+/]}_#{RUBY_PLATFORM}"])

ext/LIBDATADOG_DEVELOPMENT.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Libdatadog development
2+
3+
These instructions can quickly get outdated, so feel free to open an issue if they're not working (and/or ping @ivoanjo).
4+
5+
## Using libdatadog builds from CI or GitHub
6+
7+
If you're developing inside docker/natively on Linux, you can use libdatadog builds from CI and GitHub.
8+
9+
Here's what to do:
10+
11+
1. Create a folder for extracting libdatadog into based on your ruby platform (for instance inside the dd-trace-rb repo):
12+
13+
```bash
14+
export DD_RUBY_PLATFORM=`ruby -e 'puts Gem::Platform.local.to_s'`
15+
echo "Current ruby platform: $DD_RUBY_PLATFORM"
16+
mkdir -p my-libdatadog-build/$DD_RUBY_PLATFORM
17+
```
18+
19+
2. Find a libdatadog build from CI or [GitHub releases](https://github.com/DataDog/libdatadog/releases). This should match the Ruby platform seen above.
20+
3. Extract the libdatadog build into the folder:
21+
22+
```bash
23+
# In this example the build is in my downloads; notice the use of strip-components to get the correct folder structure
24+
tar zxvf ~/Downloads/libdatadog-x86_64-unknown-linux-gnu.tar.gz -C my-libdatadog-build/$DD_RUBY_PLATFORM/ --strip-components=1
25+
# Here's how it should look after
26+
ls my-libdatadog-build/$DD_RUBY_PLATFORM
27+
bin cmake include lib LICENSE LICENSE-3rdparty.yml NOTICE
28+
```
29+
30+
6. Tell Ruby where to find libdatadog: ```export LIBDATADOG_VENDOR_OVERRIDE=`pwd`/my-libdatadog-build/``` (Notice no platform + use of pwd for full path here)
31+
7. From dd-trace-rb, run `bundle exec rake clean compile`
32+
8. For incremental builds, usually `bundle exec rake compile` is faster and `clean` is not needed
33+
34+
If you additionally want to run the profiler test suite, also remember to `export DD_PROFILING_MACOS_TESTING=true` and re-run `rake clean compile`.
35+
36+
## Native development on macOS
37+
38+
As of this writing (August 2025), the libdatadog builds on rubygems.org only support Linux.
39+
40+
We don't officially support using libdatadog for Ruby on other platforms yet, but it is possible to use it for local development on macOS.
41+
(**Note that you don't need these instructions if you develop inside docker.**)
42+
43+
Here's how you can do so:
44+
45+
1. [Install rust](https://www.rust-lang.org/tools/install)
46+
2. Install `cbindgen`: `cargo install cbindgen`
47+
3. Clone [libdatadog](https://github.com/datadog/libdatadog)
48+
4. Create a folder for building into based on your ruby platform:
49+
50+
```bash
51+
export DD_RUBY_PLATFORM=`ruby -e 'puts Gem::Platform.local.to_s'`
52+
mkdir -p my-libdatadog-build/$DD_RUBY_PLATFORM
53+
```
54+
55+
5. From inside of the libdatadog repo, build libdatadog into this folder: `./build-profiling-ffi.sh my-libdatadog-build/$DD_RUBY_PLATFORM`
56+
6. Tell Ruby where to find libdatadog: `export LIBDATADOG_VENDOR_OVERRIDE=/adjust/this/to/be/the/full/path/to/my-libdatadog-build/` (Notice no platform here)
57+
7. From dd-trace-rb, run `bundle exec rake clean compile`
58+
8. For incremental builds, usually `bundle exec rake compile` is faster and `clean` is not needed
59+
60+
If you additionally want to run the profiler test suite, also remember to `export DD_PROFILING_MACOS_TESTING=true` and re-run `rake clean compile`.

ext/libdatadog_api/ddsketch.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#include <ruby.h>
2+
#include <datadog/ddsketch.h>
3+
4+
#include "datadog_ruby_common.h"
5+
6+
static VALUE _native_new(VALUE klass);
7+
static void ddsketch_free(void *ptr);
8+
static VALUE native_add(VALUE self, VALUE point);
9+
static VALUE native_add_with_count(VALUE self, VALUE point, VALUE count);
10+
static VALUE native_count(VALUE self);
11+
static VALUE native_encode(VALUE self);
12+
NORETURN(static void raise_ddsketch_error(const char *message, ddog_VoidResult result));
13+
14+
void ddsketch_init(VALUE core_module) {
15+
VALUE ddsketch_class = rb_define_class_under(core_module, "DDSketch", rb_cObject);
16+
17+
rb_define_alloc_func(ddsketch_class, _native_new);
18+
rb_define_method(ddsketch_class, "add", native_add, 1);
19+
rb_define_method(ddsketch_class, "add_with_count", native_add_with_count, 2);
20+
rb_define_method(ddsketch_class, "count", native_count, 0);
21+
rb_define_method(ddsketch_class, "encode", native_encode, 0);
22+
}
23+
24+
// This structure is used to define a Ruby object that stores a pointer to a ddsketch_Handle_DDSketch
25+
// See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works
26+
static const rb_data_type_t ddsketch_typed_data = {
27+
.wrap_struct_name = "Datadog::DDSketch",
28+
.function = {
29+
.dmark = NULL, // We don't store references to Ruby objects so we don't need to mark any of them
30+
.dfree = ddsketch_free,
31+
.dsize = NULL, // We don't track memory usage (although it'd be cool if we did!)
32+
//.dcompact = NULL, // Not needed -- we don't store references to Ruby objects
33+
},
34+
.flags = RUBY_TYPED_FREE_IMMEDIATELY
35+
};
36+
37+
static VALUE _native_new(VALUE klass) {
38+
ddsketch_Handle_DDSketch *state = ruby_xcalloc(1, sizeof(ddsketch_Handle_DDSketch));
39+
40+
*state = ddog_ddsketch_new();
41+
42+
return TypedData_Wrap_Struct(klass, &ddsketch_typed_data, state);
43+
}
44+
45+
static void ddsketch_free(void *ptr) {
46+
ddsketch_Handle_DDSketch *state = (ddsketch_Handle_DDSketch *) ptr;
47+
ddog_ddsketch_drop(state);
48+
ruby_xfree(ptr);
49+
}
50+
51+
static void raise_ddsketch_error(const char *message, ddog_VoidResult result) {
52+
rb_raise(rb_eRuntimeError, "%s: %"PRIsVALUE, message, get_error_details_and_drop(&result.err));
53+
}
54+
55+
static VALUE native_add(VALUE self, VALUE point) {
56+
ddsketch_Handle_DDSketch *state;
57+
TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state);
58+
59+
ddog_VoidResult result = ddog_ddsketch_add(state, NUM2DBL(point));
60+
61+
if (result.tag == DDOG_VOID_RESULT_ERR) raise_ddsketch_error("DDSketch add failed", result);
62+
63+
return self;
64+
}
65+
66+
static VALUE native_add_with_count(VALUE self, VALUE point, VALUE count) {
67+
ddsketch_Handle_DDSketch *state;
68+
TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state);
69+
70+
ddog_VoidResult result = ddog_ddsketch_add_with_count(state, NUM2DBL(point), NUM2DBL(count));
71+
72+
if (result.tag == DDOG_VOID_RESULT_ERR) raise_ddsketch_error("DDSketch add_with_count failed", result);
73+
74+
return self;
75+
}
76+
77+
static VALUE native_count(VALUE self) {
78+
ddsketch_Handle_DDSketch *state;
79+
TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state);
80+
81+
double count_out;
82+
ddog_VoidResult result = ddog_ddsketch_count(state, &count_out);
83+
84+
if (result.tag == DDOG_VOID_RESULT_ERR) raise_ddsketch_error("DDSketch count failed", result);
85+
86+
return DBL2NUM(count_out);
87+
}
88+
89+
static VALUE native_encode(VALUE self) {
90+
ddsketch_Handle_DDSketch *state;
91+
TypedData_Get_Struct(self, ddsketch_Handle_DDSketch, &ddsketch_typed_data, state);
92+
93+
ddog_Vec_U8 encoded = ddog_ddsketch_encode(state);
94+
95+
// Copy into a Ruby string
96+
VALUE bytes = rb_str_new((const char *) encoded.ptr, encoded.len);
97+
98+
ddog_Vec_U8_drop(encoded);
99+
100+
// The sketch is consumed by encode; to make this a bit more user-friendly for
101+
// a Ruby API (since we can't "kill" the Ruby object), let's re-initialize it so
102+
// it can be used again.
103+
*state = ddog_ddsketch_new();
104+
105+
return bytes;
106+
}

ext/libdatadog_api/init.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
#include "process_discovery.h"
66
#include "library_config.h"
77

8+
void ddsketch_init(VALUE core_module);
9+
810
void DDTRACE_EXPORT Init_libdatadog_api(void) {
911
VALUE datadog_module = rb_define_module("Datadog");
1012
VALUE core_module = rb_define_module_under(datadog_module, "Core");
1113

1214
crashtracker_init(core_module);
1315
process_discovery_init(core_module);
1416
library_config_init(core_module);
17+
ddsketch_init(core_module);
1518
}

ext/libdatadog_api/macos_development.md

Lines changed: 0 additions & 26 deletions
This file was deleted.

lib/datadog/core/ddsketch.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
require 'datadog/core'
4+
5+
module Datadog
6+
module Core
7+
# Used to access ddsketch APIs.
8+
# APIs in this class are implemented as native code.
9+
class DDSketch
10+
def self.supported?
11+
Datadog::Core::LIBDATADOG_API_FAILURE.nil?
12+
end
13+
14+
def initialize
15+
unless self.class.supported?
16+
raise(ArgumentError, "DDSketch is not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}")
17+
end
18+
end
19+
end
20+
end
21+
end

sig/datadog/core/ddsketch.rbs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Datadog
2+
module Core
3+
class DDSketch
4+
def self.supported?: () -> bool
5+
6+
# Adds a single point to the sketch
7+
# @param point [::Numeric] The value to add to the sketch
8+
# @return [true] Always returns true on success, raises RuntimeError on failure
9+
def add: (::Numeric point) -> true
10+
11+
# Adds a point with a count to the sketch
12+
# @param point [::Numeric] The value to add to the sketch
13+
# @param count [::Numeric] The count/weight for this point
14+
# @return [true] Always returns true on success, raises RuntimeError on failure
15+
def add_with_count: (::Numeric point, ::Numeric count) -> true
16+
17+
# Returns the total count of points in the sketch
18+
# @return [::Float] The total count of points
19+
def count: () -> ::Float
20+
21+
# Encodes the sketch to bytes and resets it for reuse
22+
# @return [::String] The encoded sketch as a binary string
23+
def encode: () -> ::String
24+
end
25+
end
26+
end

0 commit comments

Comments
 (0)