Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2ef6c3a
feat(logs): config-driven log transformations and static values for g…
AmitsinghTanwar007 Aug 7, 2026
9a45786
chore: auto-fix formatting and generated code
hyperswitch-bot[bot] Aug 7, 2026
0d7f308
fix(events): replace indexing with safe .get()/.first() for clippy::i…
AmitsinghTanwar007 Aug 7, 2026
4949a10
feat(logs): split log.static_values into incoming and outgoing maps
AmitsinghTanwar007 Aug 7, 2026
7f56c52
feat(logs): unified log fields with centralized post-patch processing…
AmitsinghTanwar007 Aug 9, 2026
5bab766
fix(logs): restore CompactJson log format (revert testing change)
AmitsinghTanwar007 Aug 9, 2026
ec4da20
feat(logs): connector-aware status_code, euler id sources, and incomi…
kanikachaudhary199 Aug 7, 2026
600da90
feat(config): euler golden-line field mapping via unified log fields
kanikachaudhary199 Aug 7, 2026
c8f44ff
chore: auto-fix formatting and generated code
hyperswitch-bot[bot] Aug 10, 2026
4745df1
feat(config): relabel tenant_name from x-tenant-id name
kanikachaudhary199 Aug 10, 2026
9a7da74
Merge branch 'main' into feat/euler-log-structure
AmitsinghTanwar007 Aug 10, 2026
c88c403
fix(external-services): import CompiledLogFields unconditionally for …
kanikachaudhary199 Aug 10, 2026
7dc5933
Merge branch 'main' into feat/euler-log-structure
AmitsinghTanwar007 Aug 10, 2026
76bc7b1
fix(logs): move CompiledLogFields import outside injector-client feat…
AmitsinghTanwar007 Aug 10, 2026
b2e6525
chore: auto-fix formatting and generated code
hyperswitch-bot[bot] Aug 10, 2026
0a19920
refactor(deps): update framework-libs-rs to Cow-based Storage keys
AmitsinghTanwar007 Aug 11, 2026
248c3c5
ci: enable log-transformations feature in test commands
AmitsinghTanwar007 Aug 11, 2026
d34f556
feat(logs): add runtime enabled flag for log-transformations
AmitsinghTanwar007 Aug 11, 2026
110a7da
Merge branch 'main' into feat/euler-log-schema-on-structure
kanikachaudhary199 Aug 11, 2026
1c4740b
feat(logs): numeric latency_ms, timestamp alias, static req_type; ena…
kanikachaudhary199 Aug 11, 2026
3dc3a6e
revert(logs): drop inert timestamp alias
kanikachaudhary199 Aug 11, 2026
7df4b57
fix(logs): drop redundant serde_json:: qualifier on latency_ms (clipp…
kanikachaudhary199 Aug 11, 2026
b008bb3
feat(logs): record request/response body and headers as structured JSON
AmitsinghTanwar007 Aug 11, 2026
7b83411
Merge branch 'main' into feat/euler-log-structure
AmitsinghTanwar007 Aug 11, 2026
80f9c10
refactor(logs): extract maskable_headers_to_json util, revert event l…
AmitsinghTanwar007 Aug 11, 2026
f59f02c
fix(logs): gate maskable_headers_to_json import behind injector-clien…
AmitsinghTanwar007 Aug 11, 2026
d49d062
fix(logs): move record_json_fields_on_span import to unconditional block
AmitsinghTanwar007 Aug 11, 2026
b510b43
fix(logs): use standard masking format for masked header values
AmitsinghTanwar007 Aug 12, 2026
644b395
Merge #2094 (feat/euler-log-structure): structured JSON request/respo…
kanikachaudhary199 Aug 12, 2026
4153235
Merge origin/main into feat/euler-log-schema-on-structure
Aug 13, 2026
e4a4f90
fix(logs): make status_code change backward-compatible via additive r…
Aug 13, 2026
c3d188a
refactor(logs): address review — move euler log.fields to deployment …
kanikachaudhary199 Aug 14, 2026
6bb3894
chore: auto-fix formatting and generated code
hyperswitch-bot[bot] Aug 14, 2026
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
5 changes: 5 additions & 0 deletions crates/common/external-services/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,10 @@ pub struct EventProcessingParams<'a> {
response.error_message = Empty,
response.status_code = Empty,
message_ = "Golden Log Line (outgoing)",
// `latency` is the pre-existing human-readable string; `latency_ms` is the same
// duration as a plain number of milliseconds, for numeric downstream consumers.
latency = Empty,
latency_ms = Empty,
Comment on lines 583 to +584

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference between both the above fields

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

)
)]
#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -1087,6 +1090,8 @@ where

let elapsed = start.elapsed().as_millis();
tracing::Span::current().record("latency", elapsed);
// Additive numeric latency alongside the existing string `latency`.
tracing::Span::current().record("latency_ms", u64::try_from(elapsed).unwrap_or_default());
// Apply outgoing log fields (transformations + static values) before emitting the golden log line
#[cfg(feature = "log-transformations")]
if event_params.log_fields_enabled {
Expand Down
2 changes: 1 addition & 1 deletion crates/common/ucs_env/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@ otel = ["dep:opentelemetry", "dep:opentelemetry-otlp", "dep:opentelemetry_sdk"]
log-transformations = ["common_utils/log-transformations"]

[lints]
workspace = true
workspace = true
9 changes: 9 additions & 0 deletions crates/grpc-server/grpc-server/src/http/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ fn grpc_code_to_http_status(code: tonic::Code) -> StatusCode {
}
}

