|
| 1 | +/* |
| 2 | + * Copyright 2026, Datadog, Inc |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +#include "context_api.h" |
| 18 | +#include "context.h" |
| 19 | +#include "guards.h" |
| 20 | +#include "otel_context.h" |
| 21 | +#include "otel_process_ctx.h" |
| 22 | +#include "profiler.h" |
| 23 | +#include "thread.h" |
| 24 | +#include <cstring> |
| 25 | + |
| 26 | +// Static member initialization |
| 27 | +ContextStorageMode ContextApi::_mode = CTX_STORAGE_PROFILER; |
| 28 | +bool ContextApi::_initialized = false; |
| 29 | +char* ContextApi::_attribute_keys[MAX_ATTRIBUTE_KEYS] = {}; |
| 30 | +int ContextApi::_attribute_key_count = 0; |
| 31 | + |
| 32 | +bool ContextApi::initialize(const Arguments& args) { |
| 33 | + if (__atomic_load_n(&_initialized, __ATOMIC_ACQUIRE)) { |
| 34 | + return true; |
| 35 | + } |
| 36 | + |
| 37 | + ContextStorageMode mode = args._context_storage; |
| 38 | + if (mode == CTX_STORAGE_OTEL) { |
| 39 | + if (!OtelContexts::initialize()) { |
| 40 | + // Fall back to profiler storage if OTEL init fails |
| 41 | + mode = CTX_STORAGE_PROFILER; |
| 42 | + __atomic_store_n(&_mode, mode, __ATOMIC_RELEASE); |
| 43 | + __atomic_store_n(&_initialized, true, __ATOMIC_RELEASE); |
| 44 | + return true; |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + __atomic_store_n(&_mode, mode, __ATOMIC_RELEASE); |
| 49 | + __atomic_store_n(&_initialized, true, __ATOMIC_RELEASE); |
| 50 | + return true; |
| 51 | +} |
| 52 | + |
| 53 | +// Called from Profiler::stop() which is single-threaded — no racing with initialize(). |
| 54 | +void ContextApi::shutdown() { |
| 55 | + if (!__atomic_load_n(&_initialized, __ATOMIC_ACQUIRE)) { |
| 56 | + return; |
| 57 | + } |
| 58 | + |
| 59 | + if (__atomic_load_n(&_mode, __ATOMIC_ACQUIRE) == CTX_STORAGE_OTEL) { |
| 60 | + OtelContexts::shutdown(); |
| 61 | + } |
| 62 | + |
| 63 | + // Free registered attribute keys (null first, free later for signal safety) |
| 64 | + int count = _attribute_key_count; |
| 65 | + _attribute_key_count = 0; |
| 66 | + for (int i = 0; i < count; i++) { |
| 67 | + char* key = _attribute_keys[i]; |
| 68 | + _attribute_keys[i] = nullptr; |
| 69 | + free(key); |
| 70 | + } |
| 71 | + |
| 72 | + __atomic_store_n(&_mode, CTX_STORAGE_PROFILER, __ATOMIC_RELEASE); |
| 73 | + __atomic_store_n(&_initialized, false, __ATOMIC_RELEASE); |
| 74 | +} |
| 75 | + |
| 76 | +bool ContextApi::isInitialized() { |
| 77 | + return __atomic_load_n(&_initialized, __ATOMIC_ACQUIRE); |
| 78 | +} |
| 79 | + |
| 80 | +ContextStorageMode ContextApi::getMode() { |
| 81 | + return __atomic_load_n(&_mode, __ATOMIC_ACQUIRE); |
| 82 | +} |
| 83 | + |
| 84 | +/** |
| 85 | + * Initialize OTel TLS for the current thread on first use. |
| 86 | + * Allocates a per-thread OtelThreadContextRecord and caches it in ProfiledThread. |
| 87 | + * Must be called with signals blocked to prevent musl TLS deadlock. |
| 88 | + */ |
| 89 | +static OtelThreadContextRecord* initializeOtelTls(ProfiledThread* thrd) { |
| 90 | + // Block profiling signals during first TLS access (same pattern as context_tls_v1) |
| 91 | + SignalBlocker blocker; |
| 92 | + |
| 93 | + // Allocate one record per thread — freed in ProfiledThread destructor/releaseFromBuffer |
| 94 | + OtelThreadContextRecord* record = new OtelThreadContextRecord(); |
| 95 | + record->valid = 0; |
| 96 | + record->_reserved = 0; |
| 97 | + record->attrs_data_size = 0; |
| 98 | + |
| 99 | + // First access to custom_labels_current_set_v2 triggers TLS init |
| 100 | + custom_labels_current_set_v2 = nullptr; |
| 101 | + |
| 102 | + // Cache in ProfiledThread for signal-safe cross-thread access |
| 103 | + thrd->markOtelContextInitialized(record); |
| 104 | + |
| 105 | + return record; |
| 106 | +} |
| 107 | + |
| 108 | +void ContextApi::set(u64 span_id, u64 root_span_id) { |
| 109 | + // Datadog tracer-profiler context uses localRootSpanId (64 bits) for trace |
| 110 | + // correlation, so trace_id_high is zero here. The 128-bit put() overload |
| 111 | + // supports full W3C trace IDs when needed. |
| 112 | + setOtel(0, root_span_id, span_id); |
| 113 | +} |
| 114 | + |
| 115 | +void ContextApi::setOtel(u64 trace_id_high, u64 trace_id_low, u64 span_id) { |
| 116 | + ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE); |
| 117 | + |
| 118 | + if (mode == CTX_STORAGE_OTEL) { |
| 119 | + // All-zero IDs = context detachment per OTEP — use NULL pointer |
| 120 | + if (trace_id_high == 0 && trace_id_low == 0 && span_id == 0) { |
| 121 | + OtelContexts::clear(); |
| 122 | + return; |
| 123 | + } |
| 124 | + |
| 125 | + // Ensure TLS + record are initialized on first use |
| 126 | + ProfiledThread* thrd = ProfiledThread::current(); |
| 127 | + if (thrd == nullptr) return; |
| 128 | + |
| 129 | + if (!thrd->isOtelContextInitialized()) { |
| 130 | + initializeOtelTls(thrd); |
| 131 | + } |
| 132 | + |
| 133 | + OtelContexts::set(trace_id_high, trace_id_low, span_id); |
| 134 | + } else { |
| 135 | + // Profiler mode: use existing TLS |
| 136 | + Context& ctx = Contexts::get(); |
| 137 | + |
| 138 | + __atomic_store_n(&ctx.checksum, 0ULL, __ATOMIC_RELEASE); |
| 139 | + __atomic_store_n(&ctx.spanId, span_id, __ATOMIC_RELAXED); |
| 140 | + __atomic_store_n(&ctx.rootSpanId, trace_id_low, __ATOMIC_RELAXED); |
| 141 | + |
| 142 | + u64 newChecksum = Contexts::checksum(span_id, trace_id_low); |
| 143 | + __atomic_store_n(&ctx.checksum, newChecksum, __ATOMIC_RELEASE); |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +bool ContextApi::get(u64& span_id, u64& root_span_id) { |
| 148 | + ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE); |
| 149 | + |
| 150 | + if (mode == CTX_STORAGE_OTEL) { |
| 151 | + u64 trace_high, trace_low; |
| 152 | + if (OtelContexts::get(trace_high, trace_low, span_id)) { |
| 153 | + root_span_id = trace_low; |
| 154 | + return true; |
| 155 | + } |
| 156 | + return false; |
| 157 | + } else { |
| 158 | + Context& ctx = Contexts::get(); |
| 159 | + u64 checksum1 = __atomic_load_n(&ctx.checksum, __ATOMIC_ACQUIRE); |
| 160 | + span_id = __atomic_load_n(&ctx.spanId, __ATOMIC_RELAXED); |
| 161 | + root_span_id = __atomic_load_n(&ctx.rootSpanId, __ATOMIC_RELAXED); |
| 162 | + return checksum1 != 0 && checksum1 == Contexts::checksum(span_id, root_span_id); |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | + |
| 167 | +void ContextApi::clear() { |
| 168 | + ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE); |
| 169 | + |
| 170 | + if (mode == CTX_STORAGE_OTEL) { |
| 171 | + OtelContexts::clear(); |
| 172 | + } else { |
| 173 | + set(0, 0); |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +bool ContextApi::setAttribute(uint8_t key_index, const char* value, uint8_t value_len) { |
| 178 | + ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE); |
| 179 | + |
| 180 | + if (mode == CTX_STORAGE_OTEL) { |
| 181 | + // Ensure TLS + record are initialized on first use |
| 182 | + ProfiledThread* thrd = ProfiledThread::current(); |
| 183 | + if (thrd == nullptr) return false; |
| 184 | + |
| 185 | + if (!thrd->isOtelContextInitialized()) { |
| 186 | + initializeOtelTls(thrd); |
| 187 | + } |
| 188 | + |
| 189 | + // Pre-register the value string in the Dictionary from this JNI thread. |
| 190 | + // The signal handler (writeCurrentContext) will later call bounded_lookup |
| 191 | + // to find the encoding — by pre-registering here, the signal handler |
| 192 | + // only does a read (no malloc), which is async-signal-safe. |
| 193 | + Profiler::instance()->contextValueMap()->bounded_lookup( |
| 194 | + value, value_len, 1 << 16); |
| 195 | + |
| 196 | + return OtelContexts::setAttribute(key_index, value, value_len); |
| 197 | + } else { |
| 198 | + // Profiler mode: register the string and write encoding to Context.tags[] |
| 199 | + if (key_index >= DD_TAGS_CAPACITY) return false; |
| 200 | + |
| 201 | + u32 encoding = Profiler::instance()->contextValueMap()->bounded_lookup( |
| 202 | + value, value_len, 1 << 16); |
| 203 | + if (encoding == INT_MAX) return false; |
| 204 | + |
| 205 | + Context& ctx = Contexts::get(); |
| 206 | + ctx.tags[key_index].value = encoding; |
| 207 | + return true; |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +// Called from JNI during setup, before profiling starts. |
| 212 | +void ContextApi::registerAttributeKeys(const char** keys, int count) { |
| 213 | + // Free any previously registered keys (null first, free later) |
| 214 | + int oldCount = _attribute_key_count; |
| 215 | + _attribute_key_count = 0; |
| 216 | + char* oldKeys[MAX_ATTRIBUTE_KEYS]; |
| 217 | + for (int i = 0; i < oldCount; i++) { |
| 218 | + oldKeys[i] = _attribute_keys[i]; |
| 219 | + _attribute_keys[i] = nullptr; |
| 220 | + } |
| 221 | + for (int i = 0; i < oldCount; i++) { |
| 222 | + free(oldKeys[i]); |
| 223 | + } |
| 224 | + |
| 225 | + _attribute_key_count = count < MAX_ATTRIBUTE_KEYS ? count : MAX_ATTRIBUTE_KEYS; |
| 226 | + for (int i = 0; i < _attribute_key_count; i++) { |
| 227 | + _attribute_keys[i] = strdup(keys[i]); |
| 228 | + } |
| 229 | + |
| 230 | + // If in OTEL mode, re-publish process context with thread_ctx_config |
| 231 | + ContextStorageMode mode = __atomic_load_n(&_mode, __ATOMIC_ACQUIRE); |
| 232 | + if (mode == CTX_STORAGE_OTEL) { |
| 233 | + // Build NULL-terminated key array for the process context config |
| 234 | + const char* key_ptrs[MAX_ATTRIBUTE_KEYS + 1]; |
| 235 | + for (int i = 0; i < _attribute_key_count; i++) { |
| 236 | + key_ptrs[i] = _attribute_keys[i]; |
| 237 | + } |
| 238 | + key_ptrs[_attribute_key_count] = nullptr; |
| 239 | + |
| 240 | + otel_thread_ctx_config_data config = { |
| 241 | + .schema_version = "tlsdesc_v1_dev", |
| 242 | + .attribute_key_map = key_ptrs, |
| 243 | + }; |
| 244 | + |
| 245 | +#ifndef OTEL_PROCESS_CTX_NO_READ |
| 246 | + // Re-publish the process context with thread_ctx_config. |
| 247 | + // Guard: otel_process_ctx_read() is only available when the read API is compiled in. |
| 248 | + // We need to read the current context and re-publish with the config |
| 249 | + otel_process_ctx_read_result read_result = otel_process_ctx_read(); |
| 250 | + if (read_result.success) { |
| 251 | + otel_process_ctx_data data = { |
| 252 | + .deployment_environment_name = read_result.data.deployment_environment_name, |
| 253 | + .service_instance_id = read_result.data.service_instance_id, |
| 254 | + .service_name = read_result.data.service_name, |
| 255 | + .service_version = read_result.data.service_version, |
| 256 | + .telemetry_sdk_language = read_result.data.telemetry_sdk_language, |
| 257 | + .telemetry_sdk_version = read_result.data.telemetry_sdk_version, |
| 258 | + .telemetry_sdk_name = read_result.data.telemetry_sdk_name, |
| 259 | + .resource_attributes = read_result.data.resource_attributes, |
| 260 | + .extra_attributes = read_result.data.extra_attributes, |
| 261 | + .thread_ctx_config = &config, |
| 262 | + }; |
| 263 | + |
| 264 | + otel_process_ctx_publish(&data); |
| 265 | + otel_process_ctx_read_drop(&read_result); |
| 266 | + } |
| 267 | +#endif |
| 268 | + } |
| 269 | +} |
| 270 | + |
| 271 | +const char* ContextApi::getAttributeKey(int index) { |
| 272 | + if (index < 0 || index >= _attribute_key_count) { |
| 273 | + return nullptr; |
| 274 | + } |
| 275 | + return _attribute_keys[index]; |
| 276 | +} |
| 277 | + |
| 278 | +int ContextApi::getAttributeKeyCount() { |
| 279 | + return _attribute_key_count; |
| 280 | +} |
0 commit comments