Skip to content

Commit

Permalink
Merge pull request #213 from SAP/retain-custom-field-type
Browse files Browse the repository at this point in the history
Optionally retain custom field types
  • Loading branch information
christiand93 authored Sep 20, 2024
2 parents 53fe4ce + 732ab04 commit eb6264d
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 46 deletions.
2 changes: 2 additions & 0 deletions docs/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ gem "just-the-docs"


gem "webrick", "~> 1.7"

gem "jekyll", "~> 3.9"
1 change: 1 addition & 0 deletions docs/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ PLATFORMS

DEPENDENCIES
github-pages (~> 228)
jekyll (~> 3.9)
jekyll-feed (~> 0.12)
just-the-docs
tzinfo (~> 1.2)
Expand Down
10 changes: 8 additions & 2 deletions docs/configuration/custom-fields-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ Example:

Supported format values:

* `application-logging`
* `cloud-logging`
* `application-logging`: to be used with SAP Application Logging
* `cloud-logging`: to be used with SAP Cloud Logging
* `all`: use application-logging and cloud-logging format in parallel.
* `disabled`: do not log any custom fields.
* `default`: set default format cloud-logging.

Additionally, the `customFieldsTypeConversion` setting can be set when logging in cloud-logging format:

* `stringify`: convert all custom field values to strings
* `retain`: keep the original custom field value types

38 changes: 29 additions & 9 deletions docs/general-usage/04-custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,51 @@ permalink: /general-usage/custom-fields

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

## Register custom fields for SAP Application Logging

## Format and type transformation for SAP logging services

The library will automatically detect which logging service is bound to your app and will set the custom field format accordingly.

For local testing purposes you can enforce a specific format like this:

```js
log.setCustomFieldsFormat("application-logging");
// possible values: "disabled", "all", "application-logging", "cloud-logging", "default"
```

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).

### SAP Application Logging

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

Custom fields are converted and printed as string values independent of their original type.

```js
log.registerCustomFields(["field"]);
info("Test data", {"field" :"value"});
info("Test data", {"field": 42});
// ... "msg":"Test data"
// ... "#cf": {"string": [{"k":"field","v":"value","i":"0"}]}...
// ... "#cf": {"string": [{"k":"field","v":"42","i":"0"}]}...
```

## General use
### SAP Cloud Logging

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.
When using the library with SAP Cloud Logging, custom fields get added as top-level fields.
Registering custom fields is not required in this case.

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

```js
log.setCustomFieldsFormat("application-logging");
// possible values: "disabled", "all", "application-logging", "cloud-logging", "default"
log.setCustomFieldsTypeConversion(CustomFieldsTypeConversion.Retain)
info("Test data", {"field": 42});
// ... "msg":"Test data"
// ... "field": 42
```

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).
## Adding custom fields

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:

Expand Down
11 changes: 10 additions & 1 deletion src/lib/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import coreConfig from './default/config-core.json';
import kymaConfig from './default/config-kyma.json';
import requestConfig from './default/config-request.json';
import sapPassportConfig from './default/config-sap-passport.json';
import { ConfigField, ConfigObject, CustomFieldsFormat, Framework, Output, Source, SourceType } from './interfaces';
import { ConfigField, ConfigObject, CustomFieldsFormat, CustomFieldsTypeConversion, Framework, Output, Source, SourceType } from './interfaces';

export default class Config {

Expand Down Expand Up @@ -63,6 +63,7 @@ export default class Config {
Config.instance.setCustomFieldsFormat(CustomFieldsFormat.CloudLogging);
}

Config.instance.setCustomFieldsTypeConversion(CustomFieldsTypeConversion.Stringify)
Config.instance.addConfig(configFiles);
}

Expand Down Expand Up @@ -213,6 +214,10 @@ export default class Config {
Config.instance.config.customFieldsFormat = file.customFieldsFormat;
}

if (file.customFieldsTypeConversion) {
Config.instance.config.customFieldsTypeConversion = file.customFieldsTypeConversion;
}

if (file.framework) {
Config.instance.config.framework = file.framework;
}
Expand All @@ -233,6 +238,10 @@ export default class Config {
Config.instance.config.customFieldsFormat = format;
}

setCustomFieldsTypeConversion(conversion: CustomFieldsTypeConversion) {
Config.instance.config.customFieldsTypeConversion = conversion;
}

