Skip to content
Merged
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
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Changelog

Notable changes to wildedge-sdk. Every behavior change lands here; entries go
under Unreleased and move into a version section at release time.

## Unreleased

### Added

- Process-wide default client: `wildedge.get_client()`, `wildedge.set_default_client()`, and module-level `trace`, `span`, `track_span`, `register_model`, `flush` delegating to it (#41)
- `init()` reuses the default client installed by `wildedge run` unless `dsn` is passed, so CLI and in-process init share one client (#41)
- `SpanContextManager.set_attributes()` and `fail()` for recording outcomes on an open span (#41)
- `wildedge run --strict` (env `WILDEDGE_STRICT`): exit with a reserved code instead of running untracked when bootstrap fails (120 config error, 122 internal error)
- Lazy default-client creation honors `WILDEDGE_INTEGRATIONS` and `WILDEDGE_HUBS` from the environment (#41)
- `docs/deployment.md`: the deployment contract (no-DSN behavior, strict mode, fork/exec servers, one client per process)

### Changed

- `--strict-integrations` now takes effect: a failed required integration exits the process with code 121. Previously the enforcing code path was never invoked, so the flag was silently ignored.
- `--print-startup-report` and `--no-propagate` now work under `wildedge run`; both were only wired to the unused runner path before.
- Constructing a client without a DSN logs once per process at INFO. Previously every construction logged a WARNING.

### Removed

- `wildedge.runtime.runner`: an alternative bootstrap entry point that `wildedge run` never invoked. `sitecustomize` is the single bootstrap path; the runner's exit codes moved to `wildedge.runtime.bootstrap` and now apply under `--strict`.

## 0.1.5 - 2026-06-23

- Opt-in inference attachments (raw input/output upload)
- Fixed accelerator detection wiring; macOS CPU frequency and thermal sampling

## 0.1.4 and earlier

Predate this changelog; see the git history.
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
1. Fork the repository and create a feature branch off `devel`.
2. Make your changes and ensure tests pass.
3. Update documentation if needed.
4. Submit a pull request targeting `devel` with a clear description of the changes.
4. Add a `CHANGELOG.md` entry under Unreleased for any user-visible or behavioral change.
5. Submit a pull request targeting `devel` with a clear description of the changes.

## Release process

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ Useful flags:
| `--integrations` | Comma-separated list of integrations to activate (or `all`) |
| `--hubs` | Hub trackers to activate: `huggingface`, `torchhub` |
| `--print-startup-report` | Print per-integration status at startup |
| `--strict-integrations` | Fail if a requested integration can't be loaded |
| `--strict-integrations` | Exit (code 121) if a requested integration can't be instrumented |
| `--strict` | Exit (120 config, 122 internal) instead of running untracked when bootstrap fails |
| `--attachments` | Enable opt-in raw input/output attachment upload |
| `--no-propagate` | Don't pass WildEdge env vars to child processes |

Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Configuration

Full reference for all `WildEdge` client parameters.
Full reference for all `WildEdge` client parameters. For runtime behavior
(no-DSN mode, strict mode and exit codes, forking servers), see
[Deployment](deployment.md).

## Core

Expand Down
101 changes: 101 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Deployment

How the SDK behaves in real process models: servers that fork, workers that
exec, build steps, and processes with no DSN configured. Everything on this
page is a contract the SDK keeps, not an implementation detail.

## How initialization happens

There are three ways a process gets its client, in order of coverage:

| Mode | Instrumentation coverage | Use when |
|---|---|---|
| `wildedge run -- <cmd>` | Guaranteed: patches before any user code runs | You control the start command |
| `wildedge.init(...)` at startup | Everything created after the call | You control application code |
| Lazy, from the first module-level call | Best effort: only what is created afterwards | Manual tracking only |

All three produce the same thing: one process-wide default client that
`wildedge.span()`, `wildedge.register_model()` and the rest delegate to.
`init()` without `dsn` reuses whatever client already exists, so code written
for in-process init runs unchanged under `wildedge run`.

The lazy mode configures itself from the environment: `WILDEDGE_DSN`,
`WILDEDGE_INTEGRATIONS`, `WILDEDGE_HUBS`. Unset integration variables mean
nothing is patched; auto-instrumentation is always opt-in.

## The no-DSN contract

Without `WILDEDGE_DSN`, the client is a no-op: no background threads, no
network, no patched frameworks, every event dropped. This is a supported mode
for development and CI, not an error; the SDK logs one INFO line per process
and stays quiet. Leave the SDK integrated and unset the variable wherever you
do not want telemetry (local dev, test runs, build and migration steps).

Under `wildedge run`, a missing DSN warns on stderr and your program runs
untracked. Telemetry failing must not take production down; that is the
default policy.

## Strict mode

`wildedge run --strict` inverts the failure policy for deployments where
running unobserved is worse than not running. Bootstrap failures then
terminate the process before your program starts:

| Exit code | Meaning |
|---|---|
| 120 | Configuration error (missing or invalid DSN) |
| 121 | A requested integration could not be instrumented (requires `--strict-integrations`) |
| 122 | Internal bootstrap error |

`--strict-integrations` is its own opt-in and exits with 121 on failure even
without `--strict`; asking for it is the request to fail.

## Forking and multi-process servers

`wildedge run` prepends `wildedge/autoload/` to `PYTHONPATH` and replaces
itself with your command. Every Python interpreter that starts under it runs
the bundled `sitecustomize.py`, which bootstraps the runtime before any user
code. Two mechanisms make this safe across worker models:

- Fresh interpreters (exec or spawn) bootstrap themselves via sitecustomize;
a `sys.modules` marker prevents double initialization within one
interpreter.
- Forked children inherit the parent's initialized runtime, and
`os.register_at_fork` hooks stop the SDK's background threads before each
fork and start fresh ones in parent and child afterward, so forked workers
transmit normally instead of inheriting dead threads.

How that plays out on common servers:

| Server | Worker model | Behavior under `wildedge run` |
|---|---|---|
| gunicorn (sync/gthread, with or without `preload_app`) | fork from master | Master bootstraps once; each worker gets fresh SDK threads via the at-fork hooks |
| uvicorn (`--workers`, `--reload`) | spawn / exec | Every worker is a fresh interpreter and bootstraps itself |
| granian | spawned worker processes | Each worker bootstraps itself |
| daphne | single process | One bootstrap, nothing special |
| waitress | single process, thread pool | One bootstrap; the client is thread-safe |
| celery (prefork pool) | fork from master | Same as gunicorn: at-fork hooks restart threads per worker |

For managed platforms, wrap only the serving command. Build steps and
migrations run in separate processes that gain nothing from telemetry; leave
them unwrapped or unset `WILDEDGE_DSN` there.

## One client per process

- Framework patches are installed at most once per process; the first client
to instrument owns the patch.
- `init()` reuses the existing default client unless you pass `dsn`
explicitly, so the CLI-installed client and application code share one
client instead of racing.
- Trace and span correlation lives in contextvars at module level, not on a
client. Auto-instrumented events emitted inside `wildedge.trace(...)` /
`wildedge.span(...)` blocks correlate into the same trace no matter which
client emits them.

## Environment propagation

By default, `wildedge run` leaves its `WILDEDGE_*` variables in the
environment so that exec'd children (reload workers) can bootstrap. Pass
`--no-propagate` to have each bootstrapped process scrub the run-scoped
variables after initialization, keeping them away from nested subprocesses
you spawn yourself.
150 changes: 150 additions & 0 deletions tests/test_autoload.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,153 @@ def test_uvicorn_reload_worker_bootstraps(tmp_path):
)
assert result.returncode == 0, result.stderr
assert "installed: True" in result.stdout


def test_strict_missing_dsn_exits_120(monkeypatch):
from wildedge.runtime.bootstrap import RuntimeConfigError

monkeypatch.delitem(sys.modules, _MARKER, raising=False)
monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1")
monkeypatch.setenv("WILDEDGE_STRICT", "1")
monkeypatch.delenv("WILDEDGE_DSN", raising=False)

with (
patch(
"wildedge.runtime.bootstrap.install_runtime",
side_effect=RuntimeConfigError("WILDEDGE_DSN must be set"),
),
patch("os._exit") as fake_exit,
):
_reload_sitecustomize()

fake_exit.assert_called_once_with(120)


def test_strict_integration_error_exits_121_without_strict_env(monkeypatch):
"""--strict-integrations is its own opt-in; it exits even without --strict."""
from wildedge.runtime.bootstrap import RuntimeStrictIntegrationError

monkeypatch.delitem(sys.modules, _MARKER, raising=False)
monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1")
monkeypatch.delenv("WILDEDGE_STRICT", raising=False)

with (
patch(
"wildedge.runtime.bootstrap.install_runtime",
side_effect=RuntimeStrictIntegrationError("strict fail"),
),
patch("os._exit") as fake_exit,
):
_reload_sitecustomize()

fake_exit.assert_called_once_with(121)


def test_strict_internal_error_exits_122(monkeypatch):
monkeypatch.delitem(sys.modules, _MARKER, raising=False)
monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1")
monkeypatch.setenv("WILDEDGE_STRICT", "1")

with (
patch(
"wildedge.runtime.bootstrap.install_runtime",
side_effect=RuntimeError("boom"),
),
patch("os._exit") as fake_exit,
):
_reload_sitecustomize()

fake_exit.assert_called_once_with(122)


def test_non_strict_config_error_warns_and_continues(monkeypatch, capsys):
from wildedge.runtime.bootstrap import RuntimeConfigError

monkeypatch.delitem(sys.modules, _MARKER, raising=False)
monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1")
monkeypatch.delenv("WILDEDGE_STRICT", raising=False)

with patch(
"wildedge.runtime.bootstrap.install_runtime",
side_effect=RuntimeConfigError("no dsn"),
):
_reload_sitecustomize() # must not raise

assert "running without telemetry" in capsys.readouterr().err


def test_startup_report_printed_from_env(monkeypatch, capsys):
class FakeContext:
debug = False
print_startup_report = False
integration_statuses: list = []

monkeypatch.delitem(sys.modules, _MARKER, raising=False)
monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1")
monkeypatch.setenv("WILDEDGE_PRINT_STARTUP_REPORT", "1")

with (
patch(
"wildedge.runtime.bootstrap.install_runtime",
return_value=FakeContext(),
),
patch(
"wildedge.runtime.bootstrap.format_startup_report",
return_value="report-text",
),
):
_reload_sitecustomize()

assert "report-text" in capsys.readouterr().err


def test_no_propagate_clears_runtime_env(monkeypatch):
import os

class FakeContext:
debug = False
print_startup_report = False

monkeypatch.delitem(sys.modules, _MARKER, raising=False)
monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1")
monkeypatch.setenv("WILDEDGE_PROPAGATE", "0")
monkeypatch.setenv("WILDEDGE_INTEGRATIONS", "all")

with patch(
"wildedge.runtime.bootstrap.install_runtime",
return_value=FakeContext(),
):
_reload_sitecustomize()

assert "WILDEDGE_AUTOLOAD" not in os.environ
assert "WILDEDGE_INTEGRATIONS" not in os.environ


def test_strict_missing_dsn_exit_code_in_real_interpreter(tmp_path):
"""sys.exit inside sitecustomize must terminate interpreter startup with our code."""
import os
import pathlib

autoload_dir = str(
pathlib.Path(
importlib.util.find_spec("wildedge.autoload.sitecustomize").origin
).parent
)

env = os.environ.copy()
env["WILDEDGE_AUTOLOAD"] = "1"
env["WILDEDGE_STRICT"] = "1"
env.pop("WILDEDGE_DSN", None)
pythonpath = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = autoload_dir + (os.pathsep + pythonpath if pythonpath else "")

result = subprocess.run(
[sys.executable, "-c", "print('should not run')"],
env=env,
capture_output=True,
text=True,
)

assert result.returncode == 120, (result.returncode, result.stderr)
assert "should not run" not in result.stdout
assert "WILDEDGE_DSN must be set" in result.stderr
Loading
Loading