Skip to content
Closed
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
2 changes: 2 additions & 0 deletions ext/libdatadog_api/datadog_ruby_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// IMPORTANT: Currently this file is copy-pasted between extensions. Make sure to update all versions when doing any change!

#include <ruby.h>
#include <datadog/common.h>
#include <datadog/library-config.h>
#include <datadog/profiling.h>

// Used to mark symbols to be exported to the outside of the extension.
Expand Down
47 changes: 35 additions & 12 deletions ext/libdatadog_api/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# rubocop:disable Style/GlobalVars

require 'rubygems'
require 'pathname'
require_relative '../libdatadog_extconf_helpers'

def skip_building_extension!(reason)
Expand All @@ -27,8 +28,14 @@ def skip_building_extension!(reason)
skip_building_extension!('current Ruby VM is not supported') if RUBY_ENGINE != 'ruby'
skip_building_extension!('Microsoft Windows is not supported') if Gem.win_platform?

libdatadog_issue = Datadog::LibdatadogExtconfHelpers.load_libdatadog_or_get_issue
skip_building_extension!("issue setting up `libdatadog` gem: #{libdatadog_issue}") if libdatadog_issue
# Check for custom libdatadog build first
custom_pkgconfig_path = File.join(__dir__, '..', '..', 'my-libdatadog-build', 'pkgconfig', 'datadog_profiling_with_rpath.pc')
using_custom_build = File.exist?(custom_pkgconfig_path)

unless using_custom_build
libdatadog_issue = Datadog::LibdatadogExtconfHelpers.load_libdatadog_or_get_issue
skip_building_extension!("issue setting up `libdatadog` gem: #{libdatadog_issue}") if libdatadog_issue
end

require 'mkmf'

Expand Down Expand Up @@ -73,10 +80,18 @@ def skip_building_extension!(reason)
CONFIG['debugflags'] = '-ggdb3'
end

# If we got here, libdatadog is available and loaded
ENV['PKG_CONFIG_PATH'] = "#{ENV["PKG_CONFIG_PATH"]}:#{Libdatadog.pkgconfig_folder}"
Logging.message("[datadog] PKG_CONFIG_PATH set to #{ENV["PKG_CONFIG_PATH"].inspect}\n")
$stderr.puts("Using libdatadog #{Libdatadog::VERSION} from #{Libdatadog.pkgconfig_folder}")
# Set up PKG_CONFIG_PATH based on whether we're using custom build or gem
if using_custom_build
custom_pkgconfig_dir = File.dirname(custom_pkgconfig_path)
ENV['PKG_CONFIG_PATH'] = "#{ENV["PKG_CONFIG_PATH"]}:#{custom_pkgconfig_dir}"
Logging.message("[datadog] PKG_CONFIG_PATH set to #{ENV["PKG_CONFIG_PATH"].inspect}\n")
$stderr.puts("Using custom libdatadog build from #{custom_pkgconfig_dir}")
else
# If we got here, libdatadog is available and loaded
ENV['PKG_CONFIG_PATH'] = "#{ENV["PKG_CONFIG_PATH"]}:#{Libdatadog.pkgconfig_folder}"
Logging.message("[datadog] PKG_CONFIG_PATH set to #{ENV["PKG_CONFIG_PATH"].inspect}\n")
$stderr.puts("Using libdatadog #{Libdatadog::VERSION} from #{Libdatadog.pkgconfig_folder}")
end

