feat(logs): euler-parity golden-line sources + mapping config (on #2075) - #2076
feat(logs): euler-parity golden-line sources + mapping config (on #2075)#2076kanikac199 wants to merge 33 commits into
Conversation
…olden log lines Add Euler-compatible log schema support: - Config-driven field renaming for golden log lines via [log.transformations.incoming] and [log.transformations.outgoing] (gated behind log-transformations feature flag) - Runtime static key-value pairs via [log.static_values], overridable per-request through x-config-override header - transformation_mode (copy/replace) for event field transformations - Deadlock fix: filter reserved keys before entering span write lock to prevent re-entrant deadlock from log_utils record_value calling tracing::warn! Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Allows different static key-value pairs for incoming (gRPC handler) and outgoing (connector call) golden log lines. Config format changes from flat [log.static_values] to [log.static_values.incoming] and [log.static_values.outgoing].
894aa60 to
0696dff
Compare
… and feature gating Replace separate log.transformations and log.static_values with a single log.fields config supporting Source (span lookup) and Value (literal) entries. Dotted target paths build nested JSON with deep-merge semantics. - Add LogFieldEntry enum, CompiledLogFields compilation, and apply_log_fields() - Add post_patch_processing() on Config for recompiling derived fields - Centralize compiled_outgoing: remove 18 duplicate compile calls, use pre-compiled config.log_fields directly from Config - Recompile log_fields in merge_config_with_override() after patch apply - Gate runtime application behind log-transformations feature flag - x-config-override header merges log.fields with extend (not replace) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ng HTTP verb - http/error.rs: expose http_status_for_status() so the golden line reports the connector's exact 4xx/5xx (not the coarse gRPC code) - incoming line records status_code (200 / connector-aware), the euler id sources (merchant_order_id / customer_id / merchant_transaction_id) as flat span fields, and action (real HTTP verb) so the log transforms can source them by flat key
Populate [log.fields.incoming|outgoing] (Amit's unified LogFieldEntry map) so both
golden lines render in euler shape: flat udf_* / category / entity / resp_code keys
plus the nested api_details object. { source = } reads a span field; { value = } is a
literal. Per-request patchable via x-config-override.
0696dff to
600da90
Compare
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
euler sends the tenant name as x-tenant-id (UCS span tenant_id); map it to tenant_name so the name is preserved, while euler forwards the real tenant_id (uuid) per-request via x-config-override (overrides on merge).
…ffi build EventProcessingParams is ungated and always holds a &CompiledLogFields field, but the import was gated on injector-client. The ffi crate builds external-services with default-features=false (no injector-client), so the type was referenced but not imported (E0412). Move the import out of the injector-client cfg block so every feature combo compiles.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Update log_utils rev to use Cow<'static, str> keys in Storage instead of String. Adapt apply_log_fields snapshot type and record_value calls.
Add grpc-server/log-transformations to both nextest run commands so CI tests compile and run with log field transformation code enabled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `enabled` field to `LogFields` config (default: false) so log field application can be toggled at runtime via config or x-config-override without rebuilding. Also add log-transformations feature to Dockerfile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts by adopting main's runtime log-fields 'enabled' toggle (LogFields.enabled + LogFieldsPatch, log_fields_enabled threading through EventProcessingParams/log_after_initialization, gated apply_log_fields) and main's Cow<'static, str> storage-key refactor; keep the branch's golden-line sources/mapping config and the unconditional CompiledLogFields import. Dependency conflicts: align framework-libs-rs (log_utils/build_info) to main's rev 76613f5d in all Cargo.toml + Cargo.lock.
…ble log fields - add numeric `latency_ms` span field (incoming wrappers + outgoing connector step) so `latency`/`api_details.latency` are numbers like euler (existing string `response_time`/`latency` left untouched — additive) - emit `timestamp` alongside `time` in the JSON formatter (euler-schema alias) - `api_details.req_type` static value: INTERNAL (incoming) / EXTERNAL (outgoing) - set `[log.fields] enabled = true` (main made application opt-in)
The edited ucs_env/logger/formatter.rs is dead code (not wired into mod.rs); the active formatter is log_utils's, which emits `time`. The timestamp edit had no effect. The euler-schema `time`->`timestamp` rename is handled by the LP overlay instead. Keeps the working latency_ms/req_type/enabled changes.
…y unused_qualifications)
Use record_json_fields_on_span() to write request_body, response_body, request.headers, request.body, response.body, and response.headers as serde_json::Value directly into span storage. This produces proper nested JSON in log lines instead of escaped JSON strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…og change, use masked_serialize for response_body
…nse body + headers Combine with the branch's golden-line work: keep masked_body for the udf_* id extraction and numeric latency_ms, adopt #2094's record_json_fields_on_span / maskable_headers_to_json so request/response body + headers emit as nested JSON (not escaped debug strings).
Resolve conflicts in utils.rs (keep status_code=200 on success + masked_body binding for udf-id extraction) and service.rs (keep CompiledLogFields comment). Dedup the identical latency_ms recording into a shared record_latency_ms() helper (review #3).
…es_code Keep the `status_code` span field exactly as main (gRPC code-name string on error, Empty on success) and emit the numeric euler value under a new `res_code` field via the Storage API. Config now sources `resp_code`/`api_details.res_code` from `res_code`. No consumer of the existing `status_code` field breaks.
| # Patchable per-request via `x-config-override` (merge semantics). | ||
| [log.fields] | ||
| # Runtime toggle (main made log-field application opt-in; default false). | ||
| enabled = true | ||
|
|
||
| [log.fields.incoming] | ||
| "x-request-id" = { source = "request_id" } | ||
| "udf_order_id" = { source = "merchant_order_id" } | ||
| "udf_customer_id" = { source = "customer_id" } | ||
| "udf_txn_uuid" = { source = "merchant_transaction_id" } | ||
| "entity" = { source = "flow" } | ||
| "resp_code" = { source = "res_code" } | ||
| "category" = { value = "INCOMING_API" } | ||
| "schema_version" = { value = "V2" } | ||
| "tag" = { value = "euler_logs" } | ||
| # euler sends the tenant NAME as x-tenant-id (span `tenant_id`); relabel it to `tenant_name`. | ||
| # euler overrides the real `tenant_id` (uuid) per-request via x-config-override. | ||
| "tenant_name" = { source = "tenant_id" } | ||
| # euler emits `latency` at the top level (in addition to the nested one); mirror it here. | ||
| "latency" = { source = "latency_ms" } | ||
| "api_details.url" = { source = "uri" } | ||
| "api_details.method" = { source = "action" } | ||
| "api_details.res_code" = { source = "res_code" } | ||
| "api_details.req_body" = { source = "request_body" } | ||
| "api_details.res_body" = { source = "response_body" } | ||
| "api_details.error" = { source = "error_message" } |
There was a problem hiding this comment.
this cant be in open source file you have to have this in your own deployment toml file
| // `CompiledLogFields` is used by the ungated `EventProcessingParams` struct, so it must be | ||
| // imported unconditionally (the ffi build compiles this crate without `injector-client`). |
There was a problem hiding this comment.
no need of this comment
| latency = Empty, | ||
| latency_ms = Empty, |
There was a problem hiding this comment.
what is the difference between both the above fields
There was a problem hiding this comment.
Both point to the same elapsed time. latency is the human-readable string we already had; latency_ms is the exact same duration but as a plain number of milliseconds, so downstream consumers that want a numeric latency can read it directly instead of parsing the string. One is the string form, the other the numeric form.
|
|
||
| let elapsed = start.elapsed().as_millis(); | ||
| tracing::Span::current().record("latency", elapsed); | ||
| // Additive numeric latency (euler `latency` is a number). `latency` (string) left as-is. |
There was a problem hiding this comment.
dont mention euler in comments
| let elapsed = start.elapsed().as_millis(); | ||
| tracing::Span::current().record("latency", elapsed); | ||
| // Additive numeric latency (euler `latency` is a number). `latency` (string) left as-is. | ||
| tracing::Span::current().record("latency_ms", u64::try_from(elapsed).unwrap_or(u64::MAX)); |
There was a problem hiding this comment.
is there any need of this because we have time out and i dont think so it will ever cross 18,446,744,073,709,551,615
There was a problem hiding this comment.
Agreed it will never realistically overflow. Duration::as_millis() returns a u128 and the workspace lints forbid as casts and .unwrap(), so I still need a checked conversion to u64. I dropped the misleading u64::MAX fallback and switched to unwrap_or_default() (i.e. 0), so on the impossible overflow it is just 0 rather than a fake max value.
| // euler's `action` = the real HTTP verb (GET/POST/…); euler maps it to the `_method` | ||
| // column. gRPC-over-HTTP2 is always POST, the HTTP gateway carries the true verb. |
There was a problem hiding this comment.
dont mention euler avoid this comments
| if let Some(ids) = masked_body.as_ref().map(|body| { | ||
| [ | ||
| ( | ||
| "merchant_order_id", | ||
| body.get("merchant_order_id").and_then(Value::as_str), | ||
| ), | ||
| ( | ||
| "customer_id", | ||
| body.get("customer") | ||
| .and_then(|customer| customer.get("id")) | ||
| .and_then(Value::as_str), | ||
| ), | ||
| ( | ||
| "merchant_transaction_id", | ||
| body.get("merchant_transaction_id").and_then(Value::as_str), | ||
| ), | ||
| ] |
There was a problem hiding this comment.
the two merchant id are euler specific id's if they know the values they can send it in runtime headers?
There was a problem hiding this comment.
These are not extra euler-only values. merchant_order_id, customer.id and merchant_transaction_id are already part of the standard request body UCS receives for every payment. I am just reading them out of the body we already have and putting them on the span as flat fields so the log mapping can pick them up (it cannot reach into the nested request_body). So the caller does not need to resend them as headers, we already have them. I reworded the comment to make that clear.
There was a problem hiding this comment.
its ok they are part of request body
but if you need them in logging can't you use the header of log transformation?
| log_utils::Storage::with_current_span_mut(|storage| { | ||
| for (key, value) in ids { | ||
| if let Some(value) = value { | ||
| storage.record_value(key, Value::String(value.to_owned())); | ||
| } | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
dont call this function instead use the existing function record_json_fields_on_span
| log_utils::Storage::with_current_span_mut(|storage| { | ||
| storage.record_value("res_code", Value::from(200_i64)); | ||
| }); |
There was a problem hiding this comment.
use the existing function
| let http_status = crate::http::error::http_status_for_status(status).as_u16(); | ||
| log_utils::Storage::with_current_span_mut(|storage| { | ||
| storage.record_value("res_code", Value::from(i64::from(http_status))); | ||
| }); |
There was a problem hiding this comment.
use the existing function
| fn record_latency_ms(duration: u128) { | ||
| log_utils::Storage::with_current_span_mut(|storage| { | ||
| storage.record_value( | ||
| "latency_ms", | ||
| Value::from(u64::try_from(duration).unwrap_or(u64::MAX)), | ||
| ); | ||
| }); | ||
| } |
There was a problem hiding this comment.
It is a small helper but it is called from two places (the streaming and non-streaming logging wrappers), so it exists to keep those two identical and avoid drift. I have simplified it to just call record_json_fields_on_span, so it is now a 3-line wrapper. Happy to inline it at both call sites if you prefer.
…toml, reuse record_json_fields_on_span, scrub comments - Remove the euler-specific [log.fields] mapping from the open-source config/development.toml (it now lives in the deployment toml). The mechanism stays; the mapping is deployment config. - Use the existing record_json_fields_on_span helper instead of poking log_utils::Storage directly for res_code, latency_ms, and the request identifiers. - Drop the saturating u64::MAX latency fallback (unwrap_or_default). - Remove connector-name-specific mentions from comments; clarify that the order/customer/transaction ids are standard request fields and that latency vs latency_ms is string-vs-numeric.
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
Stacked on top of #2075 (config-driven log-field engine). Adds the euler source fields the app must emit so the config engine can rename/nest them, plus the actual euler field-mapping config.
What this adds
Code
http/error.rs: exposehttp_status_for_status()— the golden line reports the connector's exact 4xx/5xx status, not the coarse gRPC code.utils.rs):res_code— numeric euler status (200on success, connector-aware HTTP status on error), recorded additively via the Storage API.status_codeis left untouched (see compatibility note below).latency_ms— additive numeric latency (eulerlatencyis a number), recorded via a sharedrecord_latency_ms()helper used by both the streaming and non-streaming wrappers.merchant_order_id/customer_id(customer.id) /merchant_transaction_idextracted from the masked payload and recorded as flat span fields (the engine sources by flat key; it can't reach intorequest_body). Written via the storage API, so no per-handler#[instrument]edits.action— the real HTTP verb on the request span (gRPC is always POST; the HTTP gateway carries the true verb) → feedsapi_details.method.record_json_fields_on_span+maskable_headers_to_json.Config (
development.toml)[log.fields]with a runtimeenabledtoggle (default opt-out — onlydevelopment.tomlsetsenabled = true;production.toml/sandbox.toml/docker_compose.tomlhave no[log.fields]block).[log.fields.incoming]/[log.fields.outgoing]tables. Each entry is one of:{ source = "<span field>" }— copy/rename a flat span field. Dotted target keys (e.g."api_details.api_tag") build the nestedapi_detailsobject.{ value = "<literal>" }— inject a constant (e.g."api_details.req_type" = { value = "INTERNAL" }incoming /"EXTERNAL"outgoing).latency/entity/udf_*keys plus the nestedapi_detailsobject, split per direction.status_codecompatibility — additive, not breakingstatus_codeis left exactly asmainemits it: the gRPC code-name string (e.g."InvalidArgument") on error, and unset (Empty) on success. The numeric euler value lives in a new additiveres_codefield (recorded via the Storage API, no#[instrument]edits), and the config sourcesresp_code/api_details.res_codefromres_code. No existing consumer ofstatus_codebreaks.Verified live: success →
res_code=200,status_codeabsent; error →status_code="…"unchanged,res_code=<http status>.Verified live
action="POST"→api_details.method="POST",category="INCOMING_API",udf_*populated,entity="Authorize", full nestedapi_details; outgoingcategory="OUTGOING_API",api_details.url= connector URL.api_details.{api_tag, req_type, method, latency, url},latency_ms,entity, and structured req/res bodies. Real 2c2p sandbox responses (invoiceCEBU…,PC-B050000 Success).Notes
api_details → messagerename stays downstream (Vector/LP): euler readsmessage.*, butmessageis reserved inlog_utils, so the app stages the object underapi_details.hostname/version/serviceneed no mapping — euler derives_version/_servicefrom the pod-namehostname(whichlog_utilsauto-emits); euler has no standaloneversionfield.CARGO_NET_GIT_FETCH_WITH_CLI=true(the pinnedlog_utilsrev is a bare sha).