Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e5be733
feat(tracer-flare): add bindings for integration
anais-raison Nov 17, 2025
a6c86f1
Merge remote-tracking branch 'origin/main' into juanjux/bindings-trac…
juanjux Nov 18, 2025
cd7d1c7
Commit changes for backup
juanjux Nov 19, 2025
62cf43c
Simplify and improve. All tests passing
juanjux Nov 19, 2025
d735a8c
Restore
juanjux Nov 20, 2025
5dd11a8
restore comments
juanjux Nov 20, 2025
3f6165c
More reliable deletion of flare directory
juanjux Nov 20, 2025
550e483
restore
juanjux Nov 20, 2025
c22deae
Merge branch 'main' into juanjux/bindings-tracer-flare
juanjux Nov 20, 2025
2db9662
cargo fmt
juanjux Nov 21, 2025
5679894
Merge branch 'juanjux/bindings-tracer-flare' of https://github.com/Da…
juanjux Nov 21, 2025
984a41d
ruff fmt
juanjux Nov 21, 2025
a9c5e7f
Merge branch 'main' into juanjux/bindings-tracer-flare
juanjux Nov 21, 2025
d7a0a31
Merge branch 'main' into juanjux/bindings-tracer-flare
juanjux Nov 21, 2025
72b4d39
cargo clippy fixes
juanjux Nov 21, 2025
0da55d5
Merge branch 'juanjux/bindings-tracer-flare' of https://github.com/Da…
juanjux Nov 21, 2025
a5c0160
Merge branch 'main' into juanjux/bindings-tracer-flare
juanjux Nov 24, 2025
0923c12
fmt
juanjux Nov 24, 2025
def321e
Merge branch 'juanjux/bindings-tracer-flare' of https://github.com/Da…
juanjux Nov 24, 2025
fc968e9
fmt
juanjux Nov 24, 2025
17e002c
mypy fix
juanjux Nov 24, 2025
a91aa76
fmt
juanjux Nov 24, 2025
97f3921
Merge remote-tracking branch 'origin/main' into juanjux/bindings-trac…
juanjux Nov 25, 2025
7508714
Remove get_connection after merge
juanjux Nov 25, 2025
1e6d46c
fmt
juanjux Nov 25, 2025
c9ccdb2
Merge branch 'main' into juanjux/bindings-tracer-flare
juanjux Nov 25, 2025
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
1 change: 1 addition & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"WebFetch(domain:github.com)",
"Skill(run-tests)",
"Bash(scripts/run-tests:*)",
"Bash(pytest:*)"
"Bash(gh pr list:*)",
"Bash(git remote:*)"
],
Expand Down
118 changes: 80 additions & 38 deletions ddtrace/internal/flare/flare.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ddtrace.internal.flare.json_formatter import StructuredJSONFormatter
from ddtrace.internal.logger import get_logger
from ddtrace.internal.native._native import logger as native_logger
from ddtrace.internal.utils.http import get_connection
from ddtrace.internal.native._native import register_tracer_flare as native_flare # type: ignore


