Skip to content

Fix: constructLogEntry drops extra open-record fields (e.g. error) — resolves wso2/product-integrator#152#562

Open
SasinduDilshara wants to merge 5 commits intowso2:mainfrom
SasinduDilshara:fix-152
Open

Fix: constructLogEntry drops extra open-record fields (e.g. error) — resolves wso2/product-integrator#152#562
SasinduDilshara wants to merge 5 commits intowso2:mainfrom
SasinduDilshara:fix-152

Conversation

@SasinduDilshara
Copy link
Copy Markdown

@SasinduDilshara SasinduDilshara commented Apr 5, 2026

Summary

  • constructLogEntry() in icp_server/opensearch_adapter_service.bal only assembled a hardcoded set of known fields into the logfmt output string. Any extra field present in the open LogSource record (e.g. error, errorCode, custom app fields emitted by Ballerina runtimes) was silently ignored, so the ICP log viewer never displayed it.
  • The fix adds a loop that iterates all non-handled entries in the open record and appends them as key=value pairs, skipping empty/nil values.
  • A new test file icp_server/tests/log_entry_construction_test.bal covers: the bug scenario (complex error field), basic regression, multiple extra fields, and empty-field skipping.

Changes

File Change
icp_server/opensearch_adapter_service.bal Append extra open-record fields in constructLogEntry()
icp_server/tests/log_entry_construction_test.bal New unit tests for constructLogEntry()

Linked issue

Fixes wso2/product-integrator#152 — ICP log viewer is not showing error messages.

Summary by CodeRabbit

Release Notes

  • New Features

    • Log output now includes additional custom fields from OpenSearch documents previously not displayed, providing more comprehensive logging information.
  • Tests

    • Added test coverage to validate log entry construction accuracy and prevent regressions.

SasinduDilshara and others added 4 commits April 5, 2026 19:04
Fixes wso2/product-integrator#152 — ICP log viewer was silently dropping
any field not in the hardcoded list inside constructLogEntry(). The
LogSource type is an open record, so fields like 'error' (emitted by
Ballerina runtimes as complex JSON objects) were present after
deserialization but never written into the logfmt output string.

The fix iterates all non-handled entries in the open record and appends
them as key=value pairs, skipping empty/nil values. A companion unit-test
file is added to cover the bug, the regression, and edge cases.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@SasinduDilshara
Copy link
Copy Markdown
Author

