|
| 1 | +--- |
| 2 | +title: pole-instrument Technical Design |
| 3 | +tags: [architecture, python, instrumentation] |
| 4 | +links: [pole-python-monorepo, todo] |
| 5 | +updated: 2026-08-19 |
| 6 | +sources: 0 |
| 7 | +--- |
| 8 | + |
| 9 | +# pole-instrument Technical Design |
| 10 | + |
| 11 | +## Status and product boundary |
| 12 | + |
| 13 | +Proposed for implementation. Automatic instrumentation ships as the separate PyPI |
| 14 | +distribution `pole-instrument-python`, imported as `pole_instrument`, and depends on the |
| 15 | +public API of `pole-client-python`. The compatibility client remains unchanged: its wheel |
| 16 | +must not contain `sitecustomize.py`, `usercustomize.py`, `.pth` files, framework imports, |
| 17 | +or automatic patches. |
| 18 | + |
| 19 | +The console entry point is `pole-instrument`. The instrumentation core has no framework |
| 20 | +dependency; adapter dependencies are isolated in extras. Version 1 begins with one |
| 21 | +reference adapter, `httpx`, supporting HTTPX 0.27–0.28 sync and async clients. Server |
| 22 | +instrumentation, streaming bodies, gRPC, Dubbo, Thrift, and frameworks not listed here |
| 23 | +are unsupported in version 1. |
| 24 | + |
| 25 | +## Configuration |
| 26 | + |
| 27 | +Instrumentation is disabled by default. Configuration precedence is programmatic |
| 28 | +arguments, then command-line options, then environment variables, then defaults. |
| 29 | +`POLE_INSTRUMENT_ENABLED` is a boolean and defaults to false. |
| 30 | +`POLE_INSTRUMENT_STRICT` is a boolean and defaults to false. The enabled adapter list |
| 31 | +defaults to all installed supported adapters and may be set with |
| 32 | +`POLE_INSTRUMENT_ADAPTERS=httpx`. Boolean values are exactly `true`, `false`, `1`, or `0` |
| 33 | +after ASCII case folding; adapter names are ASCII lowercase tokens. |
| 34 | + |
| 35 | +Invalid values and unknown adapters produce an `invalid_configuration` diagnostic. |
| 36 | +The programmatic API raises `ConfigurationError`; the launcher exits with status 2. |
| 37 | +No framework patch or Sidecar session is created after invalid configuration. Strict |
| 38 | +mode affects request-time failures only and does not make invalid configuration valid. |
| 39 | + |
| 40 | +## Activation lifecycle |
| 41 | + |
| 42 | +`pole_instrument.activate(config=None)` is explicit and idempotent: repeated calls with |
| 43 | +equivalent configuration return the existing `Instrumentation` handle; conflicting |
| 44 | +configuration raises `AlreadyActiveError`. Activation after framework import is |
| 45 | +supported by patching the documented public HTTPX client send methods. Activation |
| 46 | +before import registers a bounded import callback and installs the same patch once. |
| 47 | + |
| 48 | +The handle's `shutdown()` is idempotent. It closes the owned `SidecarSession`, removes |
| 49 | +the import callback, and performs unpatch only when the currently installed method is |
| 50 | +the wrapper owned by that handle. Existing user wrappers and callbacks are preserved. |
| 51 | +The console launcher activates immediately before invoking the target module or command |
| 52 | +and always shuts down in `finally`. No session, channel, or worker thread is created at |
| 53 | +module import time. |
| 54 | + |
| 55 | +## Outbound HTTPX sequence |
| 56 | + |
| 57 | +The wrapper reads the original request URL before mutation. A DNS host supplies service; |
| 58 | +the optional `x-pole-target-namespace` request extension supplies namespace and defaults |
| 59 | +to `default`. IP literals, missing hosts, and values rejected by `TargetService` mean |
| 60 | +that no target can be derived. The adapter must not guess a target or listener port. |
| 61 | + |
| 62 | +For a valid target, the wrapper obtains one snapshot, reads |
| 63 | +`ListenerSnapshot.generation`, calls |
| 64 | +`SidecarSession.endpoint(ListenerProtocol.HTTP)`, constructs public |
| 65 | +`TargetService(namespace, service)`, and calls `TargetService.to_metadata()` with the |
| 66 | +request headers. That public call incorporates `current_traffic_context()` and replaces |
| 67 | +forged reserved metadata. The request authority and Host header continue to identify the |
| 68 | +original service while the network connection is made to the Sidecar listener. Calls |
| 69 | +made by the instrumentation package set a context-local recursion guard. |
| 70 | + |
| 71 | +The adapter maintains a transport pool per snapshot generation. Before acquiring a |
| 72 | +connection it compares the current `ListenerSnapshot.generation`; a change closes and |
| 73 | +discards the old pool atomically. `SidecarUnavailableError` also invalidates the pool |
| 74 | +before fallback. A request that has observed invalidation may not acquire from the old |
| 75 | +generation. In-flight requests may complete on the generation they already acquired. |
| 76 | + |
| 77 | +## Failure policy |
| 78 | + |
| 79 | +Default request behavior is fail open: missing socket, initialization timeout, invalid |
| 80 | +snapshot, stream loss, unavailable endpoint, target derivation failure, metadata |
| 81 | +validation failure, or adapter exception invokes the original HTTPX transport against |
| 82 | +the original URL without Pole target headers. Fallback has a bounded deadline and never |
| 83 | +waits for Sidecar reconnection. In strict mode the same conditions raise a documented |
| 84 | +`InstrumentationUnavailableError` before application I/O. Registration rejection is |
| 85 | +not applicable to the version 1 client-only adapter. Diagnostics identify the category |
| 86 | +and adapter but never include URL userinfo, target values, headers, baggage, or bodies. |
| 87 | + |
| 88 | +## Patch coexistence and concurrency |
| 89 | + |
| 90 | +Wrappers retain and invoke the method present at activation, so user middleware remains |
| 91 | +in the chain exactly once. Ownership markers prevent duplicate and recursive patches. |
| 92 | +OpenTelemetry may be installed before or after Pole; Pole changes routing headers only, |
| 93 | +does not create spans, and preserves its wrapper so both instrumentations execute once. |
| 94 | +Unsupported HTTPX versions remain unpatched and report `unsupported_version`. |
| 95 | + |
| 96 | +`SidecarSession` and sync pools are protected by process-local locks. Async pools belong |
| 97 | +to the event loop that created them and are never shared across loops. Activation calls |
| 98 | +`os.register_at_fork`: the child clears inherited session, lock, pool, and activation |
| 99 | +state without joining parent threads, then lazily creates a new session on its first |
| 100 | +instrumented request. Preload servers must activate in a post-fork worker hook when one |
| 101 | +is available. Shutdown closes sync resources and schedules async cleanup on the owning |
| 102 | +loop; if that event loop is already closed, resources are abandoned with one diagnostic |
| 103 | +rather than blocking process exit. |
| 104 | + |
| 105 | +## Diagnostics, security, and compatibility |
| 106 | + |
| 107 | +`pole_instrument.activation_status()` returns an immutable snapshot containing enabled, |
| 108 | +strict, installed adapter names, patch errors, Sidecar availability, and generation. It |
| 109 | +contains no request data. Structured diagnostics use stable event names, apply a rate |
| 110 | +limit of one event per category per minute with a suppressed count, and never record |
| 111 | +credentials, target metadata, baggage, arbitrary headers, URL query/userinfo, or request bodies. |
| 112 | +Bootstrap connects only to the configured local Unix Domain Socket accepted by |
| 113 | +`SidecarSession`; remote bootstrap transports are not added. |
| 114 | + |
| 115 | +Both distributions support Python 3.9–3.13. `pole-client-python` retains its current |
| 116 | +exports, dependencies, contracts, and artifact contents. The new distribution depends |
| 117 | +on a compatible client range and exposes HTTPX only through `[httpx]`; importing core |
| 118 | +with no framework or OpenTelemetry installed succeeds. Release gates inspect both |
| 119 | +artifacts and install client-only, instrument-core, and adapter-extra environments. |
| 120 | + |
| 121 | +## Verification plan |
| 122 | + |
| 123 | +- Document-contract tests require every settled decision and this traceability table. |
| 124 | +- Activation tests cover disabled import, configuration precedence/errors, idempotency, |
| 125 | + before/after-import patching, coexistence, recursion, and owned unpatch. |
| 126 | +- A real HTTPX request plus real UDS Sidecar fixture covers endpoint routing, canonical |
| 127 | + target/TrafficContext metadata, fail open, strict mode, stream loss, and generation |
| 128 | + pool replacement for sync and async clients. |
| 129 | +- Lifecycle tests cover threads, two event loops, forked workers, repeated shutdown, |
| 130 | + redacted/rate-limited diagnostics, and secret sentinels. |
| 131 | +- Compatibility gates run the existing Thin SDK suite unchanged, compile all sources, |
| 132 | + build both distributions, inspect artifacts, and smoke-test Python 3.9–3.13. |
| 133 | + |
| 134 | +## Traceability |
| 135 | + |
| 136 | +| Acceptance | Design section | Planned test | |
| 137 | +| --- | --- | --- | |
| 138 | +| AC1, AC13, AC14 | Product boundary; compatibility | artifact and isolated-import tests | |
| 139 | +| AC2, AC3 | Configuration; activation lifecycle | activation/config table tests | |
| 140 | +| AC4, AC5, AC6 | Outbound sequence; failure policy | real HTTPX plus UDS tests | |
| 141 | +| AC7 | Product boundary | explicit v1 server non-goal assertion | |
| 142 | +| AC8 | Product boundary | HTTPX support-matrix tests | |
| 143 | +| AC9 | Patch coexistence | user/OpenTelemetry ordering tests | |
| 144 | +| AC10 | Concurrency | fork, thread, and event-loop tests | |
| 145 | +| AC11, AC12 | Diagnostics and security | status, rate limit, secret-sentinel tests | |
| 146 | +| AC15 | Traceability | document-contract test | |
| 147 | + |
| 148 | +## Open decisions |
| 149 | + |
| 150 | +There are no open decisions required for the version 1 HTTPX client milestone. Adding |
| 151 | +server adapters or another protocol requires a new design revision, support matrix, and |
| 152 | +public-seam E2E evidence before implementation. |
0 commit comments