/// The HTTP status the caller receives for a gRPC error: the connector's exact 4xx/5xx when the
/// details carry one, else the gRPC-to-HTTP fallback. Shared by the HTTP response and the golden
/// line so both report the same status.
pub fn http_status_for_status(status: &tonic::Status) -> StatusCode {
let details = extract_error_details_from_status(status);
connector_http_status_from_error_details(details.as_ref())
.unwrap_or_else(|| grpc_code_to_http_status(status.code()))
}

// Convert tonic::Status to HTTP error
impl From<tonic::Status> for HttpError {
fn from(status: tonic::Status) -> Self {
Expand Down
65 changes: 60 additions & 5 deletions crates/grpc-server/grpc-server/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub fn record_fields_from_header<B: hyper::body::Body>(request: &Request<B>) ->
"request",
uri = %url_path,
version = ?request.version(),
// `action` = the real HTTP verb (GET/POST/…). gRPC-over-HTTP2 is always POST;
// the HTTP gateway carries the true verb.
action = %request.method(),
tenant_id = tracing::field::Empty,
request_id = tracing::field::Empty,
execution_mode = tracing::field::Empty,
Expand Down Expand Up @@ -276,21 +279,51 @@ where
..
} = metadata_payload;
let current_span = tracing::Span::current();
let masked_body = hyperswitch_masking::masked_serialize(&request_data.payload)
.map_err(|e| tracing::error!("Masked serialization error: {:?}", e))
.ok();
let connector_name = connector.get_connector_name();
current_span.record("service_name", service_name);
match hyperswitch_masking::masked_serialize(&request_data.payload) {
Ok(masked_value) => {
record_json_fields_on_span(vec![("request_body", masked_value)]);
match masked_body.as_ref() {
Some(masked_value) => {
record_json_fields_on_span(vec![("request_body", masked_value.clone())]);
}
Err(e) => {
tracing::error!("Masked serialization error: {:?}", e);
None => {
current_span.record("request_body", "<masked serialization error>");
}
};
current_span.record("gateway", connector_name);
current_span.record("merchant_id", merchant_id);
current_span.record("tenant_id", tenant_id);
current_span.record("request_id", request_id);

// Standard request identifiers (order id / customer id / transaction id) surfaced as flat
// span fields so the log-field mapping can source them by flat key (it cannot reach into
// `request_body`), without declaring them on every handler's `#[instrument]`.
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),
),
]
Comment on lines +303 to +319

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the two merchant id are euler specific id's if they know the values they can send it in runtime headers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its ok they are part of request body
but if you need them in logging can't you use the header of log transformation?

}) {
record_json_fields_on_span(
ids.into_iter()
.filter_map(|(key, value)| value.map(|value| (key, Value::from(value))))
.collect(),
);
}
tracing::info!("Golden Log Line (incoming - request)");
Ok(())
}
Expand All @@ -306,6 +339,10 @@ pub fn log_after_initialization<T>(

match &result {
Ok(response) => {
// Additive numeric `res_code` (success). `status_code` is left untouched so
// existing consumers of the gRPC-code field don't break.
record_json_fields_on_span(vec![("res_code", Value::from(200_i64))]);

let res_ref = response.get_ref();

// Record response_body as structured JSON with masking
Expand Down Expand Up @@ -340,7 +377,12 @@ pub fn log_after_initialization<T>(
}
Err(status) => {
current_span.record("error_message", status.message());
// Backward-compatible: keep main's gRPC code-name string on `status_code`.
current_span.record("status_code", status.code().to_string());
// Additive numeric `res_code`: connector-aware HTTP status (e.g. 422) — matches the
// HTTP response the caller receives, not the coarse gRPC code.
let http_status = crate::http::error::http_status_for_status(status).as_u16();
record_json_fields_on_span(vec![("res_code", Value::from(i64::from(http_status)))]);
}
}
// Apply unified log fields (transformations + static values) before emitting the golden log line
Expand All @@ -356,6 +398,15 @@ pub fn log_after_initialization<T>(
tracing::info!("Golden Log Line (incoming - response)");
}

/// Record the additive numeric `latency_ms` on the current span. Shared by the streaming and
/// non-streaming logging wrappers to avoid drift.
fn record_latency_ms(duration: u128) {
record_json_fields_on_span(vec![(
"latency_ms",
Value::from(u64::try_from(duration).unwrap_or_default()),
)]);
}
Comment on lines +403 to +408

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


/// Generic gRPC logging wrapper that accepts a custom parser function.
/// This allows different parsing strategies for different flow types
/// (e.g., authenticated flows vs unauthenticated webhook flows).
Expand Down Expand Up @@ -398,6 +449,8 @@ where

let duration = start_time.elapsed().as_millis();
current_span.record("response_time", duration);
// Additive numeric latency alongside the existing `response_time`.
record_latency_ms(duration);
result
}
.await;
Expand Down Expand Up @@ -466,6 +519,8 @@ where

let duration = start_time.elapsed().as_millis();
current_span.record("response_time", duration);
// Additive numeric latency alongside the existing `response_time`.
record_latency_ms(duration);
result
}
.await;
Expand Down
Loading