Skip to content

Commit 3bb5c2a

Browse files
feat: add support for pushing events
1 parent a5d1649 commit 3bb5c2a

File tree

12 files changed

+370
-2
lines changed

12 files changed

+370
-2
lines changed

ddtrace/internal/datadog/profiling/dd_wrapper/include/ddup_interface.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ extern "C"
6767
int64_t line);
6868
void ddup_push_absolute_ns(Datadog::Sample* sample, int64_t timestamp_ns);
6969
void ddup_push_monotonic_ns(Datadog::Sample* sample, int64_t monotonic_ns);
70+
void ddup_push_event(Datadog::Sample* sample, std::string_view event_type);
71+
void ddup_push_label(Datadog::Sample* sample, std::string_view key, std::string_view val);
7072

7173
void ddup_increment_sampling_event_count();
7274
void ddup_increment_sample_count();

ddtrace/internal/datadog/profiling/dd_wrapper/include/libdatadog_helpers.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ namespace Datadog {
4646
X(trace_type, "trace type") \
4747
X(class_name, "class name") \
4848
X(lock_name, "lock name") \
49-
X(gpu_device_name, "gpu device name")
49+
X(gpu_device_name, "gpu device name") \
50+
X(event_type, "event type")
5051

5152
#define X_ENUM(a, b) a,
5253
#define X_STR(a, b) b,

ddtrace/internal/datadog/profiling/dd_wrapper/include/sample.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class Sample
9191
// Helpers
9292
bool push_label(ExportLabelKey key, std::string_view val);
9393
bool push_label(ExportLabelKey key, int64_t val);
94+
bool push_label(std::string_view key, std::string_view val);
9495
void push_frame_impl(std::string_view name, std::string_view filename, uint64_t address, int64_t line);
9596
void clear_buffers();
9697

@@ -104,6 +105,7 @@ class Sample
104105
bool push_gpu_gputime(int64_t time, int64_t count);
105106
bool push_gpu_memory(int64_t size, int64_t count);
106107
bool push_gpu_flops(int64_t flops, int64_t count);
108+
bool push_event(std::string_view event_type);
107109

108110
// Adds metadata to sample
109111
bool push_lock_name(std::string_view lock_name);

ddtrace/internal/datadog/profiling/dd_wrapper/include/types.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ enum SampleType : unsigned int
1414
GPUTime = 1 << 7,
1515
GPUMemory = 1 << 8,
1616
GPUFlops = 1 << 9,
17-
All = CPU | Wall | Exception | LockAcquire | LockRelease | Allocation | Heap | GPUTime | GPUMemory | GPUFlops
17+
Event = 1 << 10,
18+
All =
19+
CPU | Wall | Exception | LockAcquire | LockRelease | Allocation | Heap | GPUTime | GPUMemory | GPUFlops | Event
1820
};
1921

2022
// Every Sample object has a corresponding `values` vector, since libdatadog expects contiguous values per sample.
@@ -39,6 +41,7 @@ struct ValueIndex
3941
unsigned short gpu_alloc_count;
4042
unsigned short gpu_flops;
4143
unsigned short gpu_flops_samples; // Should be "count," but flops is already a count
44+
unsigned short event_count;
4245
};
4346

4447
} // namespace Datadog

ddtrace/internal/datadog/profiling/dd_wrapper/src/ddup_interface.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,18 @@ ddup_push_monotonic_ns(Datadog::Sample* sample, int64_t monotonic_ns) // cppchec
303303
sample->push_monotonic_ns(monotonic_ns);
304304
}
305305

306+
void
307+
ddup_push_event(Datadog::Sample* sample, std::string_view event_type) // cppcheck-suppress unusedFunction
308+
{
309+
sample->push_event(event_type);
310+
}
311+
312+
void
313+
ddup_push_label(Datadog::Sample* sample, std::string_view key, std::string_view val) // cppcheck-suppress unusedFunction
314+
{
315+
sample->push_label(key, val);
316+
}
317+
306318
void
307319
ddup_increment_sampling_event_count() // cppcheck-suppress unusedFunction
308320
{

ddtrace/internal/datadog/profiling/dd_wrapper/src/profile.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ Datadog::Profile::setup_samplers()
116116
val_idx.gpu_flops = get_value_idx("gpu-flops", "count");
117117
val_idx.gpu_flops_samples = get_value_idx("gpu-flops-samples", "count");
118118
}
119+
if (0U != (type_mask & SampleType::Event)) {
120+
val_idx.event_count = get_value_idx("event-samples", "count");
121+
}
119122

