Forwarders¶
Publishing captured protocol exchanges to a monitoring system.
py20305.forwarders.types ¶
The message format this client publishes captured traffic in.
:class:ProtocolMessage wraps one IEEE 2030.5 exchange -- request or
response, with its endpoints, timing and payload -- in a protocol-agnostic
envelope. The forwarder publishes these; a monitoring system subscribes to
them. Nothing in the client's own operation depends on this module, so a
deployment that does not forward anything never touches it.
The envelope is deliberately more general than 2030.5 needs, because it is
consumed by systems that also ingest other protocols and want one shape for
all of them. That is why :class:ProtocolMetadata carries a free-form
extra dict and why every optional field is omitted from the serialized
form rather than emitted as null: consumers match on presence, and adding a
field is a compatible change.
Serialization contract, as emitted by :meth:ProtocolMessage.to_dict:
version,protocol,direction,timestamp,client_id,forwarder_id,payload,source,hashandis_validare always present.destination,protocol_dataandvalidation_errorappear only when set.timestampis ISO 8601 with an offset. A trailingZis rewritten to+00:00on construction.hashis a deterministic UUIDv3 over protocol, client id, timestamp and payload, so a consumer can deduplicate replays without coordinating with the producer.
WireDirection ¶
Bases: StrEnum
Which way the message was travelling, as carried on the wire.
UPSTREAM is device toward server; DOWNSTREAM is server toward
device.
Deliberately not named MessageDirection:
:mod:py20305.forwarders.base defines a capture-side enum by
that name with the same members, and two same-named enums compare unequal
across modules without any error to say so. Distinct names make the
conversion at the boundary explicit.
Source code in src/py20305/forwarders/types.py
Protocol ¶
Bases: StrEnum
The application protocol a message was captured from.
Source code in src/py20305/forwarders/types.py
from_string
classmethod
¶
Parse a protocol from its wire value.
Source code in src/py20305/forwarders/types.py
PayloadEnvelope
dataclass
¶
A payload together with what it is, so a consumer need not guess.
2030.5 bodies are XML, but a forwarded message may also carry JSON or an
opaque blob, and the consumer has to tell them apart without sniffing.
Construct through the from_* factories rather than directly.
Source code in src/py20305/forwarders/types.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | |
from_dict
classmethod
¶
from_xml
classmethod
¶
from_binary
classmethod
¶
Wrap raw bytes, base64-encoded so the envelope stays JSON-safe.
Source code in src/py20305/forwarders/types.py
from_text
classmethod
¶
Wrap plain text, or text of a caller-specified content type.
infer_from_string
classmethod
¶
Guess the content type of a string payload.
Tries JSON, then XML, then falls back to plain text. Only for callers
holding a string with no type information; prefer a from_*
factory whenever the type is known.
Source code in src/py20305/forwarders/types.py
to_dict ¶
Parse a JSON payload back to a dict, or None if it isn't one.
Source code in src/py20305/forwarders/types.py
to_bytes ¶
Return the payload as bytes, decoding base64 when that's the encoding.
to_string ¶
serialize ¶
deserialize
classmethod
¶
Rebuild from a :meth:serialize result.
Source code in src/py20305/forwarders/types.py
NetworkEndpoint
dataclass
¶
One end of the connection a message was seen on.
mac is carried because the format defines it and a capture-side
producer populates it from the L2 frame. This client sees TCP sessions
rather than frames, so it leaves it unset -- but a message read back from
the wire has to preserve it rather than silently drop it.
Source code in src/py20305/forwarders/types.py
ProtocolMetadata
dataclass
¶
Protocol-level detail about a message, beyond its bytes.
Every field is optional and omitted from the serialized form when unset,
so a consumer reads what the producer knew rather than a wall of nulls.
extra carries anything this class does not name.
Source code in src/py20305/forwarders/types.py
to_dict ¶
Serialize, omitting unset fields and empty containers.
Source code in src/py20305/forwarders/types.py
from_dict
classmethod
¶
Rebuild from a :meth:to_dict result.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/py20305/forwarders/types.py
ProtocolMessage
dataclass
¶
One captured protocol exchange, ready to publish.
Construct it with whatever you have -- payload accepts a
:class:PayloadEnvelope, a dict, a list or a string, and is normalized
to an envelope on construction -- then call :meth:to_dict or
:meth:to_json.
Source code in src/py20305/forwarders/types.py
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 | |
to_dict ¶
Serialize for transport. See the module docstring for the contract.
Source code in src/py20305/forwarders/types.py
to_json ¶
from_dict
classmethod
¶
Rebuild a message from a :meth:to_dict result.
The inverse of the serialization contract, so a consumer can read
what a producer wrote without hand-unpacking the envelope. Unknown
keys under protocol_data survive the round trip via
:attr:ProtocolMetadata.extra, which is what lets a consumer built
against this version read a message from a later one.
Metadata keys this class does not model are preserved in
:attr:ProtocolMetadata.passthrough and re-emitted where they were
found, so a message from a producer modelling more fields keeps its
shape rather than having them folded a level deeper.
hash is taken from the input rather than recomputed. It is the
producer's deduplication key, and recomputing it here would silently
change it whenever a field this class does not model was dropped.
Raises:
| Type | Description |
|---|---|
KeyError
|
If a key the contract says is always present is missing. |
Source code in src/py20305/forwarders/types.py
from_json
classmethod
¶
py20305.forwarders.manager ¶
Forwarder manager for routing messages to multiple forwarders.
The ForwarderManager acts as a facade that routes MessageFrame objects to one or more registered forwarders (MQTT, file, console, etc.).
ForwarderManager ¶
Routes messages to multiple forwarders.
The manager handles: - Registration of forwarders - Lifecycle management (start/stop all forwarders) - Message routing to all registered forwarders - Aggregated statistics
Usage
manager = ForwarderManager() manager.add_forwarder(mqtt_adapter) await manager.start()
Queue messages - they're routed to all forwarders¶
manager.queue_message(frame)
await manager.stop()
Source code in src/py20305/forwarders/manager.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
can_deliver_events
property
¶
Return whether an event queued now would reach at least one forwarder.
A forwarder counts when it started and, if it reports live readiness
through can_deliver, is ready now. A forwarder that does not report
it counts as ready once started.
connection_observer
property
writable
¶
Where forwarders report their own broker connection outcomes.
client_lfdi
property
writable
¶
Return this client's own LFDI, used as an identifier fallback.
source_host
property
writable
¶
Return source hostname for forwarded messages.
add_forwarder ¶
Register a forwarder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forwarder
|
BaseForwarder
|
Forwarder instance to add |
required |
Source code in src/py20305/forwarders/manager.py
remove_forwarder ¶
Unregister a forwarder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forwarder
|
BaseForwarder
|
Forwarder instance to remove |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if removed, False if not found |
Source code in src/py20305/forwarders/manager.py
start
async
¶
Start all registered forwarders.
Forwarders that fail to start are logged but don't prevent other forwarders from starting.
Source code in src/py20305/forwarders/manager.py
stop
async
¶
Stop all registered forwarders gracefully.
Source code in src/py20305/forwarders/manager.py
queue_message ¶
Route a message to all registered forwarders.
Messages are queued to each forwarder. Errors in one forwarder don't affect others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
MessageFrame
|
Message frame to route |
required |
Source code in src/py20305/forwarders/manager.py
failed_forwarders ¶
Registered forwarders whose start raised and has not since succeeded.
A forwarder whose broker was unreachable at startup drops everything handed to it, while the manager reports itself running -- so "the manager started" is not the same question as "anything is being delivered", and a caller retrying needs the second.
Recorded when the start fails rather than read back off the forwarder:
BaseForwarder does not require a running attribute, so asking
for one would classify any implementation without it as healthy and
never retry the very forwarders this exists for.
Source code in src/py20305/forwarders/manager.py
retry_failed
async
¶
Try to start the forwarders that are not running. Returns how many came up.
Separate from :meth:start so a caller can drive it on whatever
cadence suits it. Without this a broker that is briefly unreachable
when the client boots -- which is exactly when a broker restarted
alongside it would be -- silently disables forwarding for the lifetime
of the process.
Source code in src/py20305/forwarders/manager.py
queue_event ¶
Route an event to all registered forwarders.
Mirrors :meth:queue_message: non-blocking, and one forwarder's
failure does not affect the others. Forwarders that do not carry
events drop them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event
|
EventFrame
|
Event frame to route |
required |
Source code in src/py20305/forwarders/manager.py
queue_telemetry ¶
Route measured device state to all registered forwarders.
A forwarder that does not carry telemetry drops it (the default
implementation on AbstractForwarder), so this is safe to call
regardless of which forwarders are registered.
Source code in src/py20305/forwarders/manager.py
get_statistics ¶
Return aggregated statistics from all forwarders.
Source code in src/py20305/forwarders/manager.py
from_config
async
classmethod
¶
Create and configure a ForwarderManager from config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
ForwarderConfig
|
Forwarder configuration |
required |
Returns:
| Type | Description |
|---|---|
ForwarderManager
|
Configured ForwarderManager instance |
Source code in src/py20305/forwarders/manager.py
py20305.forwarders.config ¶
Configuration for message forwarders.
Configuration is loaded from the config file's forwarders map. Each forwarder type is an optional typed field on ForwarderConfig.
MQTTForwarderConfig ¶
Bases: _StrictForwarderModel
Configuration for MQTT message forwarding.
Supports both mTLS connections (e.g., AWS IoT Core) and plain MQTT brokers. When cert_path/key_path/ca_path are all omitted, connects without TLS. When any cert path is provided, all three are required and validated.
Source code in src/py20305/forwarders/config.py
validate_cert_paths ¶
Validate certificate file configuration.
If all cert paths are None, TLS is disabled (plain MQTT). If any cert path is provided, all three must be present and point to existing files.
Source code in src/py20305/forwarders/config.py
ConnectionTelemetryConfig ¶
Bases: _StrictForwarderModel
Configuration for reporting this client's own connection outcomes.
Off by default: an operator who has not asked for connection telemetry should not start publishing it on an upgrade.
Source code in src/py20305/forwarders/config.py
DeviceTelemetryConfig ¶
Bases: _StrictForwarderModel
Configuration for reporting southbound device reads and writes.
Off by default: an operator who has not asked for southbound telemetry should not start publishing their device traffic on an upgrade.
Source code in src/py20305/forwarders/config.py
validate_topic_suffix
classmethod
¶
Normalize the suffix, allowing empty to mean "the default topic".
Wildcards are rejected: they are subscription filters, not topic
names, and a suffix like out/# would validate here and then
publish to a literal-# topic no subscriber matches.
Source code in src/py20305/forwarders/config.py
AuditConfig ¶
Bases: _StrictForwarderModel
Configuration for the control audit trail.
One switch for both halves: device writes, enriched with what caused them, and the event lifecycle and loss-of-communications records on their own topic. Off by default, like the other telemetry streams.
Writes are published without the high-volume reads, so an operator can keep an audit trail without turning on full device telemetry. With both on, each write is still published once.
Source code in src/py20305/forwarders/config.py
ForwarderConfig ¶
Bases: _StrictForwarderModel
Top-level forwarder configuration.
Each forwarder type is an optional typed field. This replaces the previous list-of-ForwarderSpec approach with named fields.
Source code in src/py20305/forwarders/config.py
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | |
validate_audit_topic_is_its_own ¶
Keep the lifecycle records off every other stream's topic.
Checked whether or not the other streams are enabled: turning one on later must not quietly start mixing two payload shapes on one topic.
Source code in src/py20305/forwarders/config.py
validate_telemetry_topics_differ ¶
Keep the two telemetry streams off one another's topic.
They carry different payload shapes -- OCSF connection events and protocol-message envelopes -- and a consumer reading one would silently receive the other mixed in. Device telemetry's empty default means the protocol-message topic, so the comparison is on effective topics.
Source code in src/py20305/forwarders/config.py
has_enabled_forwarders ¶
py20305.forwarders.connection_telemetry ¶
Connection telemetry -- reporting this client's own connection outcomes.
The forwarder already carries this client's captured IEEE 2030.5 exchanges to a security-monitoring system, and the device-telemetry channel carries its southbound reads and writes. This module adds the third channel: the client's own connection outcomes. A network sensor on a span port can approximate the connection attempts a monitoring platform wants logged. It cannot produce the error log at all -- a certificate that fails validation, a redirect loop, a 500 from the server are outcomes this client knows and a capture cannot recover from inside TLS.
This module turns those outcomes into OCSF Network Activity events (the
vendored ocsf module beside this one) and publishes them on their own
topic. What lives here is the mapping from this client's error taxonomy onto
the schema, and the coalescing the volume of a polling client requires.
Two rules come from the compliance argument rather than from the schema, and the event class enforces both at construction:
- A failure carries its reason. That reason is the record's entire value.
- Failures are never coalesced. Successes may be collapsed into a window carrying bounds and a count; collapsing failures would discard the reasons.
The emitter implements the client's
:class:~py20305.client.observer.ConnectionObserver seam, so wiring it up is
one assignment: client.http.connection_observer = emitter.
A host application can report its other links through the same emitter:
:meth:ConnectionTelemetryEmitter.record_connection and
:meth:ConnectionTelemetryEmitter.record_connection_failure take the link's
role, service, direction and endpoints. Every event carries one
role:<role> label in metadata.labels (see :class:ConnectionRole)
and the connection's direction in connection_info.direction_id, so a
consumer can tell the links apart.
ConnectionRole ¶
Bases: StrEnum
Which of the host application's links a connection event describes.
Serialized as one role:<value> label in metadata.labels (see
:func:role_label). A consumer keys on these strings, so they are part of
the wire contract: add a member rather than renaming one.
Source code in src/py20305/forwarders/connection_telemetry.py
Window
dataclass
¶
A closed window of successful connections, ready to emit.
Attributes:
| Name | Type | Description |
|---|---|---|
count |
int
|
Attempts the window represents. |
start_ms |
int
|
First attempt, as an OCSF timestamp. |
end_ms |
int
|
Last attempt, as an OCSF timestamp. |
src |
Endpoint | None
|
The source endpoint observed when the window opened, or the first one seen inside it. Carried with the window rather than re-read at emission, because the success that closes a window belongs to a different attempt than the one that opened it. |
dst |
Endpoint | None
|
The destination endpoint observed together with |
Source code in src/py20305/forwarders/connection_telemetry.py
Outcome
dataclass
¶
A connection outcome classified for OCSF.
Attributes:
| Name | Type | Description |
|---|---|---|
activity_id |
NetworkActivityId
|
|
detail |
str
|
Why it happened. Required on every failure. |
status_code |
str | None
|
The peer's own code, when it gave one. |
Source code in src/py20305/forwarders/connection_telemetry.py
CoalescingWindow ¶
Collapses successful connections on one link into one event per window.
A polling client would otherwise put more volume on the collector's flow path than the passive capture beside it. A coalesced record still has to answer the question a connection log asks -- was there an attempt at time T -- so it carries the window's bounds and the attempt count, and every attempt in it shared one link: role, direction, service and peer. It is not a sample.
Only successes are coalesced. Failures never enter here.
A window keeps the endpoints observed when it opened. The success that closes a window is a different attempt from the one that opened it, so reading the current endpoints at close time would report a connection that carried none of the successes being described. A coalesced record therefore names the source port of the first connection in the window only.
Not synchronized: every caller runs on the event loop, and none of the methods here await, so a window update cannot be interleaved with another.
Source code in src/py20305/forwarders/connection_telemetry.py
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 | |
record ¶
Record one success, returning a window to emit when one closes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
now_ms
|
int
|
The time of the success, in epoch milliseconds. |
required |
src
|
Endpoint | None
|
The source endpoint of the connection this success was carried on, when it is known. |
None
|
dst
|
Endpoint | None
|
The destination endpoint of that connection, when it is known. |
None
|
Returns:
| Type | Description |
|---|---|
Window | None
|
The closed window when this success closed one, otherwise |
Window | None
|
The returned window never includes the success that closed it -- |
Window | None
|
that one opens the next window, so no attempt is counted twice or |
Window | None
|
dropped -- and it carries the endpoints observed when it opened, |
Window | None
|
not the ones live at close time. |
Source code in src/py20305/forwarders/connection_telemetry.py
take_expired ¶
Close the window if its configured length has elapsed.
The window normally closes when the success after it arrives. A link whose successes stop -- it starts failing, or goes idle -- would otherwise hold its last successes unreported until shutdown, so other activity through the emitter offers the clock a chance to close an expired window.
Returns:
| Type | Description |
|---|---|
Window | None
|
The expired window, or |
Window | None
|
still inside its length. |
Source code in src/py20305/forwarders/connection_telemetry.py
flush ¶
Close the open window, if any.
Called on shutdown so the successes accumulated since the last close are reported rather than discarded -- an unreported attempt is a gap in the connection log.
Returns:
| Type | Description |
|---|---|
Window | None
|
The pending window, or |
Source code in src/py20305/forwarders/connection_telemetry.py
LinkObserver ¶
Bases: Protocol
Receives connection outcomes from the links this package opens or accepts.
:class:ConnectionTelemetryEmitter implements it. A host application can
put its own object in front of the emitter, for example to replace the
emitter at runtime or to pass calls from worker threads to the event loop.
Implementations must never raise.
Source code in src/py20305/forwarders/connection_telemetry.py
record_connection ¶
record_connection(*, role: ConnectionRole | str, service: str | None, direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, dst: Endpoint | None = None, src: Endpoint | None = None, url: str | None = None, now_ms: int | None = None) -> None
Record one successful connection.
Source code in src/py20305/forwarders/connection_telemetry.py
record_connection_failure ¶
record_connection_failure(*, role: ConnectionRole | str, service: str | None, reason: BaseException | str, direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, dst: Endpoint | None = None, src: Endpoint | None = None, url: str | None = None, activity_id: NetworkActivityId | None = None, status_code: str | None = None, now_ms: int | None = None) -> None
Record one failed connection with its reason.
Source code in src/py20305/forwarders/connection_telemetry.py
ConnectionTelemetryEmitter ¶
Turns connection outcomes into events on the forwarder transport.
One instance per host application. Attached as the IEEE 2030.5 client's
connection observer (client.http.connection_observer = emitter), the
client calls :meth:begin_request / :meth:record_success /
:meth:record_failure / :meth:on_connect / :meth:flush through that
seam, and those events carry the northbound role. The host reports
its other links through :meth:record_connection and
:meth:record_connection_failure.
Successes are coalesced per link into windows, unless the configuration turns coalescing off, in which case every success is its own event. Failures are always one event each.
Events produced while the transport cannot deliver them -- the broker is unreachable, or the forwarder has not started -- are held, up to a bound, and sent once it can. A broker outage is itself a connection event, and its record would otherwise be lost to the outage it describes.
Nothing here may raise into the caller. Telemetry that can break the connection it describes is worse than absent telemetry, so every entry point swallows its own errors and logs them.
Source code in src/py20305/forwarders/connection_telemetry.py
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 | |
coalescing
property
¶
Whether successes are collapsed into windows rather than sent one by one.
held_events
property
¶
Events waiting for the transport to become able to deliver them.
set_server ¶
Record the server this client targets, for events with no live socket.
A connection that never established has no socket to report, but the address the client was trying to reach is exactly what the failure record needs to name.
Source code in src/py20305/forwarders/connection_telemetry.py
begin_request ¶
Clear any socket left from an earlier request in this task.
Scopes attribution to "a connection established while handling this request". Without it, a task that opens a connection and then makes a second request over the pool would attribute the first connection's local port to the second request -- right only by luck, and wrong as soon as the pool hands back a different connection.
Source code in src/py20305/forwarders/connection_telemetry.py
on_connect ¶
Observe a newly established connection's socket.
Reached through the client's connector, which is the only place the local port is available.
Source code in src/py20305/forwarders/connection_telemetry.py
record_success ¶
Record one successful IEEE 2030.5 exchange with the server.
Source code in src/py20305/forwarders/connection_telemetry.py
record_failure ¶
Record one failed IEEE 2030.5 connection or exchange, with its reason.
Never coalesced: each failure keeps its own event and its own reason.
Source code in src/py20305/forwarders/connection_telemetry.py
record_connection ¶
record_connection(*, role: ConnectionRole | str, service: str | None, direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, dst: Endpoint | None = None, src: Endpoint | None = None, url: str | None = None, now_ms: int | None = None) -> None
Record one successful connection on one of the host's links.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
role
|
ConnectionRole | str
|
Which link the connection belongs to. |
required |
service
|
str | None
|
Service label for |
required |
direction
|
ConnectionDirectionId
|
|
OUTBOUND
|
dst
|
Endpoint | None
|
The responder: the device or broker for an outbound connection, the host's own listener for an inbound one. Required for the event to be reported. |
None
|
src
|
Endpoint | None
|
The initiator, when its address is known. |
None
|
url
|
str | None
|
URL the connection targeted, if any. Credentials and the query string are removed. |
None
|
now_ms
|
int | None
|
Time of the connection in epoch milliseconds; now when omitted. |
None
|
Source code in src/py20305/forwarders/connection_telemetry.py
record_connection_failure ¶
record_connection_failure(*, role: ConnectionRole | str, service: str | None, reason: BaseException | str, direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, dst: Endpoint | None = None, src: Endpoint | None = None, url: str | None = None, activity_id: NetworkActivityId | None = None, status_code: str | None = None, now_ms: int | None = None) -> None
Record one failed connection on one of the host's links, with its reason.
Never coalesced. The reason is reduced to a bounded line of text with
credentials in URLs removed; see :func:link_outcome.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
role
|
ConnectionRole | str
|
Which link the connection belongs to. |
required |
service
|
str | None
|
Service label for |
required |
reason
|
BaseException | str
|
The exception the connection failed with, or reason text. |
required |
direction
|
ConnectionDirectionId
|
|
OUTBOUND
|
dst
|
Endpoint | None
|
The responder. Required for the event to be reported. |
None
|
src
|
Endpoint | None
|
The initiator, when its address is known. |
None
|
url
|
str | None
|
URL the connection targeted, if any. |
None
|
activity_id
|
NetworkActivityId | None
|
Overrides the classified activity; |
None
|
status_code
|
str | None
|
The peer's own code, when it gave one. |
None
|
now_ms
|
int | None
|
Time of the failure in epoch milliseconds; now when omitted. |
None
|
Source code in src/py20305/forwarders/connection_telemetry.py
flush_expired ¶
Emit every success window whose length has elapsed, then send held events.
Windows also close on any later activity through the emitter. A host calls this on a timer so a link that went quiet is still reported within about one window length, and so events held during a broker outage go out once it ends.
Source code in src/py20305/forwarders/connection_telemetry.py
flush ¶
Emit every open success window.
Called when the client or the host closes: successes accumulated since the last window closed are still attempts the connection log accounts for.
Source code in src/py20305/forwarders/connection_telemetry.py
release_held ¶
Send held events if the transport can deliver them now.
Source code in src/py20305/forwarders/connection_telemetry.py
role_label ¶
classify ¶
Classify an exception from the request path into an OCSF outcome.
The distinction that matters to whoever reads the log is whether the connection was ever established:
- A transport failure -- connect, timeout, TLS handshake -- means nothing
was exchanged, and reports
FAIL. A refused connection reportsREFUSE, which is more specific and is what the peer actually did. - An application-layer failure over a connection that did open -- a 500,
a 429, a redirect loop, an unusable body -- reports
OPENwith a failure status. Reporting it asFAILwould tell a reader the client never reached the server, which would send an investigation the wrong way.
Returns:
| Type | Description |
|---|---|
Outcome | None
|
The classified outcome, or |
Outcome | None
|
connection outcome at all and nothing should be reported. |
Source code in src/py20305/forwarders/connection_telemetry.py
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | |
build_metadata ¶
build_metadata(product_version: str | None = None, *, role: ConnectionRole | str = ConnectionRole.NORTHBOUND, product_name: str = PRODUCT_NAME, vendor_name: str = DEFAULT_VENDOR_NAME) -> Metadata
Build the OCSF metadata block naming the producer and the link's role.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
product_version
|
str | None
|
Version of the producing application. |
None
|
role
|
ConnectionRole | str
|
The link the event describes, recorded as one |
NORTHBOUND
|
product_name
|
str
|
Name of the producing application. Defaults to this library; a host application passes its own name so the event identifies the product that actually emitted it. |
PRODUCT_NAME
|
vendor_name
|
str
|
Publisher of the producing application. |
DEFAULT_VENDOR_NAME
|
Source code in src/py20305/forwarders/connection_telemetry.py
endpoint_for_host ¶
Build an endpoint for a configured host, which may be an address or a name.
OCSF types the endpoint's ip as an IP address. A configured host is
often a DNS name, and a name in ip is a schema violation a consumer may
reject, so a name goes in hostname.
Returns:
| Type | Description |
|---|---|
Endpoint | None
|
The endpoint, or |
Source code in src/py20305/forwarders/connection_telemetry.py
build_failure_event ¶
build_failure_event(outcome: Outcome, *, metadata: Metadata, socket_pair: SocketPair | None, server_endpoint: Endpoint | None, url: str | None = None, service: str | None = SERVICE_LABEL, direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, time: int | None = None) -> NetworkActivity | None
Build one failure event. Failures are never coalesced.
Returns None when there is no destination to report -- an event with
neither endpoint says nothing about a connection, and the event class
rejects it.
Source code in src/py20305/forwarders/connection_telemetry.py
build_coalesced_success_event ¶
build_coalesced_success_event(window: Window, *, metadata: Metadata, server_endpoint: Endpoint | None, url: str | None = None, service: str | None = SERVICE_LABEL, direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND) -> NetworkActivity | None
Build one event standing for a window of successful connections.
server_endpoint is the destination reported when the window observed
none, as for a request that reused a pooled connection.
Source code in src/py20305/forwarders/connection_telemetry.py
link_outcome ¶
link_outcome(reason: BaseException | str, *, activity_id: NetworkActivityId | None = None, status_code: str | None = None) -> Outcome
Classify a failure the host reported on one of its own links.
Unlike :func:classify, this always returns an outcome: the caller has
already decided the exception is a connection failure. An exception the
IEEE 2030.5 taxonomy does not know is reported as FAIL with its type
and message. Credentials in any URL inside the reason are removed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reason
|
BaseException | str
|
The exception, or the reason text. |
required |
activity_id
|
NetworkActivityId | None
|
Overrides the classified activity. |
None
|
status_code
|
str | None
|
The peer's own code, when it gave one. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py20305/forwarders/connection_telemetry.py
py20305.forwarders.audit ¶
The control audit trail: event lifecycle and loss-of-communications records.
A utility program asks of a control client what it did with each control it was sent: when an event was scheduled, when it went active and on which devices, how it ended, and when the client stopped acting on the server because the server went quiet. The protocol exchanges themselves are already forwarded, but they record what the server said, not what this client decided. These records are the decisions.
Each record is a flat JSON document published on its own topic, so a search index stores every field as a field and an audit query hits one small index.
Delivery is best-effort, as for every stream on this transport. What this
module adds is the ability to tell a complete trail from an incomplete one:
every record carries boot_id and seq, one counter per client shared
with that client's device-write records, so a gap in seq marks a lost
record and a new boot_id marks a restart.
Nothing here may raise into event processing. A trail that can stop a control from being applied is worse than no trail.
AuditSequence ¶
One client's audit counter: a random boot_id and a rising seq.
Shared by that client's device-write and lifecycle records, so a single gap check covers both streams. One per client rather than per process: two clients interleaving on one counter would each show a gap at every other record, which reads as loss.
Source code in src/py20305/forwarders/audit.py
stamp ¶
AuditEmitter ¶
Publishes lifecycle and loss-of-communications records.
Built unconfigured and disabled, then pointed at the transport and the operator's configuration once both exist -- the same two-step wiring the device telemetry emitter uses, for the same reason: the forwarder is built after the client.
Source code in src/py20305/forwarders/audit.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
sequence
property
¶
This client's counter. Hand it to the client's device telemetry emitter so device writes number in the same stream.
attach_forwarder ¶
configure ¶
Apply operator configuration after construction.
event_transition ¶
event_transition(*, event_mrid: bytes, program_href: str, from_state: str | None, to_state: str, effective_start: int, effective_duration: int, primacy: int, lfdis: list[str], at: float, applied_lfdis: list[str] | None = None, rejected_lfdis: list[str] | None = None, superseded_by: bytes | None = None, superseded_lfdis: list[str] | None = None, superseded_modes: dict[str, list[str]] | None = None) -> None
Record one event moving from one state to another.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_mrid
|
bytes
|
The event's mRID. |
required |
program_href
|
str
|
The program the event belongs to. |
required |
from_state
|
str | None
|
The state before, or |
required |
to_state
|
str
|
|
required |
effective_start
|
int
|
Start after randomization, epoch seconds. |
required |
effective_duration
|
int
|
Duration after randomization, seconds. |
required |
primacy
|
int
|
The program's primacy. |
required |
lfdis
|
list[str]
|
The devices the event targets. |
required |
at
|
float
|
When the transition happened, on the server's timebase. |
required |
applied_lfdis
|
list[str] | None
|
On |
None
|
rejected_lfdis
|
list[str] | None
|
On |
None
|
superseded_by
|
bytes | None
|
On |
None
|
superseded_lfdis
|
list[str] | None
|
On a partial supersession, the devices it covers. |
None
|
superseded_modes
|
dict[str, list[str]] | None
|
On a partial supersession, the modes per device. |
None
|
Source code in src/py20305/forwarders/audit.py
comms_loss ¶
comms_loss(*, transition: str, threshold: int, simulated: bool, at: float, elapsed_seconds: int | None = None, duration_seconds: int | None = None, resume_after: int | None = None) -> None
Record a step into or out of loss-of-communications mode.
Published in the order things happen: entered before any event is
opted out, recovering once contact returns and before the schedule
is polled again, cleared once the mode ends. A recovery that fails
is retried, so recovering can repeat before one cleared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transition
|
str
|
|
required |
threshold
|
int
|
The configured silence threshold. |
required |
simulated
|
bool
|
Whether an operator-triggered simulation caused it. |
required |
at
|
float
|
When it happened, on the server's timebase. |
required |
elapsed_seconds
|
int | None
|
On |
None
|
duration_seconds
|
int | None
|
On |
None
|
resume_after
|
int | None
|
On |
None
|
Source code in src/py20305/forwarders/audit.py
late_dispatch ¶
late_dispatch(*, event_mrid: bytes, lfdi: str, applied: bool, at: float, error: str | None = None, error_type: str | None = None) -> None
Record how a dispatch that missed the activation deadline ended.
The active record lists such a device as rejected, because that is
what the server was told. The apply carries on regardless, and this is
the record of whether it then reached the device.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
event_mrid
|
bytes
|
The event whose control it was. |
required |
lfdi
|
str
|
The device, or its href where no LFDI is known. |
required |
applied
|
bool
|
Whether the apply finished without error. |
required |
at
|
float
|
When it finished, on the server's timebase. |
required |
error
|
str | None
|
The failure, when it failed. |
None
|
error_type
|
str | None
|
The failure's exception class name. |
None
|
Source code in src/py20305/forwarders/audit.py
mrid_hex ¶
An mRID as uppercase hex, the form the mRID element takes on the wire.
Matching the wire form lets a record be joined to the raw control by a plain text match on the identifier.
Source code in src/py20305/forwarders/audit.py
py20305.forwarders.ocsf ¶
A minimal OCSF Network Activity (class 4001) event, for connection telemetry.
The Open Cybersecurity Schema Framework is a public, vendor-neutral schema
(https://schema.ocsf.io/). This module carries just enough of it to emit one
class -- Network Activity, class_uid 4001 -- which is how this client
reports its own connection outcomes to a security-monitoring platform:
src_endpoint / dst_endpoint / status_id / status_detail map
onto what a connection log needs, and a consumer can ingest an established
schema without a translation layer.
Vendored rather than depended on: the emitting side needs four factories and a serializer, not a schema toolkit, and a library client should not carry a platform dependency to publish a public format.
Two rules here come from the compliance argument behind connection logging rather than from the schema, and are enforced rather than documented:
- A failure event must carry
status_detail. The reason is the entire value of an error record; a failure without one satisfies nobody. - Failures are never aggregated. Successful connections may be coalesced into a window carrying a count -- a polling client would otherwise emit more connection events than the passive capture beside it -- but collapsing distinct failure reasons into a count destroys the record's only value.
SeverityId ¶
Bases: IntEnum
OCSF severity_id -- how consequential the event is.
Source code in src/py20305/forwarders/ocsf.py
StatusId ¶
NetworkActivityId ¶
ConnectionDirectionId ¶
Bases: IntEnum
OCSF connection_info.direction_id -- which way the connection runs.
Source code in src/py20305/forwarders/ocsf.py
Endpoint
dataclass
¶
An OCSF network_endpoint -- one end of a connection.
ip and hostname are distinct OCSF attributes with typed meanings:
ip must hold an IP address, and a DNS name goes in hostname. A
configured server URL usually names a host, while an established socket
reports addresses, so either alone identifies an endpoint -- but at least
one must be present, or the endpoint identifies nothing.
port may be None: an application-layer client frequently cannot
determine the ephemeral local port its socket was assigned, and an unknown
port is reported as unknown rather than invented. (This is why the OCSF
path does not reuse :class:~py20305.forwarders.types.NetworkEndpoint,
whose wire contract requires a port.)
Source code in src/py20305/forwarders/ocsf.py
to_dict ¶
Serialize; unset attributes are omitted.
Source code in src/py20305/forwarders/ocsf.py
Product
dataclass
¶
OCSF product object -- the software that produced the event.
Source code in src/py20305/forwarders/ocsf.py
to_dict ¶
Serialize to an OCSF product object; unset fields are omitted.
Source code in src/py20305/forwarders/ocsf.py
Metadata
dataclass
¶
OCSF metadata object -- required on every OCSF event.
Attributes:
| Name | Type | Description |
|---|---|---|
product |
Product
|
The software that produced the event. |
version |
str
|
The OCSF schema version the event conforms to. |
labels |
list[str]
|
Free-form labels. Connection telemetry uses one
|
Source code in src/py20305/forwarders/ocsf.py
to_dict ¶
Serialize to an OCSF metadata object; empty labels are omitted.
Source code in src/py20305/forwarders/ocsf.py
NetworkActivity
dataclass
¶
An OCSF Network Activity (4001) event describing one connection outcome.
Prefer the factories -- :meth:for_connection_success,
:meth:for_connection_failure, :meth:for_exchange_failure, and
:meth:for_coalesced_successes -- over the constructor. They set the
activity_id / status_id pair together, which is the part of OCSF
conformance no validator can check: emitting Open / Success for a
connection that actually failed is structurally valid and semantically
wrong.
The two failure factories split on which layer failed, because that is
what a reader needs to know first. :meth:for_connection_failure covers
transport-level failures -- the connection never opened (Fail,
Refuse) or was torn down after it did (Reset).
:meth:for_exchange_failure covers a connection that stayed up and whose
exchange over it failed, so the activity remains Open and only the
status is Failure.
Attributes:
| Name | Type | Description |
|---|---|---|
activity_id |
NetworkActivityId
|
What happened to the connection. |
metadata |
Metadata
|
The producing software and the OCSF version emitted. |
time |
int
|
Event time in epoch milliseconds. For an aggregate this is
|
severity_id |
SeverityId
|
OCSF severity. |
status_id |
StatusId
|
Outcome of the activity. |
status_detail |
str | None
|
Why it happened. Required on every failure. |
status_code |
str | None
|
Optional producer-specific code, e.g. an HTTP status. |
message |
str | None
|
Optional human-readable description. |
count |
int | None
|
Number of connections aggregated into this record. Unset for
discrete events; |
start_time |
int | None
|
First event in the aggregation window, epoch ms. |
end_time |
int | None
|
Last event in the aggregation window, epoch ms. |
duration |
int | None
|
Window length in ms. Derived from the bounds when not supplied; an explicit value that disagrees with them is rejected. |
src_endpoint |
Endpoint | None
|
The initiator of the connection. |
dst_endpoint |
Endpoint | None
|
The responder. At least one endpoint must be present, per the OCSF constraint on the network category. |
service |
str | None
|
Service label, e.g. |
connection_direction |
ConnectionDirectionId
|
Direction of the connection. Defaults to outbound, the case a client-side emitter reports. |
protocol_name |
str | None
|
Transport protocol name, e.g. |
url |
str | None
|
Optional URL the connection targeted; on a failure it is what identifies which interface was unreachable. |
Source code in src/py20305/forwarders/ocsf.py
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 | |
to_dict ¶
Serialize to an OCSF 4001 event dictionary.
Source code in src/py20305/forwarders/ocsf.py
for_connection_success
classmethod
¶
for_connection_success(*, metadata: Metadata, dst_endpoint: Endpoint, src_endpoint: Endpoint | None = None, service: str | None = None, url: str | None = None, time: EventTime | None = None, protocol_name: str | None = 'tcp', connection_direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, severity_id: SeverityId = SeverityId.INFORMATIONAL, message: str | None = None) -> NetworkActivity
Report one connection that was established: Open / Success.
Source code in src/py20305/forwarders/ocsf.py
for_connection_failure
classmethod
¶
for_connection_failure(*, metadata: Metadata, dst_endpoint: Endpoint, status_detail: str, activity_id: NetworkActivityId = NetworkActivityId.FAIL, src_endpoint: Endpoint | None = None, service: str | None = None, url: str | None = None, time: EventTime | None = None, status_code: str | None = None, severity_id: SeverityId = SeverityId.MEDIUM, protocol_name: str | None = 'tcp', connection_direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, message: str | None = None) -> NetworkActivity
Report a transport-level connection failure, and why.
status_detail is required: it is the attribute the error record
exists to carry. A status the peer returned is an exchange failure,
not this -- see :meth:for_exchange_failure.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py20305/forwarders/ocsf.py
for_exchange_failure
classmethod
¶
for_exchange_failure(*, metadata: Metadata, dst_endpoint: Endpoint, status_detail: str, src_endpoint: Endpoint | None = None, service: str | None = None, url: str | None = None, time: EventTime | None = None, status_code: str | None = None, severity_id: SeverityId = SeverityId.MEDIUM, protocol_name: str | None = 'tcp', connection_direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, message: str | None = None) -> NetworkActivity
Report a connection that opened and whose exchange over it failed.
The counterpart to :meth:for_connection_failure. Here the connection
worked and stayed up -- a server answered 500, rate-limited the client,
or sent it round a redirect loop -- so the activity is Open and
only the status is Failure. Reporting a 500 as Fail would tell
whoever reads the log that the client never reached the peer, sending
an investigation after a network problem that did not happen;
reporting it as Open / Success because the socket worked would
lose the failure entirely.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py20305/forwarders/ocsf.py
for_coalesced_successes
classmethod
¶
for_coalesced_successes(*, metadata: Metadata, dst_endpoint: Endpoint, count: int, start_time: EventTime, end_time: EventTime, src_endpoint: Endpoint | None = None, service: str | None = None, url: str | None = None, protocol_name: str | None = 'tcp', connection_direction: ConnectionDirectionId = ConnectionDirectionId.OUTBOUND, severity_id: SeverityId = SeverityId.INFORMATIONAL, time: EventTime | None = None, message: str | None = None) -> NetworkActivity
Report a window of successful connections that shared an endpoint.
A polling client opens connections continuously, and one event per connection would put more traffic on the collector than the passive capture beside it. A coalesced event stands in for the whole window, so it preserves what a per-attempt record would have answered: the window's bounds, how many attempts it represents, and the endpoint and service they shared. It is not a representative sample.
time defaults to start_time: OCSF sets an aggregate's time
to the earliest event it covers, so aggregates and discrete events
order consistently on one timeline.
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/py20305/forwarders/ocsf.py
to_epoch_ms ¶
Normalize a time value to an OCSF timestamp_t.
OCSF timestamps are milliseconds since the Unix epoch. This accepts a
datetime (naive values are read as UTC), an ISO 8601 string, or a
number already in epoch milliseconds.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If a string is not parseable as ISO 8601. |
Source code in src/py20305/forwarders/ocsf.py
py20305.client.observer ¶
Connection-outcome observer seam for :class:~py20305.client.http.Sep2Client.
A passive capture beside the client cannot recover the client's own connection
outcomes from inside TLS, so the client reports them itself to whoever asks.
An embedder implements :class:ConnectionObserver and attaches it at
construction (Sep2Client(connection_observer=...)) or afterwards through
the connection_observer property; the client then reports each logical
request's outcome and each established TCP connection's addresses.
Every callback is invoked from the client's own request path, so implementations must never raise and never block: an observer that throws would fail the request it is describing.
ConnectionObserver ¶
Bases: Protocol
What :class:~py20305.client.http.Sep2Client reports connection outcomes to.
Outcomes are reported per logical request, not per retry attempt: the retry wrapper collapses exhausted transport attempts into one error, so individual attempts are not distinguishable at this seam. A 204 No Content counts as a success — it is a validated contact that happens to signal itself by raising.
Source code in src/py20305/client/observer.py
begin_request ¶
A logical request is starting.
Called before the first attempt, so the observer can scope any
per-request attribution (for example, tying an on_connect that
fires during the request to that request).
Source code in src/py20305/client/observer.py
record_success ¶
record_failure ¶
on_connect ¶
A TCP connection was established and passed the handshake audit.
The local port is only knowable at this moment — a pooled connection carries many requests on one local port, and the response object has already released its connection by the time a caller could ask.