Skip to content

Commit eb6264d

Browse files
authored
Merge pull request #213 from SAP/retain-custom-field-type
Optionally retain custom field types
2 parents 53fe4ce + 732ab04 commit eb6264d

File tree

12 files changed

+150
-46
lines changed

12 files changed

+150
-46
lines changed

docs/Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ gem "just-the-docs"
3333

3434

3535
gem "webrick", "~> 1.7"
36+
37+
gem "jekyll", "~> 3.9"

docs/Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ PLATFORMS
287287

288288
DEPENDENCIES
289289
github-pages (~> 228)
290+
jekyll (~> 3.9)
290291
jekyll-feed (~> 0.12)
291292
just-the-docs
292293
tzinfo (~> 1.2)

docs/configuration/custom-fields-format.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ Example:
2121

2222
Supported format values:
2323

24-
* `application-logging`
25-
* `cloud-logging`
24+
* `application-logging`: to be used with SAP Application Logging
25+
* `cloud-logging`: to be used with SAP Cloud Logging
2626
* `all`: use application-logging and cloud-logging format in parallel.
2727
* `disabled`: do not log any custom fields.
2828
* `default`: set default format cloud-logging.
29+
30+
Additionally, the `customFieldsTypeConversion` setting can be set when logging in cloud-logging format:
31+
32+
* `stringify`: convert all custom field values to strings
33+
* `retain`: keep the original custom field value types
34+

docs/general-usage/04-custom-fields.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,51 @@ permalink: /general-usage/custom-fields
1010

1111
You can use the custom field feature to add custom fields to your logs.
1212

13-
## Register custom fields for SAP Application Logging
13+
14+
## Format and type transformation for SAP logging services
15+
16+
The library will automatically detect which logging service is bound to your app and will set the custom field format accordingly.
17+
18+
For local testing purposes you can enforce a specific format like this:
19+
20+
```js
21+
log.setCustomFieldsFormat("application-logging");
22+
// possible values: "disabled", "all", "application-logging", "cloud-logging", "default"
23+
```
24+
25+
Alternatively, you can force the logging format by setting a configuration file as explained in [Custom Fields Format](/cf-nodejs-logging-support/configuration/custom-fields-format).
26+
27+
### SAP Application Logging
1428

1529
In case you want to use this library with SAP Application Logging Service you need to register your custom fields.
1630
Please make sure to call the registration exactly once globally before logging any custom fields.
1731
Registered fields will be indexed based on the order given by the provided field array.
1832

33+
Custom fields are converted and printed as string values independent of their original type.
34+
1935
```js
2036
log.registerCustomFields(["field"]);
21-
info("Test data", {"field" :"value"});
37+
info("Test data", {"field": 42});
2238
// ... "msg":"Test data"
23-
// ... "#cf": {"string": [{"k":"field","v":"value","i":"0"}]}...
39+
// ... "#cf": {"string": [{"k":"field","v":"42","i":"0"}]}...
2440
```
2541

26-
## General use
42+
### SAP Cloud Logging
2743

28-
As of version 6.5.0 this library will automatically detect which logging service your app is bound to and will set the logging format accordingly.
44+
When using the library with SAP Cloud Logging, custom fields get added as top-level fields.
45+
Registering custom fields is not required in this case.
2946

30-
For local testing purposes you can still enforce a specific format like this:
47+
By default all custom field values get stringified, which can help to prevent field type mismatches when indexing to SAP Cloud Logging.
48+
However, if you want to retain the original type of your custom fields, you can change the type conversion configuration accordingly:
3149

3250
```js
33-
log.setCustomFieldsFormat("application-logging");
34-
// possible values: "disabled", "all", "application-logging", "cloud-logging", "default"
51+
log.setCustomFieldsTypeConversion(CustomFieldsTypeConversion.Retain)
52+
info("Test data", {"field": 42});
53+
// ... "msg":"Test data"
54+
// ... "field": 42
3555
```
3656

37-
Alternatively, you can force the logging format by setting a configuration file as explained in [Custom Fields Format](/cf-nodejs-logging-support/configuration/custom-fields-format).
57+
## Adding custom fields
3858

3959
In addition to logging messages as described in [Message Logs](/cf-nodejs-logging-support/general-usage/message-logs) you can attach custom fields as an object of key-value pairs as last parameter:
4060