setStartupMessageEnabled(enabled: boolean) {
Config.instance.config.outputStartupMsg = enabled;
}
Expand Down
10 changes: 10 additions & 0 deletions src/lib/config/default/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@
],
"type": "string"
},
"CustomFieldsTypeConversion": {
"enum": [
"retain",
"stringify"
],
"type": "string"
},
"DetailName": {
"enum": [
"level",
Expand Down Expand Up @@ -193,6 +200,9 @@
"customFieldsFormat": {
"$ref": "#/definitions/CustomFieldsFormat"
},
"customFieldsTypeConversion": {
"$ref": "#/definitions/CustomFieldsTypeConversion"
},
"fields": {
"items": {
"$ref": "#/definitions/ConfigField"
Expand Down
6 changes: 6 additions & 0 deletions src/lib/config/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface ConfigObject {
fields?: ConfigField[];
customFieldsFormat?: CustomFieldsFormat;
customFieldsTypeConversion?: CustomFieldsTypeConversion;
outputStartupMsg?: boolean;
reqLoggingLevel?: string;
framework?: Framework;
Expand Down Expand Up @@ -60,6 +61,11 @@ export enum CustomFieldsFormat {
Default = "default"
}

export enum CustomFieldsTypeConversion {
Retain = "retain",
Stringify = "stringify"
}

export enum SourceType {
Static = "static",
Env = "env",
Expand Down
23 changes: 14 additions & 9 deletions src/lib/logger/recordFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import jsonStringifySafe from 'json-stringify-safe';
import util from 'util';

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

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

let indexedCustomFields: any = {};
providedFields.forEach((value, key) => {
// Stringify, if necessary.
if ((typeof value) != "string") {
value = jsonStringifySafe(value);
}

if ([CustomFieldsFormat.CloudLogging, CustomFieldsFormat.All, CustomFieldsFormat.Default].includes(customFieldsFormat)
|| record.payload[key] != null || this.config.isSettable(key)) {
// Stringify, if conversion type 'stringify' is selected and value is not a string already.
if (customFieldsTypeConversion == CustomFieldsTypeConversion.Stringify && (typeof value) != "string") {
record.payload[key] = jsonStringifySafe(value);
} else {
record.payload[key] = value;
}
}

if ([CustomFieldsFormat.ApplicationLogging, CustomFieldsFormat.All].includes(customFieldsFormat)) {
indexedCustomFields[key] = value;
// Stringify, if necessary.
if ((typeof value) != "string") {
indexedCustomFields[key] = jsonStringifySafe(value);
} else {
indexedCustomFields[key] = value;
}
}
});

//writes custom fields in the correct order and correlates i to the place in registeredCustomFields
// Write custom fields in the correct order and correlates i to the place in registeredCustomFields
if (Object.keys(indexedCustomFields).length > 0) {
let res: any = {};
res.string = [];
Expand Down
6 changes: 5 additions & 1 deletion src/lib/logger/rootLogger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Config from '../config/config';
import {
ConfigObject, CustomFieldsFormat, Framework, Output, SourceType
ConfigObject, CustomFieldsFormat, CustomFieldsTypeConversion, Framework, Output, SourceType
} from '../config/interfaces';
import EnvService from '../helper/envService';
import Middleware from '../middleware/middleware';
Expand Down Expand Up @@ -48,6 +48,10 @@ export default class RootLogger extends Logger {
return this.config.setCustomFieldsFormat(format);
}

setCustomFieldsTypeConversion(conversion: CustomFieldsTypeConversion) {
return this.config.setCustomFieldsTypeConversion(conversion);
}

setStartupMessageEnabled(enabled: boolean) {
return this.config.setStartupMessageEnabled(enabled);
}
Expand Down
31 changes: 31 additions & 0 deletions src/test/acceptance-test/custom-fields.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,37 @@ describe('Test custom fields', function () {
});
});

describe('Test custom field type conversion', function () {
describe('stringify fields', function () {
beforeEach(function () {
log = importFresh("../../../build/main/index");
log.setCustomFields({ "field-a": 42, "field-b": true, "field-c": { "key": "value" }});
log.logMessage("info", "test-message");
});

it('logs custom fields as strings', function () {
expect(lastOutput).to.have.property('field-a', '42').that.is.a('string');
expect(lastOutput).to.have.property('field-b', 'true').that.is.a('string');
expect(lastOutput).to.have.property('field-c', '{"key":"value"}').that.is.a('string');
});
});

describe('retain field types', function () {
beforeEach(function () {
log = importFresh("../../../build/main/index");
log.setCustomFieldsTypeConversion("retain");
log.setCustomFields({ "field-a": 42, "field-b": true, "field-c": { "key": "value" }});
log.logMessage("info", "test-message");
});

it('logs custom fields with their retained type', function () {
expect(lastOutput).to.have.property('field-a', 42).that.is.a('number');
expect(lastOutput).to.have.property('field-b', true).that.is.a('boolean');
expect(lastOutput).to.have.property('field-c').that.deep.equals({"key":"value"})
});
});
})

describe('Test custom field format', function () {
describe('"cloud-logging" format', function () {
beforeEach(function () {
Expand Down
Loading

0 comments on commit eb6264d

Please sign in to comment.