120123
// Whatever the first sampler happens to be is the default "period" for the profile
121124
// The value of 1 is a pointless default.

ddtrace/internal/datadog/profiling/dd_wrapper/src/sample.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,23 @@ Datadog::Sample::push_label(const ExportLabelKey key, int64_t val)
125125
return true;
126126
}
127127

128+
bool
129+
Datadog::Sample::push_label(std::string_view key, std::string_view val)
130+
{
131+
// Push a custom label with arbitrary key and value
132+
if (val.empty() || key.empty()) {
133+
return true;
134+
}
135+
136+
// Persist both key and val strings in the arena
137+
key = string_storage.insert(key);
138+
val = string_storage.insert(val);
139+
auto& label = labels.emplace_back();
140+
label.key = to_slice(key);
141+
label.str = to_slice(val);
142+
return true;
143+
}
144+
128145
void
129146
Datadog::Sample::clear_buffers()
130147
{
@@ -340,6 +357,22 @@ Datadog::Sample::push_gpu_flops(int64_t size, int64_t count)
340357
return false;
341358
}
342359

360+
bool
361+
Datadog::Sample::push_event(std::string_view event_type)
362+
{
363+
static bool already_warned = false; // cppcheck-suppress threadsafety-threadsafety
364+
if (0U != (type_mask & SampleType::Event)) {
365+
push_label(ExportLabelKey::event_type, event_type);
366+
values[profile_state.val().event_count] += 0;
367+
return true;
368+
}
369+
if (!already_warned) {
370+
already_warned = true;
371+
std::cerr << "bad push event" << std::endl;
372+
}
373+
return false;
374+
}
375+
343376
bool
344377
Datadog::Sample::push_lock_name(std::string_view lock_name)
345378
{

ddtrace/internal/datadog/profiling/ddup/_ddup.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ class SampleHandle:
4141
def push_alloc(self, value: int, count: int) -> None: ...
4242
def push_class_name(self, class_name: StringType) -> None: ...
4343
def push_cputime(self, value: int, count: int) -> None: ...
44+
def push_event(self, event_type: StringType) -> None: ...
4445
def push_exceptioninfo(self, exc_type: Union[None, bytes, str, type], count: int) -> None: ...
4546
def push_frame(self, name: StringType, filename: StringType, address: int, line: int) -> None: ...
4647
def push_gpu_device_name(self, device_name: StringType) -> None: ...
4748
def push_gpu_flops(self, value: int, count: int) -> None: ...
4849
def push_gpu_gputime(self, value: int, count: int) -> None: ...
4950
def push_gpu_memory(self, value: int, count: int) -> None: ...
5051
def push_heap(self, value: int) -> None: ...
52+
def push_label(self, key: StringType, val: StringType) -> None: ...
5153
def push_lock_name(self, lock_name: StringType) -> None: ...
5254
def push_monotonic_ns(self, monotonic_ns: int) -> None: ...
5355
def push_release(self, value: int, count: int) -> None: ...
@@ -56,3 +58,10 @@ class SampleHandle:
5658
def push_task_name(self, task_name: StringType) -> None: ...
5759
def push_threadinfo(self, thread_id: int, thread_native_id: int, thread_name: StringType) -> None: ...
5860
def push_walltime(self, value: int, count: int) -> None: ...
61+
62+
def push_event(
63+
event_type: str,
64+
labels: Optional[Dict[str, str]] = None,
65+
capture_stack: bool = True,
66+
max_nframes: Optional[int] = None,
67+
) -> None: ...

ddtrace/internal/datadog/profiling/ddup/_ddup.pyx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ cdef extern from "ddup_interface.hpp":
8383
void ddup_push_frame(Sample *sample, string_view _name, string_view _filename, uint64_t address, int64_t line)
8484
void ddup_push_monotonic_ns(Sample *sample, int64_t monotonic_ns)
8585
void ddup_push_absolute_ns(Sample *sample, int64_t monotonic_ns)
86+
void ddup_push_event(Sample *sample, string_view event_type)
87+
void ddup_push_label(Sample *sample, string_view key, string_view val)
8688
void ddup_flush_sample(Sample *sample)
8789
void ddup_drop_sample(Sample *sample)
8890

@@ -300,6 +302,37 @@ cdef call_ddup_push_trace_type(Sample* sample, trace_type: StringType):
300302
if utf8_data != NULL:
301303
ddup_push_trace_type(sample, string_view(utf8_data, utf8_size))
302304

305+
cdef call_ddup_push_event(Sample* sample, event_type: StringType):
306+
if not event_type:
307+
return
308+
if isinstance(event_type, bytes):
309+
ddup_push_event(sample, string_view(<const char*>event_type, len(event_type)))
310+
return
311+
cdef const char* utf8_data
312+
cdef Py_ssize_t utf8_size
313+
utf8_data = PyUnicode_AsUTF8AndSize(event_type, &utf8_size)
314+
if utf8_data != NULL:
315+
ddup_push_event(sample, string_view(utf8_data, utf8_size))
316+
317+
cdef call_ddup_push_label(Sample* sample, key: StringType, val: StringType):
318+
if not key or not val:
319+
return
320+
if isinstance(key, bytes) and isinstance(val, bytes):
321+
ddup_push_label(sample, string_view(<const char*>key, len(key)), string_view(<const char*>val, len(val)))
322+
return
323+
cdef const char* key_utf8_data
324+
cdef Py_ssize_t key_utf8_size
325+
cdef const char* val_utf8_data
326+
cdef Py_ssize_t val_utf8_size
327+
key_utf8_data = PyUnicode_AsUTF8AndSize(key, &key_utf8_size)
328+
val_utf8_data = PyUnicode_AsUTF8AndSize(val, &val_utf8_size)
329+
if key_utf8_data != NULL and val_utf8_data != NULL:
330+
ddup_push_label(
331+
sample,
332+
string_view(key_utf8_data, key_utf8_size),
333+
string_view(val_utf8_data, val_utf8_size)
334+
)
335+
303336
# Conversion functions
304337
cdef uint64_t clamp_to_uint64_unsigned(value):
305338
# This clamps a Python int to the nonnegative range of an unsigned 64-bit integer.
@@ -323,6 +356,50 @@ cdef int64_t clamp_to_int64_unsigned(value):
323356
cdef bint _code_provenance_set = False
324357

325358

359+
def push_event(
360+
event_type: str,
361+
labels: Optional[Dict[str, str]] = None,
362+
capture_stack: bool = True,
363+
max_nframes: Optional[int] = None,
364+
) -> None:
365+
"""Push a custom event to the profiler.
366+
367+
Events are samples with a value of 0 that represent points in time.
368+
They are tagged with an event_type label and optional custom labels.
369+
370+
Args:
371+
event_type: The type of event (e.g., "task_start", "task_end")
372+
labels: Optional dictionary of custom labels to attach to the event
373+
capture_stack: Whether to capture the current stack trace (default: True)
374+
max_nframes: Maximum number of frames to capture (default: use global config)
375+
"""
376+
import sys
377+
from types import FrameType
378+
from ddtrace.profiling.collector import _traceback
379+
from ddtrace.internal.settings.profiling import config
380+
381+
handle = SampleHandle()
382+
383+
# Push the event type label
384+
handle.push_event(event_type)
385+
386+
# Push any custom labels
387+
if labels:
388+
for key, value in labels.items():
389+
handle.push_label(str(key), str(value))
390+
391+
# Capture stack trace if requested
392+
if capture_stack:
393+
nframes = max_nframes if max_nframes is not None else config.max_frames
394+
# Skip this function's frame (sys._getframe(0) is push_event itself)
395+
frame: FrameType = sys._getframe(1)
396+
frames, _ = _traceback.pyframe_to_frames(frame, nframes)
397+
for ddframe in frames:
398+
handle.push_frame(ddframe.function_name, ddframe.file_name, 0, ddframe.lineno)
399+
400+
handle.flush_sample()
401+
402+
326403
def config(
327404
service: StringType = None,
328405
env: StringType = None,
@@ -523,6 +600,14 @@ cdef class SampleHandle:
523600
if self.ptr is not NULL:
524601
ddup_push_absolute_ns(self.ptr, <int64_t>timestamp_ns)
525602

603+
def push_event(self, event_type: StringType) -> None:
604+
if self.ptr is not NULL:
605+
call_ddup_push_event(self.ptr, event_type)
606+
607+
def push_label(self, key: StringType, val: StringType) -> None:
608+
if self.ptr is not NULL:
609+
call_ddup_push_label(self.ptr, key, val)
610+
526611
def flush_sample(self) -> None:
527612
# Flushing the sample consumes it. The user will no longer be able to use
528613
# this handle after flushing it.

examples/profiling_push_events.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Example demonstrating how to push custom events to the profiler.
2+
3+
This shows how to use the new push_event() API to send custom events
4+
(like asyncio task starts) to your profiles.
5+
"""
6+
import asyncio
7+
from typing import Optional
8+
9+
from ddtrace.internal.datadog.profiling import ddup
10+
11+
12+
def example_simple_event() -> None:
13+
"""Push a simple event with no stack trace."""
14+
ddup.push_event("my_custom_event", capture_stack=False)
15+
16+
17+
def example_event_with_stack() -> None:
18+
"""Push an event with a stack trace (default behavior)."""
19+
ddup.push_event("function_called")
20+
21+
22+
def example_event_with_labels() -> None:
23+
"""Push an event with custom labels."""
24+
labels = {
25+
"task_name": "my_task",
26+
"parent_task": "parent_id_123",
27+
"status": "started",
28+
}
29+
ddup.push_event("task_start", labels=labels)
30+
31+
32+
def example_event_with_limited_stack() -> None:
33+
"""Push an event with a limited stack trace."""
34+
ddup.push_event("checkpoint", max_nframes=10)
35+
36+
37+
async def track_asyncio_task(task_name: str, parent_task: Optional[str] = None) -> None:
38+
"""Example: Track an asyncio task lifecycle."""
39+
labels = {"task_name": task_name}
40+
if parent_task:
41+
labels["parent_task"] = parent_task
42+
43+
# Push task start event
44+
ddup.push_event("asyncio_task_start", labels=labels)
45+
46+
try:
47+
# Simulate some work
48+
await asyncio.sleep(0.1)
49+
50+
# Push checkpoint events as needed
51+
ddup.push_event("asyncio_task_checkpoint", labels={**labels, "checkpoint": "1"})
52+
53+
await asyncio.sleep(0.1)
54+
55+
finally:
56+
# Push task end event
57+
ddup.push_event("asyncio_task_end", labels=labels)
58+
59+
60+
def example_manual_sample_creation() -> None:
61+
"""Example: Manually create a sample with more control."""
62+
handle = ddup.SampleHandle()
63+
64+
# Push the event type (this makes it an Event sample)
65+
handle.push_event("custom_event")
66+
67+
# Add custom labels
68+
handle.push_label("label1", "value1")
69+
handle.push_label("label2", "value2")
70+
71+
# Optionally add stack frames manually
72+
import sys
73+
frame = sys._getframe(0)
74+
handle.push_frame("my_function", "my_file.py", 0, frame.f_lineno)
75+
76+
# Flush the sample to the profile
77+
handle.flush_sample()
78+
79+
80+
if __name__ == "__main__":
81+
# Initialize the profiler
82+
ddup.init(
83+
service="my-service",
84+
env="dev",
85+
version="1.0.0",
86+
)
87+
ddup.start()
88+
89+
# Run examples
90+
example_simple_event()
91+
example_event_with_stack()
92+
example_event_with_labels()
93+
example_event_with_limited_stack()
94+
example_manual_sample_creation()
95+
96+
# Run async example
97+
asyncio.run(track_asyncio_task("main_task"))
98+
99+
# Upload the profile
100+
ddup.upload()
101+

0 commit comments

Comments
 (0)