TRACER_FLARE_DIRECTORY = "tracer_flare"
Expand Down Expand Up @@ -61,6 +61,13 @@ def __init__(
# Use a fixed boundary for consistency
self._BOUNDARY = "83CAD6AA-8A24-462C-8B3D-FF9CC683B51B"

# Initialize native manager eagerly to fail fast if there's an issue
try:
self._native_manager = native_flare.TracerFlareManager(agent_url=self.url, language="python")
except Exception as e:
log.error("Failed to initialize native tracer flare manager: %s", e)
raise

def prepare(self, log_level: str) -> bool:
"""
Update configurations to start sending tracer logs to a file
Expand All @@ -72,6 +79,10 @@ def prepare(self, log_level: str) -> bool:
log.error("Flare prepare: failed to create %s directory: %s", self.flare_dir, e)
return False

if not isinstance(log_level, str):
log.error("Flare prepare: Invalid log level provided: %s (must be a string)", log_level)
return False

flare_log_level_int = getattr(logging, log_level.upper(), None)
if flare_log_level_int is None or not isinstance(flare_log_level_int, int):
log.error("Flare prepare: Invalid log level provided: %s", log_level)
Expand Down Expand Up @@ -101,26 +112,31 @@ def send(self, flare_send_req: FlareSendRequest):

def _generate_config_file(self, pid: int):
config_file = self.flare_dir / f"tracer_config_{pid}.json"

# Redact API key if present
api_key = self.ddconfig.get("_dd_api_key")
if api_key:
self.ddconfig["_dd_api_key"] = "*" * (len(api_key) - 4) + api_key[-4:]

tracer_configs = {
"configs": self.ddconfig,
}

config_json = json.dumps(
tracer_configs,
default=lambda obj: obj.__repr__() if hasattr(obj, "__repr__") else obj.__dict__,
)

try:
with open(config_file, "w") as f:
# Redact API key if present
api_key = self.ddconfig.get("_dd_api_key")
if api_key:
self.ddconfig["_dd_api_key"] = "*" * (len(api_key) - 4) + api_key[-4:]

tracer_configs = {
"configs": self.ddconfig,
}
json.dump(
tracer_configs,
f,
default=lambda obj: obj.__repr__() if hasattr(obj, "__repr__") else obj.__dict__,
indent=4,
)
self._native_manager.write_config_file(str(config_file), config_json)
except Exception as e:
log.warning("Failed to generate %s: %s", config_file, e)
log.warning("Failed to write config file %s: %s", config_file, e)
# Clean up partial file if it exists
if os.path.exists(config_file):
os.remove(config_file)
try:
os.remove(config_file)
except Exception as cleanup_error:
log.debug("Failed to clean up partial config file: %s", cleanup_error)

def revert_configs(self):
ddlogger = get_logger("ddtrace")
Expand Down Expand Up @@ -221,7 +237,7 @@ def _generate_payload(self, flare_send_req):
Generate the multipart form-data payload for the flare request.
"""

# Create the multipart form data in the same order:
# Create the multipart form data in the same order as the .NET implementation:
# source, case_id, hostname, email, uuid, flare_file
body = io.BytesIO()
self._write_body_field(body, "source", "tracer_python")
Expand Down Expand Up @@ -265,30 +281,56 @@ def _send_flare_request(self, flare_send_req: FlareSendRequest):
lock_path = self.flare_dir / TRACER_FLARE_LOCK
if not os.path.exists(lock_path):
open(lock_path, "w").close()
client = None

# Collect all files in the flare directory
files_to_send = [str(f) for f in self.flare_dir.iterdir() if f.is_file()]

# Create AgentTaskFile for the send action
try:
client = get_connection(self.url, timeout=self.timeout)
headers, body = self._generate_payload(flare_send_req)
client.request("POST", TRACER_FLARE_ENDPOINT, body, headers)
response = client.getresponse()
if response.status == 200:
log.info("Successfully sent the flare to Zendesk ticket %s", flare_send_req.case_id)
else:
msg = "Tracer flare upload responded with status code %s:(%s) %s" % (
response.status,
response.reason,
response.read().decode(),
)
raise TracerFlareSendError(msg)
# Convert case_id to integer, handling test patterns
case_id_int = (
int(flare_send_req.case_id.split("-")[0])
if "-" in flare_send_req.case_id
else int(flare_send_req.case_id)
)

agent_task = native_flare.AgentTaskFile(
case_id=case_id_int,
hostname=flare_send_req.hostname,
user_handle=flare_send_req.email,
task_type="tracer_flare",
uuid=flare_send_req.uuid,
)

# Create ReturnAction.Send
send_action = native_flare.ReturnAction.send(agent_task)
except Exception as e:
log.error("Failed to create flare send request: %s", e)
raise

# Use native zip_and_send
try:
self._native_manager.zip_and_send(files_to_send, send_action)
log.info("Successfully sent the flare to Zendesk ticket %s", flare_send_req.case_id)
except Exception as e:
log.error("Failed to send tracer flare to Zendesk ticket %s: %s", flare_send_req.case_id, e)
raise e
finally:
if client is not None:
client.close()
raise

def clean_up_files(self):
def _cleanup_directory_python(self):
"""Clean up the flare directory using Python's shutil."""
try:
shutil.rmtree(self.flare_dir)
except Exception as e:
log.warning("Failed to clean up tracer flare files: %s", e)

def clean_up_files(self):
# Use native implementation with Python fallback
try:
self._native_manager.cleanup_directory(str(self.flare_dir))
# Check if directory was actually deleted
if self.flare_dir.exists():
log.debug("Native cleanup succeeded but directory still exists, cleaning up with Python")
self._cleanup_directory_python()
except Exception as e:
log.debug("Native cleanup failed, falling back to Python: %s", e)
self._cleanup_directory_python()
Loading
Loading