Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added datadog logging adapter #684

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=
MFE_CONFIG_API_URL=
APP_ID=
SUPPORT_URL=https://support.edx.org
SUPPORT_URL=https://support.edx.org
Copy link

Choose a reason for hiding this comment

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

Could you please keep this list alphabetized? So, insert this line into line 8?

DATADOG_ENABLED=false
17,244 changes: 10,384 additions & 6,860 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.2.0",
"@openedx/frontend-build": "14.0.3",
"@openedx/paragon": "22.3.1",
"@openedx/frontend-build": "github:openedx/frontend-build#abdullahwaheed/datadog-logging",
Copy link

Choose a reason for hiding this comment

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

Are you planning to merge this change to master? Or just use this branch on an MFE for testing first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, we'll first merge it in alpha to test it

"@openedx/paragon": "22.2.2",
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
Expand Down Expand Up @@ -75,7 +75,6 @@
"universal-cookie": "4.0.4"
},
"peerDependencies": {
"@openedx/frontend-build": ">= 14.0.0",
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0 || ^17.0.0",
Expand Down
27 changes: 20 additions & 7 deletions src/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import {
getConfig, mergeConfig,
} from './config';
import {
configure as configureLogging, getLoggingService, NewRelicLoggingService, logError,
configure as configureLogging, getLoggingService, NewRelicLoggingService, logError, DatadogLoggingService
} from './logging';
import {
configure as configureAnalytics, SegmentAnalyticsService, identifyAnonymousUser, identifyAuthenticatedUser,
Expand Down Expand Up @@ -287,6 +287,7 @@ function applyOverrideHandlers(overrides) {
*/
export async function initialize({
loggingService = NewRelicLoggingService,
datDogloggingService = DatadogLoggingService,
analyticsService = SegmentAnalyticsService,
authService = AxiosJwtAuthService,
authMiddleware = [],
Expand Down Expand Up @@ -318,15 +319,27 @@ export async function initialize({
// If a service wasn't supplied we fall back to the default parameters on the initialize
// function signature.
const loggingServiceImpl = getConfig().loggingService || loggingService;
const ddloggingServiceImpl = getConfig().datDogloggingService || datDogloggingService;
const analyticsServiceImpl = getConfig().analyticsService || analyticsService;
const authServiceImpl = getConfig().authService || authService;

// Logging
configureLogging(loggingServiceImpl, {
config: getConfig(),
});
await handlers.logging();
publish(APP_LOGGING_INITIALIZED);
if (loggingServiceImpl) {
// new relic Logging
configureLogging(loggingServiceImpl, {
config: getConfig(),
});
await handlers.logging();
publish(APP_LOGGING_INITIALIZED);
}

if (getConfig().DATADOG_ENABLED) {
// Datadog Logging
configureLogging(ddloggingServiceImpl, {
config: getConfig(),
});
await handlers.logging();
publish(APP_LOGGING_INITIALIZED);
}

// Internationalization
configureI18n({
Expand Down
173 changes: 173 additions & 0 deletions src/logging/DataDogLoggingService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
export const MAX_ERROR_LENGTH = 4000;

function fixErrorLength(error) {
if (error.message && error.message.length > MAX_ERROR_LENGTH) {
const processedError = Object.create(error);
processedError.message = processedError.message.substring(0, MAX_ERROR_LENGTH);
return processedError;
}
if (typeof error === 'string' && error.length > MAX_ERROR_LENGTH) {
return error.substring(0, MAX_ERROR_LENGTH);
}
return error;
}

const pageActionNameInfo = 'INFO';
const pageActionNameIgnoredError = 'IGNORED_ERROR';

function sendPageAction(actionName, message, customAttributes) {
if (process.env.NODE_ENV === 'development') {
console.log(actionName, message, customAttributes); // eslint-disable-line
}
if (window && typeof window.datadog !== 'undefined') {
// https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/addpageaction/
window.datadog.log(message, customAttributes);
}
}

function sendError(error, customAttributes) {
if (process.env.NODE_ENV === 'development') {
console.error(error, customAttributes); // eslint-disable-line
}
if (window && typeof window.datadog !== 'undefined') {
// https://docs.datadoghq.com/real_user_monitoring/browser/collecting_browser_errors/?tab=npm#collect-errors-manually
window.datadog.error(fixErrorLength(error), customAttributes);
}
}

function setCustomAttribute(name, value) {
if (process.env.NODE_ENV === 'development') {
console.log(name, value); // eslint-disable-line
}
if (window && typeof window.newrelic !== 'undefined') {
// https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setcustomattribute/
window.newrelic.setCustomAttribute(name, value);
}
}

/**
* The NewRelicLoggingService is a concrete implementation of the logging service interface that
* sends messages to NewRelic that can be seen in NewRelic Browser and NewRelic Insights. When in
* development mode, all messages will instead be sent to the console.
*
* When you use `logError`, your errors will be checked to see if they're ignored *or* not.
* Not-ignored errors will appear under "JS errors" for your Browser application.
*
* ```
* SELECT * from JavaScriptError WHERE errorStatus is not null SINCE 10 days ago
* ```
*
* Ignored errors will appear in New Relic Insights as page actions, which can be queried:
*
* ```
* SELECT * from PageAction WHERE actionName = 'IGNORED_ERROR' SINCE 1 hour ago
* ```
*
* When using `logInfo`, these only appear in New Relic Insights when querying for page actions as
* follows:
*
* ```
* SELECT * from PageAction WHERE actionName = 'INFO' SINCE 1 hour ago
* ```
*
* You can also add your own custom metrics as an additional argument, or see the code to find
* other standard custom attributes. By default, userId is added (via setCustomAttribute) for logged
* in users via the auth service (AuthAxiosJwtService).
*
* Requires the NewRelic Browser JavaScript snippet.
*
* @implements {LoggingService}
* @memberof module:Logging
*/
export default class DatadogLoggingService {
constructor(options) {
const config = options ? options.config : undefined;
/*
String which is an explicit error message regex. If an error message matches the regex, the error
is considered an *ignored* error and submitted to New Relic as a page action - not an error.

Ignored error regexes are configured per frontend application (MFE).

The regex for all ignored errors are represented in the .env files as a single string. If you need to
ignore multiple errors, use the standard `|` regex syntax.

For example, here's a .env line which ignores two specific errors:

IGNORED_ERROR_REGEX='^\\[frontend-auth\\] Unimportant Error|Specific non-critical error #[\\d]+'

This example would ignore errors with the following messages:

[frontend-app-generic] - Specific non-critical error #45678 happened.
[frontend-app-generic] - Specific non-critical error #93475 happened.
[frontend-auth] Unimportant Error: Browser strangeness occurred.

To test your regex additions, use a JS CLI environment (such as node) and run code like this:

x = new RegExp('^\\[frontend-auth\\] Unimportant Error|Specific non-critical error #[\\d]+');
'[frontend-app-generic] - Specific non-critical error #45678 happened.'.match(x);
'[frontend-auth] Unimportant Error: Browser strangeness occurred.'.match(x);
'This error should not match anything!'.match(x);

For edx.org, add new error message regexes in edx-internal YAML as needed.
*/
this.ignoredErrorRegexes = config ? config.IGNORED_ERROR_REGEX : undefined;
}

/**
*
*
* @param {*} infoStringOrErrorObject
* @param {*} [customAttributes={}]
* @memberof DatadogLoggingService
*/
logInfo(infoStringOrErrorObject, customAttributes = {}) {
let message = infoStringOrErrorObject;
// let customAttrs = customAttributes;
if (typeof infoStringOrErrorObject === 'object' && 'message' in infoStringOrErrorObject) {
// const infoCustomAttributes = infoStringOrErrorObject.customAttributes || {};
// customAttrs = { ...infoCustomAttributes, ...customAttributes };
message = infoStringOrErrorObject.message;
}
window.datadog.info(message, customAttributes);
}

/**
*
*
* @param {*} errorStringOrObject
* @param {*} [customAttributes={}]
* @memberof DatadogLoggingService
*/
logError(errorStringOrObject, customAttributes = {}) {
const errorCustomAttributes = errorStringOrObject.customAttributes || {};
let allCustomAttributes = { ...errorCustomAttributes, ...customAttributes };
if (Object.keys(allCustomAttributes).length === 0) {
// noticeError expects undefined if there are no custom attributes.
allCustomAttributes = undefined;
}

/*
Separate the errors into ignored errors and other errors.
Ignored errors are logged via adding a page action.
Other errors are logged via noticeError and count as "JS Errors" for the application.
*/
const errorMessage = errorStringOrObject.message || (typeof errorStringOrObject === 'string' ? errorStringOrObject : '');
if (this.ignoredErrorRegexes && errorMessage.match(this.ignoredErrorRegexes)) {
/* ignored error */
sendPageAction(pageActionNameIgnoredError, errorMessage, allCustomAttributes);
} else {
/* error! */
sendError(errorStringOrObject, allCustomAttributes);
}
}

/**
* Sets a custom attribute that will be included with all subsequent log messages.
*
* @param {string} name
* @param {string|number|null} value
*/
setCustomAttribute(name, value) {
setCustomAttribute(name, value);
}
}
1 change: 1 addition & 0 deletions src/logging/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export {
logError,
} from './interface';
export { default as NewRelicLoggingService } from './NewRelicLoggingService';
export { default as DatadogLoggingService } from './DatadogLoggingService';
export { default as MockLoggingService } from './MockLoggingService';
Loading