Issue Analysis — [Issue #152]: ICP log viewer is not showing error messages

Classification

  • Type: Bug
  • Severity Assessment: High — error-level log entries with complex error fields are silently dropped from the log viewer, making it impossible to diagnose BI runtime failures.
  • Affected Component(s): OpenSearch Adapter Service (icp_server/opensearch_adapter_service.bal), Frontend Log Viewer (frontend/src/pages/RuntimeLogs.tsx)
  • Affected Feature(s): Runtime log viewing for BI (Ballerina Integrator) runtimes using the OpenSearch log provider

Reproducibility

  • Reproducible: Yes — confirmed via standalone Ballerina reproduction script
  • Environment:
    • Branch: main
    • Ballerina: 2201.13.1 (Swan Lake Update 13)
    • OS: macOS Darwin 24.0.0
    • ICP version: 2.0.0-SNAPSHOT
  • Steps Executed:
    1. Created a standalone Ballerina script (/tmp/reproduce_icp_152/main.bal) that replicates the LogSource record type and constructLogEntry() function from icp_server/opensearch_adapter_service.bal.
    2. Constructed a JSON payload representing an OpenSearch _source document for a BI log line that includes a complex error field (JSON object value), as described in the issue:
      time=2026-03-13T09:23:35.946+05:30 level=ERROR module=ballerinax/wso2.icp message="Heartbeat response error" error={"causes":[{"message":"Connection refused: localhost/127.0.0.1:9445","detail":{},"stackTrace":[]}],"message":"Something wrong with the connection","detail":{},"stackTrace":[]} icp.runtimeId="musicforweather"
      
    3. Bound the JSON to a LogSource open record and confirmed the error field is preserved.
    4. Called constructLogEntry(sourceData) and inspected the output.
    5. Verified the error field is absent from the constructed string.
  • Expected Behavior: The log entry string delivered to the frontend should include the error field so the log viewer displays the full error details.
  • Actual Behavior: The constructed log entry only includes time, level, module, message, and icp.runtimeId. The error field is completely omitted:
    time=2026-03-13T09:23:35.946+05:30 level=ERROR module="ballerinax/wso2.icp" message="Heartbeat response error" icp.runtimeId="musicforweather"
    
  • Logs/Evidence:
    error field in LogSource: {"causes":[{"message":"Connection refused: localhost/127.0.0.1:9445","detail":{},"stackTrace":[]}],"message":"Something wrong with the connection","detail":{},"stackTrace":[]}
    
    === Constructed log entry (what the frontend receives) ===
    time=2026-03-13T09:23:35.946+05:30 level=ERROR module="ballerinax/wso2.icp" message="Heartbeat response error" icp.runtimeId="musicforweather"
    
    === BUG DEMONSTRATION ===
    FAIL: error field is MISSING from the log entry - Bug #152 confirmed!
          The 'error' field is in the OpenSearch document but constructLogEntry() ignores it.
    

Root Cause Analysis

The bug is entirely in constructLogEntry() at icp_server/opensearch_adapter_service.bal:738-773.

LogSource is an open record (record { ... } syntax without |...| delimiters), so extra fields from OpenSearch — including errorare preserved and accessible via sourceData["error"]. The data is not lost during deserialization.

However, constructLogEntry() only reads a hardcoded set of named fields:

  • time, level, message, service_type
  • module, app_name (for Ballerina service type)
  • artifact_container (for MI service type)
  • traceId, spanId, icp_runtimeId

Any field not in this list is silently ignored. The error field — which Ballerina runtimes emit when logging a non-string error value (e.g., log:printError("msg", 'error = someError)) — is never included in the output string.

The constructed string is stored as LogEntry in the response row and displayed verbatim as log.logLine in frontend/src/pages/RuntimeLogs.tsx:136. Since error never makes it into that string, the user sees a truncated log entry with no error details.

Fix required: constructLogEntry() must iterate over all extra (rest) fields in the open LogSource record and append them to the logfmt output. Fields not in the explicitly handled set should be appended as key=value pairs.

The relevant change is in icp_server/opensearch_adapter_service.bal around line 772:

// After building the known fields, append any remaining fields from the open record
// (e.g. 'error', custom application fields)
string extraFields = "";
// Known fields that are already handled explicitly
string[] knownFields = ["time", "level", "message", "service_type", "module", "app_name",
                        "artifact_container", "traceId", "spanId", "icp_runtimeId",
                        "@timestamp", "log_file_path", "product", "deployment", "app",
                        "app_module", "class", "icp.runtimeId"];
map<anydata> sourceMap = <map<anydata>>sourceData;
foreach [string, anydata] [key, val] in sourceMap.entries() {
    if knownFields.indexOf(key) is () && val != () && val.toString() != "" {
        extraFields += string ` ${key}=${val.toString()}`;
    }
}
return string `time=${time} level=${level}${serviceSpecificFields} message="${message}"${traceId}${spanId}${runtimeId}${extraFields}`;

Test Coverage Assessment

  • Existing tests covering this path: None. There are no existing tests for constructLogEntry() or the log entry response construction in opensearch_adapter_service.bal. The existing test suite (icp_server/tests/) covers GraphQL API, auth, OIDC, and token refresh — not observability/log processing.
  • Coverage gaps identified:
    • constructLogEntry() has zero test coverage
    • The full log request/response pipeline through the OpenSearch adapter (resource function post logs/[string componentType]) has no unit or integration tests
    • No tests verify that extra/unknown fields from OpenSearch documents appear in the log entry output
  • Proposed test plan:
    • Unit test: testConstructLogEntryIncludesComplexErrorField — bind a JSON with an error field to LogSource, call constructLogEntry, assert error= appears in output. (Skeleton created at icp_server/tests/log_entry_construction_test.bal.)
    • Unit test: testConstructLogEntrySimpleMessage — verify basic fields (level, message, runtimeId) still appear correctly after the fix (regression guard).
    • Unit test: testConstructLogEntryMultipleExtraFields — verify multiple unknown fields (e.g., error, errorCode, httpStatus) all appear in output.
    • Integration test: Start the OpenSearch adapter service with a mock OpenSearch backend, POST a log request, verify the LogEntry column in the response contains the error field value.
    • Negative/edge cases:
      • Log entry with no error field — output should be unchanged from current behavior.
      • error field is null or empty string — should not appear in output.
      • error field contains special logfmt characters (spaces, =, quotes) — value should be appropriately quoted.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

Walkthrough

The changes enhance the log entry construction function to capture and include additional fields from OpenSearch documents that were previously ignored, alongside adding comprehensive test coverage to validate the new behavior with complex error objects.

Changes

Cohort / File(s) Summary
Core Log Entry Enhancement
icp_server/opensearch_adapter_service.bal
Modified constructLogEntry() to append extra OpenSearch document fields as key-value pairs to the log output. Added knownFields list to identify pre-handled fields and concatenate remaining non-empty fields as extraFields at the end of the constructed log line.
Test Coverage
icp_server/tests/log_entry_construction_test.bal
Added four test cases validating the enhanced log entry construction, including regression tests for complex error fields, known field deduplication, multiple extra fields, and empty field omission.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A log that once whispered now speaks up loud,
Extra fields dancing, no longer in a crowd,
Complex errors shown, no more hidden away,
The viewer now gleams in the light of the day!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes a clear summary of the problem, solution, and changes made. However, it lacks most sections from the required template (Goals, Approach, User stories, Release note, Documentation, Training, Certification, Marketing, Security checks, etc.). Fill in the missing required template sections: Goals, Approach, User stories, Release note, Documentation, Training, Certification, Marketing, and Security checks to meet repository standards.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing constructLogEntry to include extra open-record fields (e.g., error) and references the linked issue #152.
Linked Issues check ✅ Passed The PR directly addresses the coding requirement in issue #152: constructLogEntry() now appends extra open-record fields as key=value pairs, including the error field that was previously dropped.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving issue #152: modifications to constructLogEntry() logic and comprehensive unit tests for the fix, with no unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +773 to +776
string[] knownFields = ["time", "level", "message", "service_type", "module", "app_name",
"artifact_container", "traceId", "spanId", "icp_runtimeId",
"@timestamp", "log_file_path", "product", "deployment", "app",
"app_module", "class", "icp.runtimeId"];
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.

Log Improvement Suggestion No: 1

Suggested change
string[] knownFields = ["time", "level", "message", "service_type", "module", "app_name",
"artifact_container", "traceId", "spanId", "icp_runtimeId",
"@timestamp", "log_file_path", "product", "deployment", "app",
"app_module", "class", "icp.runtimeId"];
string[] knownFields = ["time", "level", "message", "service_type", "module", "app_name",
"artifact_container", "traceId", "spanId", "icp_runtimeId",
"@timestamp", "log_file_path", "product", "deployment", "app",
"app_module", "class", "icp.runtimeId"];
log.debug("Processing extra fields for log entry construction");

Comment on lines +777 to +778
string extraFields = "";
map<anydata> sourceMap = <map<anydata>>sourceData;
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.

Log Improvement Suggestion No: 2

Suggested change
string extraFields = "";
map<anydata> sourceMap = <map<anydata>>sourceData;
string extraFields = "";
map<anydata> sourceMap = <map<anydata>>sourceData;
if (log.isDebugEnabled()) {
log.debug(string `Checking for extra fields in source map with ${sourceMap.length()} total fields`);
}

Copy link
Copy Markdown
Contributor

@wso2-engineering wso2-engineering bot left a comment

Choose a reason for hiding this comment

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

AI Agent Log Improvement Checklist

⚠️ Warning: AI-Generated Review Comments

  • The log-related comments and suggestions in this review were generated by an AI tool to assist with identifying potential improvements. Purpose of reviewing the code for log improvements is to improve the troubleshooting capabilities of our products.
  • Please make sure to manually review and validate all suggestions before applying any changes. Not every code suggestion would make sense or add value to our purpose. Therefore, you have the freedom to decide which of the suggestions are helpful.

✅ Before merging this pull request:

  • Review all AI-generated comments for accuracy and relevance.
  • Complete and verify the table below. We need your feedback to measure the accuracy of these suggestions and the value they add. If you are rejecting a certain code suggestion, please mention the reason briefly in the suggestion for us to capture it.
Comment Accepted (Y/N) Reason
#### Log Improvement Suggestion No: 1
#### Log Improvement Suggestion No: 2

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
icp_server/tests/log_entry_construction_test.bal (2)

32-38: Use a real nested JSON object for the complex-error fixture.

The current fixture stores error as a pre-serialized string. Using an actual nested object better matches the OpenSearch complex-field scenario.

💡 Proposed fixture refinement
-        "error": "{\"causes\":[{\"message\":\"Connection refused: localhost/127.0.0.1:9445\",\"detail\":{},\"stackTrace\":[]}],\"message\":\"Something wrong with the connection\",\"detail\":{},\"stackTrace\":[]}",
+        "error": {
+            "causes": [
+                {
+                    "message": "Connection refused: localhost/127.0.0.1:9445",
+                    "detail": {},
+                    "stackTrace": []
+                }
+            ],
+            "message": "Something wrong with the connection",
+            "detail": {},
+            "stackTrace": []
+        },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@icp_server/tests/log_entry_construction_test.bal` around lines 32 - 38, The
test fixture in log_entry_construction_test uses sourceJson with the "error"
field set to a serialized string; update the fixture so sourceJson's "error"
property is a real nested JSON object (e.g., an object with "causes", "message",
"detail", "stackTrace") instead of a JSON string so the test mirrors the
OpenSearch complex-field scenario; locate the sourceJson variable in
log_entry_construction_test.bal and replace the string value for "error" with an
actual JSON object structure.

108-110: Align extra-field assertions with logfmt-safe formatting.

This assertion currently hardcodes an unquoted spaced value. If extra-field serialization is made logfmt-safe, this expectation should be updated to quoted output.

💡 Proposed test update
-        test:assertTrue(logEntry.includes("error=Connection refused"), "Log entry must include 'error' extra field");
+        test:assertTrue(logEntry.includes("error=\"Connection refused\""), "Log entry must include quoted 'error' extra field");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@icp_server/tests/log_entry_construction_test.bal` around lines 108 - 110, The
assertions in log_entry_construction_test.bal currently expect unquoted
extra-field values (e.g., test:assertTrue(logEntry.includes("error=Connection
refused"))), which will fail if extra-field serialization is made logfmt-safe;
update the three test:assertTrue assertions that reference logEntry to expect
quoted values instead (check for error="Connection refused",
errorCode="ERR_CONN_REFUSED", and httpStatus="503") so the assertions match
logfmt-safe output while still using the existing logEntry variable and same
test:assertTrue calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@icp_server/opensearch_adapter_service.bal`:
- Around line 779-784: The loop that builds extraFields from sourceMap (foreach
[key, val] in sourceMap.entries()) appends raw strVal which can contain spaces
or quotes and will break logfmt; update the code where extraFields is appended
(currently using string ` ${key}=${strVal}`) to first escape backslashes and
quotes in strVal and wrap the value in double quotes (e.g., transform " -> \"
and \ -> \\), or call a new helper like escapeForLogfmt(strVal) that performs
this escaping, then append using the quoted-escaped value so extraFields and
logfmt tokenization remain correct.

---

Nitpick comments:
In `@icp_server/tests/log_entry_construction_test.bal`:
- Around line 32-38: The test fixture in log_entry_construction_test uses
sourceJson with the "error" field set to a serialized string; update the fixture
so sourceJson's "error" property is a real nested JSON object (e.g., an object
with "causes", "message", "detail", "stackTrace") instead of a JSON string so
the test mirrors the OpenSearch complex-field scenario; locate the sourceJson
variable in log_entry_construction_test.bal and replace the string value for
"error" with an actual JSON object structure.
- Around line 108-110: The assertions in log_entry_construction_test.bal
currently expect unquoted extra-field values (e.g.,
test:assertTrue(logEntry.includes("error=Connection refused"))), which will fail
if extra-field serialization is made logfmt-safe; update the three
test:assertTrue assertions that reference logEntry to expect quoted values
instead (check for error="Connection refused", errorCode="ERR_CONN_REFUSED", and
httpStatus="503") so the assertions match logfmt-safe output while still using
the existing logEntry variable and same test:assertTrue calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d435576-aeec-423b-8ba2-ea4948a88443

📥 Commits

Reviewing files that changed from the base of the PR and between 834f36e and 253b52b.

📒 Files selected for processing (2)
  • icp_server/opensearch_adapter_service.bal
  • icp_server/tests/log_entry_construction_test.bal

Comment on lines +779 to +784
foreach [string, anydata] [key, val] in sourceMap.entries() {
if knownFields.indexOf(key) is () {
string strVal = val.toString();
if strVal != "" && strVal != "()" {
extraFields += string ` ${key}=${strVal}`;
}
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.

⚠️ Potential issue | 🟠 Major

Quote and escape extra-field values before appending to logfmt.

At Line 783, raw strVal is appended directly. Values with spaces/quotes (for example error messages) can break logfmt tokenization and truncate/split field values.

💡 Proposed fix
     foreach [string, anydata] [key, val] in sourceMap.entries() {
         if knownFields.indexOf(key) is () {
             string strVal = val.toString();
             if strVal != "" && strVal != "()" {
-                extraFields += string ` ${key}=${strVal}`;
+                boolean requiresQuotes = strVal.contains(" ") || strVal.contains("=") || strVal.contains("\"");
+                string escapedVal = strVal.replace("\\", "\\\\").replace("\"", "\\\"");
+                string formattedVal = requiresQuotes ? string `"${escapedVal}"` : escapedVal;
+                extraFields += string ` ${key}=${formattedVal}`;
             }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach [string, anydata] [key, val] in sourceMap.entries() {
if knownFields.indexOf(key) is () {
string strVal = val.toString();
if strVal != "" && strVal != "()" {
extraFields += string ` ${key}=${strVal}`;
}
foreach [string, anydata] [key, val] in sourceMap.entries() {
if knownFields.indexOf(key) is () {
string strVal = val.toString();
if strVal != "" && strVal != "()" {
boolean requiresQuotes = strVal.contains(" ") || strVal.contains("=") || strVal.contains("\"");
string escapedVal = strVal.replace("\\", "\\\\").replace("\"", "\\\"");
string formattedVal = requiresQuotes ? string `"${escapedVal}"` : escapedVal;
extraFields += string ` ${key}=${formattedVal}`;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@icp_server/opensearch_adapter_service.bal` around lines 779 - 784, The loop
that builds extraFields from sourceMap (foreach [key, val] in
sourceMap.entries()) appends raw strVal which can contain spaces or quotes and
will break logfmt; update the code where extraFields is appended (currently
using string ` ${key}=${strVal}`) to first escape backslashes and quotes in
strVal and wrap the value in double quotes (e.g., transform " -> \" and \ ->
\\), or call a new helper like escapeForLogfmt(strVal) that performs this
escaping, then append using the quoted-escaped value so extraFields and logfmt
tokenization remain correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ICP] ICP log viewer is not showing error messages

1 participant