Skip to content
Draft
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
20 changes: 20 additions & 0 deletions docs/doctoring/people-api-operational-telemetry-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# People API operational telemetry references

Accessed 2026-08-23. These references are engineering evidence for PR #90; they do not assert OpenTelemetry certification or complete Semantic Conventions conformance.

## APA 7 references

OpenTelemetry Authors. (2026). *OpenTelemetry semantic conventions 1.44.0*. OpenTelemetry. https://opentelemetry.io/docs/specs/semconv/

OpenTelemetry Authors. (2026). *Semantic conventions for HTTP metrics*. OpenTelemetry. https://opentelemetry.io/docs/specs/semconv/http/http-metrics/

OpenTelemetry Authors. (2026). *Semantic conventions for HTTP spans*. OpenTelemetry. https://opentelemetry.io/docs/specs/semconv/http/http-spans/

## Design consequences used by this slice

- The current Semantic Conventions release is 1.44.0.
- HTTP server request duration is represented by the stable `http.server.request.duration` metric in seconds; this slice stores a duration in seconds but leaves histogram/export configuration to the deployment adapter.
- A request method unknown to instrumentation maps to `_OTHER`, and method values are case-sensitive. PR #90 therefore does not place arbitrary request method text into its normal metric dimension.
- `http.route` is intended to be a low-cardinality application route and raw URI paths must not be substituted for it. PR #90 therefore recognizes only application-owned People route templates and emits no route value for unknown paths.
- `error.type` should be predictable and low-cardinality. Successful requests should not set it. PR #90 uses only decimal 5xx status strings plus two bounded middleware states (`unhandled_exception` and `missing_response_status`).
- The OpenTelemetry HTTP conventions include attributes beyond the internal measurement in this slice. A future exporter adapter must satisfy those current requirements itself and must not infer missing values by copying sensitive request metadata into metrics.
38 changes: 38 additions & 0 deletions docs/traceability/people-api-operational-telemetry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# People API operational telemetry traceability

## Truth status

- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` exposes governed People HTTP boundaries and candidate SLOs, but it does not contain a People HTTP measurement middleware or an OpenTelemetry exporter.
- **Active PR truth:** PR #90 adds an adapter-neutral, privacy-minimized request-completion measurement boundary in `orgmetra_people_api.telemetry`.
- **Not claimed:** this slice does not configure an OpenTelemetry SDK/Collector, export OTLP, publish dashboards or alerts, prove an SLO, instrument database calls, or make a release/deployment claim.

## Buyer-visible requirement to executable evidence

| Requirement | Executable boundary | Regression evidence |
| --- | --- | --- |
| Request latency can be measured without copying HR identifiers | `PeopleHttpTelemetryMiddleware` emits `duration_seconds` with only a known route template or `None` | `test_emits_duration_without_identifying_request_values`, `test_unknown_route_never_uses_raw_path_as_metric_route` |
| Route dimensions stay low-cardinality | `classify_people_http_route` recognizes only five application-owned People route templates and never substitutes a raw path | `test_classifies_only_known_low_cardinality_people_routes` |
| HTTP methods stay bounded | `normalize_http_method` emits the reviewed known method set or `_OTHER`; exact built-in strings prevent hostile runtime equality/hash behavior | `test_normalizes_unknown_or_runtime_subclass_methods_to_other` |
| Server errors are aggregatable without backend exception disclosure | HTTP 5xx uses its decimal status string; any propagated exception uses `unhandled_exception` regardless of response-status capture; a missing response start uses `missing_response_status` | `test_records_server_error_as_low_cardinality_status_error_type`, `test_unhandled_exception_is_measured_then_reraised`, `test_missing_response_start_is_bounded_operational_error` |
| Successful/client-error requests do not manufacture server-error dimensions | status < 500 leaves `error_type` unset | `test_does_not_mark_client_error_as_server_failure` |
| Telemetry outage cannot deny governed HR work | sink/measurement failures are caught after the wrapped request outcome is determined; only bounded operator metadata is logged | `test_exporter_failure_never_breaks_people_response` |
| ASGI non-HTTP scopes are not mislabeled as HTTP traffic | non-HTTP scopes pass directly to the wrapped app | `test_non_http_scope_passes_through_without_measurement` |
| Invalid direct measurement construction fails closed | `PeopleHttpRequestMeasurement` validates exact types, finite duration, status range, route allow-list and error/status consistency | `test_measurement_rejects_unbounded_or_noncanonical_dimensions` |
| Middleware wiring fails before traffic when dependencies are unusable | constructor validates wrapped app, metric sink method and monotonic clock callability | `test_rejects_unusable_middleware_dependencies_before_traffic` |
| Response status capture is deterministic | middleware records the first valid integer HTTP response status and ignores invalid/duplicate starts | `test_ignores_invalid_and_duplicate_response_start_statuses` |

## Privacy and cardinality boundary

The measurement deliberately excludes tenant, Person, Candidate, Employment, Position and Assignment identifiers; raw URL/path values; query strings; headers; bearer credentials; actor references; request/response bodies; HR values; support references; exception messages; database details; and foreign-service identifiers. Unknown routes produce `route_template=None` rather than a caller-controlled string.

The sink receives a completed immutable measurement. Exporters may translate that measurement to their backend, but they must not enrich it with PII or uncontrolled request attributes. Export is non-authoritative and best-effort: telemetry loss is operationally visible but cannot change authorization, mutation, read, hire, audit/outbox, or HTTP outcome semantics.

## OpenTelemetry alignment boundary

The design is grounded in OpenTelemetry Semantic Conventions 1.44.0. It follows the stable HTTP server guidance that `http.server.request.duration` is measured in seconds, unknown request methods map to `_OTHER`, `http.route` is a low-cardinality route template rather than a raw path, and `error.type` is predictable and low-cardinality.

This package is **not** an OpenTelemetry instrumentation library and does not claim full Semantic Conventions conformance. A deployment adapter that maps the internal measurement to OpenTelemetry remains responsible for the current required/recommended resource/network attributes, exporter configuration, Collector policy, any deployment-specific override of recognized HTTP methods, histogram aggregation, temporality and retention. Those concerns must not be implemented by copying request PII into metric attributes.

## Owner boundaries

This slice writes only Orgmetra. Keyverse, Naruon, contextual-orchestrator, Psychometrics Commons, TEPP and other dedicated-writer CWL repositories remain read-only dependencies. No cross-service application-table SQL is introduced.
16 changes: 16 additions & 0 deletions services/people-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,19 @@ The People API quality workflow is part of this contract and must run for pull r
`PeopleMutationAsgiApp` exposes the governed People mutation API as `POST /v1/employment-records`, `POST /v1/position-records`, and `POST /v1/assignment-records`. Each command requires an idempotency key, tenant/actor/purpose headers, a non-blank accountable decision reason, human confirmation, and versioned evidence. The HTTP boundary enforces the exact OpenAPI evidence-object shape and cardinality, rejects additional fields and duplicate evidence items, and canonicalizes the complete reference/version set independent of array order. It first derives a PII-minimized `evidence_set_v1:<sha256>` identity and then binds that identity together with the exact validated decision reason into `governance_evidence_v1:<sha256>`. The free-text reason and raw evidence references are not copied into the portable audit envelope, but any reason/reference/version drift changes the governance binding, the immutable audit correlation evidence, and the durable idempotency command digest. A caller therefore cannot reuse the same key after silently changing the high-impact rationale and receive an incorrect replay. The validated `Idempotency-Key` is copied onto the application command and into `PostgresPeopleMutationPort`. Employment and assignment writes require a current `candidate_worker_conversion_record` (`recorded_to IS NULL`) and reuse `orgmetra_hris_kernel` exclusivity and assignment-coverage checks before the port inserts the authoritative fact, calls `record_audit_outbox_event`, and stores `people_mutation_idempotency_record` in the same transaction. A matching retry returns the first committed identity without a second HRIS, audit, or outbox fact. Successful responses contain only opaque record identifiers.

The superseded persistence model must not be restored, and the service must not use direct cross-service application-table SQL.

## Operational telemetry wiring

`PeopleHttpTelemetryMiddleware` is the governed composition point for privacy-safe People HTTP telemetry. Deployment adapters wrap each mounted ASGI app exactly once so live traffic emits one bounded `http.server.request.duration` measurement without ever placing tenant, person, candidate, raw path, query, header, credential, or payload values into metric dimensions:

```python
from orgmetra_people_api import (
PeopleAsgiApp,
PeopleHttpTelemetryMiddleware,
)

governed_reads = PeopleAsgiApp(authenticator=..., policy=..., read_port=...)
served_app = PeopleHttpTelemetryMiddleware(app=governed_reads, sink=your_metric_sink)
```

The middleware never changes request status or exception behavior: exporter, route-classification, and even clock-source failures degrade telemetry to a value-free warning record while the wrapped HR request completes normally. Route labels are restricted to the application-owned low-cardinality templates in `classify_people_http_route`; every other path is recorded with no route label rather than a raw URL.
10 changes: 10 additions & 0 deletions services/people-api/src/orgmetra_people_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
from orgmetra_people_api.postgres import PostgresPeopleReadPort
from orgmetra_people_api.postgres_hire import PostgresHireAcceptancePort
from orgmetra_people_api.postgres_mutations import PostgresPeopleMutationPort
from orgmetra_people_api.telemetry import (
PeopleHttpRequestMeasurement,
PeopleHttpTelemetryMiddleware,
classify_people_http_route,
normalize_http_method,
)

__all__ = [
"AuthenticatedPrincipal",
Expand All @@ -59,6 +65,8 @@
"PeopleMutationIntegrityError",
"PeopleMutationNotFound",
"PeopleMutationPort",
"PeopleHttpRequestMeasurement",
"PeopleHttpTelemetryMiddleware",
"PeopleReadPort",
"PeopleRecordIntegrityError",
"PeopleRecordNotFound",
Expand All @@ -75,9 +83,11 @@
"WorkerPeopleRecord",
"accept_confirmed_hire",
"authorize_resource_fields",
"classify_people_http_route",
"create_assignment_record",
"create_employment_record",
"create_position_record",
"extract_bearer_token",
"normalize_http_method",
"read_worker_people_record",
]
Loading
Loading