unless pkg_config('datadog_profiling_with_rpath')
Logging.message("[datadog] Ruby detected the pkg-config command is #{$PKGCONFIG.inspect}\n")
Expand All @@ -91,12 +106,20 @@ def skip_building_extension!(reason)
# See comments on the helper methods being used for why we need to additionally set this.
# The extremely excessive escaping around ORIGIN below seems to be correct and was determined after a lot of
# experimentation. We need to get these special characters across a lot of tools untouched...
extra_relative_rpaths = [
Datadog::LibdatadogExtconfHelpers.libdatadog_folder_relative_to_native_lib_folder(current_folder: __dir__),
*Datadog::LibdatadogExtconfHelpers.libdatadog_folder_relative_to_ruby_extensions_folders,
]
extra_relative_rpaths.each { |folder| $LDFLAGS += " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{folder.to_str}" }
Logging.message("[datadog] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
unless using_custom_build
extra_relative_rpaths = [
Datadog::LibdatadogExtconfHelpers.libdatadog_folder_relative_to_native_lib_folder(current_folder: __dir__),
*Datadog::LibdatadogExtconfHelpers.libdatadog_folder_relative_to_ruby_extensions_folders,
]
extra_relative_rpaths.each { |folder| $LDFLAGS += " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{folder.to_str}" }
Logging.message("[datadog] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
else
# For custom build, add rpath to our custom lib folder
custom_lib_folder = File.join(File.dirname(custom_pkgconfig_path), '..', 'arm64-darwin-24', 'lib')
custom_relative_path = Pathname.new(custom_lib_folder).relative_path_from(Pathname.new("#{__dir__}/../../lib/")).to_s
$LDFLAGS += " -Wl,-rpath,$$$\\\\{ORIGIN\\}/#{custom_relative_path}"
Logging.message("[datadog] Custom build $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
end

# Tag the native extension library with the Ruby version and Ruby platform.
# This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
Expand Down
198 changes: 198 additions & 0 deletions ext/libdatadog_api/feature_flags.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#include <ruby.h>
#include <datadog/datadog_ffe.h>

#include "datadog_ruby_common.h"

// Forward declarations
static VALUE configuration_alloc(VALUE klass);
static void configuration_free(void *ptr);
static VALUE configuration_initialize(VALUE self, VALUE json_str);

static VALUE evaluation_context_alloc(VALUE klass);
static void evaluation_context_free(void *ptr);
static VALUE evaluation_context_initialize(VALUE self, VALUE targeting_key);
static VALUE evaluation_context_initialize_with_attribute(VALUE self, VALUE targeting_key, VALUE attr_name, VALUE attr_value);

static VALUE assignment_alloc(VALUE klass);
static void assignment_free(void *ptr);

static VALUE native_get_assignment(VALUE self, VALUE config, VALUE flag_key, VALUE context);

NORETURN(static void raise_ffe_error(const char *message, ddog_VoidResult result));

void feature_flags_init(VALUE core_module) {
VALUE feature_flags_module = rb_define_module_under(core_module, "FeatureFlags");

// Configuration class
VALUE configuration_class = rb_define_class_under(feature_flags_module, "Configuration", rb_cObject);
rb_define_alloc_func(configuration_class, configuration_alloc);
rb_define_method(configuration_class, "_native_initialize", configuration_initialize, 1);

// EvaluationContext class
VALUE evaluation_context_class = rb_define_class_under(feature_flags_module, "EvaluationContext", rb_cObject);
rb_define_alloc_func(evaluation_context_class, evaluation_context_alloc);
rb_define_method(evaluation_context_class, "_native_initialize", evaluation_context_initialize, 1);
rb_define_method(evaluation_context_class, "_native_initialize_with_attribute", evaluation_context_initialize_with_attribute, 3);

// Assignment class
VALUE assignment_class = rb_define_class_under(feature_flags_module, "Assignment", rb_cObject);
rb_define_alloc_func(assignment_class, assignment_alloc);

// Module-level method
rb_define_module_function(feature_flags_module, "_native_get_assignment", native_get_assignment, 3);
}

// Configuration TypedData definition
static const rb_data_type_t configuration_typed_data = {
.wrap_struct_name = "Datadog::Core::FeatureFlags::Configuration",
.function = {
.dmark = NULL,
.dfree = configuration_free,
.dsize = NULL,
},
.flags = RUBY_TYPED_FREE_IMMEDIATELY
};

static VALUE configuration_alloc(VALUE klass) {
ddog_ffe_Handle_Configuration *config = ruby_xcalloc(1, sizeof(ddog_ffe_Handle_Configuration));
return TypedData_Wrap_Struct(klass, &configuration_typed_data, config);
}

static void configuration_free(void *ptr) {
ddog_ffe_Handle_Configuration *config = (ddog_ffe_Handle_Configuration *) ptr;
ddog_ffe_configuration_drop(config);
ruby_xfree(ptr);
}

static VALUE configuration_initialize(VALUE self, VALUE json_str) {
Check_Type(json_str, T_STRING);

ddog_ffe_Handle_Configuration *config;
TypedData_Get_Struct(self, ddog_ffe_Handle_Configuration, &configuration_typed_data, config);

struct ddog_ffe_Result_HandleConfiguration result = ddog_ffe_configuration_new(RSTRING_PTR(json_str));
if (result.tag == DDOG_FFE_RESULT_HANDLE_CONFIGURATION_ERR_HANDLE_CONFIGURATION) {
rb_raise(rb_eRuntimeError, "Failed to create configuration: %"PRIsVALUE, get_error_details_and_drop(&result.err));
}

*config = result.ok;

return self;
}

// EvaluationContext TypedData definition
static const rb_data_type_t evaluation_context_typed_data = {
.wrap_struct_name = "Datadog::Core::FeatureFlags::EvaluationContext",
.function = {
.dmark = NULL,
.dfree = evaluation_context_free,
.dsize = NULL,
},
.flags = RUBY_TYPED_FREE_IMMEDIATELY
};

static VALUE evaluation_context_alloc(VALUE klass) {
ddog_ffe_Handle_EvaluationContext *context = ruby_xcalloc(1, sizeof(ddog_ffe_Handle_EvaluationContext));
return TypedData_Wrap_Struct(klass, &evaluation_context_typed_data, context);
}

static void evaluation_context_free(void *ptr) {
ddog_ffe_Handle_EvaluationContext *context = (ddog_ffe_Handle_EvaluationContext *) ptr;
ddog_ffe_evaluation_context_drop(context);
ruby_xfree(ptr);
}

static VALUE evaluation_context_initialize(VALUE self, VALUE targeting_key) {
Check_Type(targeting_key, T_STRING);

ddog_ffe_Handle_EvaluationContext *context;
TypedData_Get_Struct(self, ddog_ffe_Handle_EvaluationContext, &evaluation_context_typed_data, context);

*context = ddog_ffe_evaluation_context_new(RSTRING_PTR(targeting_key));

return self;
}

static VALUE evaluation_context_initialize_with_attribute(VALUE self, VALUE targeting_key, VALUE attr_name, VALUE attr_value) {
Check_Type(targeting_key, T_STRING);
Check_Type(attr_name, T_STRING);
Check_Type(attr_value, T_STRING);

ddog_ffe_Handle_EvaluationContext *context;
TypedData_Get_Struct(self, ddog_ffe_Handle_EvaluationContext, &evaluation_context_typed_data, context);

struct ddog_ffe_AttributePair attr = {
.name = RSTRING_PTR(attr_name),
.value = RSTRING_PTR(attr_value)
};

*context = ddog_ffe_evaluation_context_new_with_attributes(
RSTRING_PTR(targeting_key),
&attr,
1
);

return self;
}

// Assignment TypedData definition
static const rb_data_type_t assignment_typed_data = {
.wrap_struct_name = "Datadog::Core::FeatureFlags::Assignment",
.function = {
.dmark = NULL,
.dfree = assignment_free,
.dsize = NULL,
},
.flags = RUBY_TYPED_FREE_IMMEDIATELY
};

static VALUE assignment_alloc(VALUE klass) {
ddog_ffe_Handle_Assignment *assignment = ruby_xcalloc(1, sizeof(ddog_ffe_Handle_Assignment));
return TypedData_Wrap_Struct(klass, &assignment_typed_data, assignment);
}

static void assignment_free(void *ptr) {
ddog_ffe_Handle_Assignment *assignment = (ddog_ffe_Handle_Assignment *) ptr;
ddog_ffe_assignment_drop(assignment);
ruby_xfree(ptr);
}

static void raise_ffe_error(const char *message, ddog_VoidResult result) {
rb_raise(rb_eRuntimeError, "%s: %"PRIsVALUE, message, get_error_details_and_drop(&result.err));
}

static VALUE native_get_assignment(VALUE self, VALUE config_obj, VALUE flag_key, VALUE context_obj) {
Check_Type(flag_key, T_STRING);

ddog_ffe_Handle_Configuration *config;
TypedData_Get_Struct(config_obj, ddog_ffe_Handle_Configuration, &configuration_typed_data, config);

ddog_ffe_Handle_EvaluationContext *context;
TypedData_Get_Struct(context_obj, ddog_ffe_Handle_EvaluationContext, &evaluation_context_typed_data, context);

struct ddog_ffe_Result_HandleAssignment result = ddog_ffe_get_assignment(*config, RSTRING_PTR(flag_key), *context);

if (result.tag == DDOG_FFE_RESULT_HANDLE_ASSIGNMENT_ERR_HANDLE_ASSIGNMENT) {
rb_raise(rb_eRuntimeError, "Feature flag evaluation failed: %"PRIsVALUE, get_error_details_and_drop(&result.err));
}

ddog_ffe_Handle_Assignment assignment_out = result.ok;

// Check if assignment is empty (no assignment returned)
if (assignment_out.inner == NULL) {
return Qnil;
}

// Create a new Assignment Ruby object and wrap the result
VALUE assignment_class = rb_const_get_at(rb_const_get_at(rb_const_get(rb_cObject, rb_intern("Datadog")), rb_intern("Core")), rb_intern("FeatureFlags"));
assignment_class = rb_const_get(assignment_class, rb_intern("Assignment"));

VALUE assignment_obj = assignment_alloc(assignment_class);

ddog_ffe_Handle_Assignment *assignment_ptr;
TypedData_Get_Struct(assignment_obj, ddog_ffe_Handle_Assignment, &assignment_typed_data, assignment_ptr);

*assignment_ptr = assignment_out;

return assignment_obj;
}
2 changes: 2 additions & 0 deletions ext/libdatadog_api/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "library_config.h"

void ddsketch_init(VALUE core_module);
void feature_flags_init(VALUE core_module);

void DDTRACE_EXPORT Init_libdatadog_api(void) {
VALUE datadog_module = rb_define_module("Datadog");
Expand All @@ -15,4 +16,5 @@ void DDTRACE_EXPORT Init_libdatadog_api(void) {
process_discovery_init(core_module);
library_config_init(core_module);
ddsketch_init(core_module);
feature_flags_init(core_module);
}
62 changes: 62 additions & 0 deletions lib/datadog/core/feature_flags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

require 'datadog/core'

module Datadog
module Core
# Feature flagging and experimentation engine APIs.
# APIs in this module are implemented as native code.
module FeatureFlags
def self.supported?
Datadog::Core::LIBDATADOG_API_FAILURE.nil?
end

# Configuration for feature flag evaluation
class Configuration
def initialize(json_config)
unless FeatureFlags.supported?
raise(ArgumentError, "Feature Flags are not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}")
end

_native_initialize(json_config)
end
end

# Evaluation context with targeting key and attributes
class EvaluationContext
def initialize(targeting_key)
unless FeatureFlags.supported?
raise(ArgumentError, "Feature Flags are not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}")
end

_native_initialize(targeting_key)
end

def self.new_with_attribute(targeting_key, attr_name, attr_value)
unless FeatureFlags.supported?
raise(ArgumentError, "Feature Flags are not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}")
end

context = allocate
context._native_initialize_with_attributes(targeting_key, [{ attr_name => attr_value }])
context
end
end

# Assignment result from feature flag evaluation
class Assignment
# Assignment objects are created by the native get_assignment method
# No explicit initialization needed
end

# Evaluates a feature flag and returns an Assignment or nil
def self.get_assignment(configuration, flag_key, evaluation_context)
unless supported?
raise(ArgumentError, "Feature Flags are not supported: #{Datadog::Core::LIBDATADOG_API_FAILURE}")
end

_native_get_assignment(configuration, flag_key, evaluation_context)
end
end
end
end
Loading
Loading