src/lib/config/config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import coreConfig from './default/config-core.json';
77
import kymaConfig from './default/config-kyma.json';
88
import requestConfig from './default/config-request.json';
99
import sapPassportConfig from './default/config-sap-passport.json';
10-
import { ConfigField, ConfigObject, CustomFieldsFormat, Framework, Output, Source, SourceType } from './interfaces';
10+
import { ConfigField, ConfigObject, CustomFieldsFormat, CustomFieldsTypeConversion, Framework, Output, Source, SourceType } from './interfaces';
1111

1212
export default class Config {
1313

@@ -63,6 +63,7 @@ export default class Config {
6363
Config.instance.setCustomFieldsFormat(CustomFieldsFormat.CloudLogging);
6464
}
6565

66+
Config.instance.setCustomFieldsTypeConversion(CustomFieldsTypeConversion.Stringify)
6667
Config.instance.addConfig(configFiles);
6768
}
6869

@@ -213,6 +214,10 @@ export default class Config {
213214
Config.instance.config.customFieldsFormat = file.customFieldsFormat;
214215
}
215216

217+
if (file.customFieldsTypeConversion) {
218+
Config.instance.config.customFieldsTypeConversion = file.customFieldsTypeConversion;
219+
}
220+
216221
if (file.framework) {
217222
Config.instance.config.framework = file.framework;
218223
}
@@ -233,6 +238,10 @@ export default class Config {
233238
Config.instance.config.customFieldsFormat = format;
234239
}
235240

241+
setCustomFieldsTypeConversion(conversion: CustomFieldsTypeConversion) {
242+
Config.instance.config.customFieldsTypeConversion = conversion;
243+
}
244+
236245
setStartupMessageEnabled(enabled: boolean) {
237246
Config.instance.config.outputStartupMsg = enabled;
238247
}

