From 1d8cefb75f2bfb1674f2691e85e1338b5247a123 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:59:36 +0000 Subject: [PATCH] [release-12.3.1] Logs: Fixed prettify JSON behavior with unescaped content (#114407) Logs: Fixed prettify JSON behavior with unescaped content (#114403) * processing: move escaping to a later stage * Regression test (cherry picked from commit c42d6a53a6f7fcb41f8a9ac9d4778fcc801f0578) Co-authored-by: Matias Chomicki --- .../logs/components/panel/processing.test.ts | 39 +++++++++++++++++++ .../logs/components/panel/processing.ts | 9 +++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/public/app/features/logs/components/panel/processing.test.ts b/public/app/features/logs/components/panel/processing.test.ts index 2b58efeee15..5998ff36d21 100644 --- a/public/app/features/logs/components/panel/processing.test.ts +++ b/public/app/features/logs/components/panel/processing.test.ts @@ -190,6 +190,45 @@ describe('preProcessLogs', () => { expect(logListModel.body).not.toBe(entry); }); + test('Prettifies and escapes wrapped JSON', () => { + const entry = '{"key": "value", "otherKey": "other\\nValue"}'; + const logListModel = createLogLine( + { entry, hasUnescapedContent: true }, + { + escape: true, // escape + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, // wrapped + prettifyJSON: true, + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).toBe(`{ + "key": "value", + "otherKey": "other +Value" +}`); + }); + + test('Prettifies JSON without escaping', () => { + const entry = '{"key": "value", "otherKey": "other\\nValue"}'; + const logListModel = createLogLine( + { entry, hasUnescapedContent: true }, + { + escape: false, // escape = false + order: LogsSortOrder.Descending, + timeZone: 'browser', + wrapLogMessage: true, // wrapped + prettifyJSON: true, + } + ); + expect(logListModel.entry).toBe(entry); + expect(logListModel.body).toBe(`{ + "key": "value", + "otherKey": "other\\nValue" +}`); + }); + test('Uses lossless parsing', () => { const entry = '{"number": 90071992547409911}'; const logListModel = createLogLine( diff --git a/public/app/features/logs/components/panel/processing.ts b/public/app/features/logs/components/panel/processing.ts index 21324f942ef..0f3ea0ac6b1 100644 --- a/public/app/features/logs/components/panel/processing.ts +++ b/public/app/features/logs/components/panel/processing.ts @@ -62,6 +62,7 @@ export class LogListModel implements LogRowModel { private _highlightedBody: string | undefined = undefined; private _highlightedLogAttributesTokens: Array | undefined = undefined; private _highlightTokens: Array | undefined = undefined; + private _escapeUnescapedString = false; private _fields: FieldDef[] | undefined = undefined; private _getFieldLinks: GetFieldLinksFn | undefined = undefined; private _prettifyJSON: boolean; @@ -111,11 +112,10 @@ export class LogListModel implements LogRowModel { this._virtualization = virtualization; this._wrapLogMessage = wrapLogMessage; - let raw = log.raw; if (escape && log.hasUnescapedContent) { - raw = escapeUnescapedString(raw); + this._escapeUnescapedString = true; } - this.raw = raw; + this.raw = log.raw; if (config.featureToggles.otelLogsFormatting && this.otelLanguage) { this.labels[OTEL_LOG_LINE_ATTRIBUTES_FIELD_NAME] = getOtelAttributesField(this, wrapLogMessage); @@ -143,6 +143,9 @@ export class LogListModel implements LogRowModel { if (reStringified) { this.raw = reStringified; } + if (this._escapeUnescapedString) { + this.raw = escapeUnescapedString(this.raw); + } } catch (error) {} const raw = this.raw; this._body = this.collapsed