Skip to content
This repository was archived by the owner on Mar 11, 2026. It is now read-only.

Commit cb5f424

Browse files
authored
fix: Logging to stdout in Cloud Run creates a JSON object as "message" (#1305)
* fix: Logging to stdout in Cloud Run creates a JSON object as "message" * Add test * Add comments to code * Add more tests to cover different data types
1 parent 7f778db commit cb5f424

3 files changed

Lines changed: 79 additions & 8 deletions

File tree

src/entry.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ export interface StructuredJson {
8686
// Properties not supported by all agents (e.g. Cloud Run, Functions)
8787
logName?: string;
8888
resource?: object;
89+
// Properties to be stored in jsonPayload when running in serverless (e.g. Cloud Run , Functions)
90+
[key: string]: unknown;
8991
}
9092

9193
export interface ToJsonOptions {
@@ -202,7 +204,7 @@ class Entry {
202204
toJSON(options: ToJsonOptions = {}, projectId = '') {
203205
const entry: EntryJson = extend(true, {}, this.metadata) as {} as EntryJson;
204206
// Format log message
205-
if (Object.prototype.toString.call(this.data) === '[object Object]') {
207+
if (this.isObject(this.data)) {
206208
entry.jsonPayload = objToStruct(this.data, {
207209
removeCircular: !!options.removeCircular,
208210
stringify: true,
@@ -243,7 +245,7 @@ class Entry {
243245
* Serialize an entry to a standard format for any transports, e.g. agents.
244246
* Read more: https://cloud.google.com/logging/docs/structured-logging
245247
*/
246-
toStructuredJSON(projectId = '') {
248+
toStructuredJSON(projectId = '', useMessageField = true) {
247249
const meta = this.metadata;
248250
// Mask out the keys that need to be renamed.
249251
/* eslint-disable @typescript-eslint/no-unused-vars */
@@ -260,7 +262,7 @@ class Entry {
260262
...validKeys
261263
} = meta;
262264
/* eslint-enable @typescript-eslint/no-unused-vars */
263-
const entry: StructuredJson = extend(true, {}, validKeys) as {};
265+
let entry: StructuredJson = extend(true, {}, validKeys) as {};
264266
// Re-map keys names.
265267
entry[LABELS_KEY] = meta.labels
266268
? Object.assign({}, meta.labels)
@@ -273,9 +275,29 @@ class Entry {
273275
? meta.traceSampled
274276
: undefined;
275277
// Format log payload.
276-
entry.message =
277-
meta.textPayload || meta.jsonPayload || meta.protoPayload || undefined;
278-
entry.message = this.data || entry.message;
278+
const data =
279+
this.data ||
280+
meta.textPayload ||
281+
meta.jsonPayload ||
282+
meta.protoPayload ||
283+
undefined;
284+
if (useMessageField) {
285+
/** If useMessageField is set, we add the payload to {@link StructuredJson#message} field.*/
286+
entry.message = data;
287+
} else {
288+
/** useMessageField is false, we add the structured payload to {@link StructuredJson} key-value map.
289+
* It could be especially useful for serverless environments like Cloud Run/Functions when stdout transport is used.
290+
* Note that text still added to {@link StructuredJson#message} field for text payload since it does not have fields within. */
291+
if (data !== undefined && data !== null) {
292+
if (this.isObject(data)) {
293+
entry = extend(true, {}, entry, data);
294+
} else if (typeof data === 'string') {
295+
entry.message = data;
296+
} else {
297+
entry.message = JSON.stringify(data);
298+
}
299+
}
300+
}
279301
// Format timestamp
280302
if (meta.timestamp instanceof Date) {
281303
entry.timestamp = meta.timestamp.toISOString();
@@ -333,6 +355,15 @@ class Entry {
333355
}
334356
return serializedEntry;
335357
}
358+
359+
/**
360+
* Determines whether `value` is a JavaScript object.
361+
* @param value The value to be checked
362+
* @returns true if `value` is a JavaScript object, false otherwise
363+
*/
364+
private isObject(value: unknown): value is object {
365+
return Object.prototype.toString.call(value) === '[object Object]';
366+
}
336367
}
337368

338369
/**

src/log-sync.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ import {
3030
WriteOptions,
3131
} from './utils/log-common';
3232

33+
export interface LogSyncOptions {
34+
// The flag indicating if "message" field should be used to store structured,
35+
// non-text data inside jsonPayload field. By default this value is true
36+
useMessageField?: boolean;
37+
}
38+
3339
/**
3440
* A logSync is a named collection of entries in structured log format. In Cloud
3541
* Logging, structured logs refer to log entries that use the jsonPayload field
@@ -61,9 +67,16 @@ class LogSync implements LogSeverityFunctions {
6167
logging: Logging;
6268
name: string;
6369
transport: Writable;
70+
useMessageField_: boolean;
6471

6572
// not projectId, formattedname is expected
66-
constructor(logging: Logging, name: string, transport?: Writable) {
73+
constructor(
74+
logging: Logging,
75+
name: string,
76+
transport?: Writable,
77+
options?: LogSyncOptions
78+
) {
79+
options = options || {};
6780
this.formattedName_ = formatLogName(logging.projectId, name);
6881
this.logging = logging;
6982
/**
@@ -73,6 +86,7 @@ class LogSync implements LogSeverityFunctions {
7386
this.name = this.formattedName_.split('/').pop()!;
7487
// Default to writing to stdout
7588
this.transport = transport || process.stdout;
89+
this.useMessageField_ = options.useMessageField ?? true;
7690
}
7791

7892
/**
@@ -417,7 +431,10 @@ class LogSync implements LogSeverityFunctions {
417431
if (!(entry instanceof Entry)) {
418432
entry = this.entry(entry);
419433
}
420-
return entry.toStructuredJSON(this.logging.projectId);
434+
return entry.toStructuredJSON(
435+
this.logging.projectId,
436+
this.useMessageField_
437+
);
421438
});
422439
for (const entry of structuredEntries) {
423440
entry.logName = this.formattedName_;

test/entry.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,5 +368,28 @@ describe('Entry', () => {
368368
assert.strictEqual(json[entryTypes.SPAN_ID_KEY], '1');
369369
assert.strictEqual(json[entryTypes.TRACE_SAMPLED_KEY], false);
370370
});
371+
372+
it('should add message field for structured data', () => {
373+
entry.data = {message: 'message', test: 'test'};
374+
let json = entry.toStructuredJSON();
375+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
376+
assert(((json.message as any).message = 'message'));
377+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
378+
assert(((json.message as any).test = 'test'));
379+
json = entry.toStructuredJSON(undefined, false);
380+
assert((json.message = 'message'));
381+
assert((json.test = 'test'));
382+
});
383+
384+
it('should add message field only when needed', () => {
385+
entry.data = 1;
386+
let json = entry.toStructuredJSON();
387+
assert((json.message = '1'));
388+
json = entry.toStructuredJSON(undefined, false);
389+
assert((json.message = '1'));
390+
entry.data = 'test';
391+
json = entry.toStructuredJSON(undefined, false);
392+
assert((json.message = 'test'));
393+
});
371394
});
372395
});

0 commit comments

Comments
 (0)