src/lib/config/default/config-schema.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@
105105
],
106106
"type": "string"
107107
},
108+
"CustomFieldsTypeConversion": {
109+
"enum": [
110+
"retain",
111+
"stringify"
112+
],
113+
"type": "string"
114+
},
108115
"DetailName": {
109116
"enum": [
110117
"level",
@@ -193,6 +200,9 @@
193200
"customFieldsFormat": {
194201
"$ref": "#/definitions/CustomFieldsFormat"
195202
},
203+
"customFieldsTypeConversion": {
204+
"$ref": "#/definitions/CustomFieldsTypeConversion"
205+
},
196206
"fields": {
197207
"items": {
198208
"$ref": "#/definitions/ConfigField"

src/lib/config/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface ConfigObject {
22
fields?: ConfigField[];
33
customFieldsFormat?: CustomFieldsFormat;
4+
customFieldsTypeConversion?: CustomFieldsTypeConversion;
45
outputStartupMsg?: boolean;
56
reqLoggingLevel?: string;
67
framework?: Framework;
@@ -60,6 +61,11 @@ export enum CustomFieldsFormat {
6061
Default = "default"
6162
}
6263

64+
export enum CustomFieldsTypeConversion {
65+
Retain = "retain",
66+
Stringify = "stringify"
67+
}
68+
6369
export enum SourceType {
6470
Static = "static",
6571
Env = "env",

src/lib/logger/recordFactory.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import jsonStringifySafe from 'json-stringify-safe';
22
import util from 'util';
33

44
import Config from '../config/config';
5-
import { CustomFieldsFormat, Output } from '../config/interfaces';
5+
import { CustomFieldsFormat, CustomFieldsTypeConversion, Output } from '../config/interfaces';
66
import StacktraceUtils from '../helper/stacktraceUtils';
77
import { isValidObject } from '../middleware/utils';
88
import Cache from './cache';
@@ -102,6 +102,7 @@ export default class RecordFactory {
102102
customFieldsFromArgs: Map<string, any> = new Map()) {
103103
const providedFields = new Map<string, any>([...loggerCustomFields, ...customFieldsFromArgs]);
104104
const customFieldsFormat = this.config.getConfig().customFieldsFormat!;
105+
const customFieldsTypeConversion = this.config.getConfig().customFieldsTypeConversion!;
105106

106107
// if format "disabled", do not log any custom fields
107108
if (customFieldsFormat == CustomFieldsFormat.Disabled) {
@@ -110,23 +111,27 @@ export default class RecordFactory {
110111

111112
let indexedCustomFields: any = {};
112113
providedFields.forEach((value, key) => {
113-
// Stringify, if necessary.
114-
if ((typeof value) != "string") {
115-
value = jsonStringifySafe(value);
116-
}
117-
118114
if ([CustomFieldsFormat.CloudLogging, CustomFieldsFormat.All, CustomFieldsFormat.Default].includes(customFieldsFormat)
119115
|| record.payload[key] != null || this.config.isSettable(key)) {
116+
// Stringify, if conversion type 'stringify' is selected and value is not a string already.
117+
if (customFieldsTypeConversion == CustomFieldsTypeConversion.Stringify && (typeof value) != "string") {
118+
record.payload[key] = jsonStringifySafe(value);
119+
} else {
120120
record.payload[key] = value;
121-
121+
}
122122
}
123123

124124
if ([CustomFieldsFormat.ApplicationLogging, CustomFieldsFormat.All].includes(customFieldsFormat)) {
125-
indexedCustomFields[key] = value;
125+
// Stringify, if necessary.
126+
if ((typeof value) != "string") {
127+
indexedCustomFields[key] = jsonStringifySafe(value);
128+
} else {
129+
indexedCustomFields[key] = value;
130+
}
126131
}
127132
});
128133

129-
//writes custom fields in the correct order and correlates i to the place in registeredCustomFields
134+
// Write custom fields in the correct order and correlates i to the place in registeredCustomFields
130135
if (Object.keys(indexedCustomFields).length > 0) {
131136
let res: any = {};
132137
res.string = [];

src/lib/logger/rootLogger.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Config from '../config/config';
22
import {
3-
ConfigObject, CustomFieldsFormat, Framework, Output, SourceType
3+
ConfigObject, CustomFieldsFormat, CustomFieldsTypeConversion, Framework, Output, SourceType
44
} from '../config/interfaces';
55
import EnvService from '../helper/envService';
66
import Middleware from '../middleware/middleware';
@@ -48,6 +48,10 @@ export default class RootLogger extends Logger {
4848
return this.config.setCustomFieldsFormat(format);
4949
}
5050

51+
setCustomFieldsTypeConversion(conversion: CustomFieldsTypeConversion) {
52+
return this.config.setCustomFieldsTypeConversion(conversion);
53+
}
54+
5155
setStartupMessageEnabled(enabled: boolean) {
5256
return this.config.setStartupMessageEnabled(enabled);
5357
}

src/test/acceptance-test/custom-fields.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,37 @@ describe('Test custom fields', function () {
122122
});
123123
});
124124

125+
describe('Test custom field type conversion', function () {
126+
describe('stringify fields', function () {
127+
beforeEach(function () {
128+
log = importFresh("../../../build/main/index");
129+
log.setCustomFields({ "field-a": 42, "field-b": true, "field-c": { "key": "value" }});
130+
log.logMessage("info", "test-message");
131+
});
132+
133+
it('logs custom fields as strings', function () {
134+
expect(lastOutput).to.have.property('field-a', '42').that.is.a('string');
135+
expect(lastOutput).to.have.property('field-b', 'true').that.is.a('string');
136+
expect(lastOutput).to.have.property('field-c', '{"key":"value"}').that.is.a('string');
137+
});
138+
});
139+
140+
describe('retain field types', function () {
141+
beforeEach(function () {
142+
log = importFresh("../../../build/main/index");
143+
log.setCustomFieldsTypeConversion("retain");
144+
log.setCustomFields({ "field-a": 42, "field-b": true, "field-c": { "key": "value" }});
145+
log.logMessage("info", "test-message");
146+
});
147+
148+
it('logs custom fields with their retained type', function () {
149+
expect(lastOutput).to.have.property('field-a', 42).that.is.a('number');
150+
expect(lastOutput).to.have.property('field-b', true).that.is.a('boolean');
151+
expect(lastOutput).to.have.property('field-c').that.deep.equals({"key":"value"})
152+
});
153+
});
154+
})
155+
125156
describe('Test custom field format', function () {
126157
describe('"cloud-logging" format', function () {
127158
beforeEach(function () {

0 commit comments

Comments
 (0)