diff --git a/docs/docs/feature-library/00_overview.md b/docs/docs/feature-library/00_overview.md index ab8cdffaa..7bbb6ffa3 100644 --- a/docs/docs/feature-library/00_overview.md +++ b/docs/docs/feature-library/00_overview.md @@ -23,6 +23,7 @@ The **Flex Project Template** comes with a set of features enabled by default wi | [Conference (external)](conference) | _provide agents the ability to conference in external numbers_ | | [Conversation Transfer](conversation-transfer) | _introduce conversation-based messaging transfer functionality for agents_ | | [Custom Transfer Directory](custom-transfer-directory) | _customize the agent and queue transfer directories_ | +| [Datadog Log Integration](datadog-log-integration) | _forward logs emitted by the template to datadog_ | [Device Manager](device-manager) | _provide agents the ability to select the audio output device_ | | [Dispositions](dispositions) | _provide agents the ability to select a disposition/wrap-up code and enter notes_ | | [Emoji Picker](emoji-picker) | _adds an emoji picker for messaging tasks_ | diff --git a/docs/docs/feature-library/datadog-log-integration.md b/docs/docs/feature-library/datadog-log-integration.md new file mode 100644 index 000000000..f42fca30c --- /dev/null +++ b/docs/docs/feature-library/datadog-log-integration.md @@ -0,0 +1,37 @@ +--- +sidebar_label: datadog-log-integration +title: datadog-log-integration +--- + +This feature forwards logs emitted by the Flex Project Template to Datadog. + +### Configuration + +This feature allows the following configuration settings. + +![datadog-log-integration settings](../../static/img/features/datadog-log-integration/settings.png) + +| Setting | Description | +| --------| ------------| +| Api key | Your Datadog Account [client token](https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens) +| Log Level | The minimum log level to send to Datadog. See [Logging](/flex-project-template/building/template-utilities/logging) for more details. +| Intake Region | The [Datadog Site](https://docs.datadoghq.com/getting_started/site/) for your account. Valid values are `us` | `us5` | `us3` | `eu`. +| Flush Timeout | In milliseconds. Because we send logs to datadog over HTTP - we do not want to make an HTTP request for every log written. This feature will buffer log messages for the flush timeout before making a single HTTP request to Datadog with all buffered log messages. If there are no logs within the Flush timeout, there is no HTTP request to Datadog. | + +### Flex User Experience + +Logs are forwarded automatically with no indication to the Flex User. + +### Dependencies + +You will need a Datadog account, with a [client token](https://docs.datadoghq.com/account_management/api-app-keys/#client-tokens), as well as your intake region, or [Datadog Site](https://docs.datadoghq.com/getting_started/site/). + +_Client tokens are safe for use in browsers._ + +### Metadata + +All logs sent to Datadog are decorated with common metadata. In this case, we add the worker name and workerSid to every log message sent to Datadog automatically! + +### Note + +Logs are forwarded to Datadog through HTTP, not the datadog sdk client. This is done to save build size of the template. If you're looking for a tighter integration with Datadog, consider a custom feature that implements their client sdk. diff --git a/docs/static/img/features/datadog-log-integration/settings.png b/docs/static/img/features/datadog-log-integration/settings.png new file mode 100644 index 000000000..75a392e30 Binary files /dev/null and b/docs/static/img/features/datadog-log-integration/settings.png differ diff --git a/flex-config/ui_attributes.common.json b/flex-config/ui_attributes.common.json index 9d5f58b4c..77020c3b3 100644 --- a/flex-config/ui_attributes.common.json +++ b/flex-config/ui_attributes.common.json @@ -351,6 +351,13 @@ }, "sip_support": { "enabled": false + }, + "datadog_log_integration": { + "enabled": false, + "log_level": "info", + "api_key": "", + "intake_region": "", + "flush_timeout": 5000 } } } diff --git a/plugin-flex-ts-template-v2/.prettierignore b/plugin-flex-ts-template-v2/.prettierignore index 42061c01a..b43bf86b5 100644 --- a/plugin-flex-ts-template-v2/.prettierignore +++ b/plugin-flex-ts-template-v2/.prettierignore @@ -1 +1 @@ -README.md \ No newline at end of file +README.md diff --git a/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/config.ts b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/config.ts new file mode 100644 index 000000000..4b02ac514 --- /dev/null +++ b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/config.ts @@ -0,0 +1,30 @@ +import { getFeatureFlags } from '../../utils/configuration'; +import DatadogLogIntegrationConfig from './types/ServiceConfiguration'; + +const { + enabled = false, + api_key, + intake_region, + flush_timeout, + log_level, +} = (getFeatureFlags()?.features?.datadog_log_integration as DatadogLogIntegrationConfig) || {}; + +export const isFeatureEnabled = () => { + return enabled; +}; + +export const getApiKey = () => { + return api_key; +}; + +export const getIntakeRegion = () => { + return intake_region; +}; + +export const getFlushTimeout = () => { + return flush_timeout; +}; + +export const getLogLevel = () => { + return log_level; +}; diff --git a/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/destination/Datadog.ts b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/destination/Datadog.ts new file mode 100644 index 000000000..58a0826ce --- /dev/null +++ b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/destination/Datadog.ts @@ -0,0 +1,75 @@ +import querystring from 'querystring'; + +import Destination from '../../../utils/logger/destination'; +import { LogLevel } from '../../../utils/logger'; +import { DatadogDestinationConfig } from '../types/ServiceConfiguration'; + +export default class DatadogDestination extends Destination { + buffer: any[] = []; + + api: string = ''; + + constructor(opts: DatadogDestinationConfig) { + super({ minLogLevel: opts.minLogLevel }); + + const { flushTimeout, apiKey, intakeRegion } = opts; + + if (intakeRegion === 'eu') { + this.api = `https://http-intake.logs.datadoghq.eu/api/v2/logs`; + } else if (intakeRegion === 'us3') { + this.api = `https://http-intake.logs.us3.datadoghq.com/api/v2/logs`; + } else if (intakeRegion === 'us5') { + this.api = `https://http-intake.logs.us5.datadoghq.com/api/v2/logs`; + } else { + this.api = `https://http-intake.logs.datadoghq.com/api/v2/logs`; + } + + const query = { + 'dd-api-key': apiKey, + }; + const qs = querystring.encode(query); + + this.api = `${this.api}?${qs}`; + + setInterval(() => { + this.flush(); + }, flushTimeout); + } + + async handle(level: LogLevel, message: string, context: any, meta: any): Promise { + return new Promise((resolve) => { + this.buffer.push({ + level, + message, + context, + meta, + }); + return resolve(); + }); + } + + hasUnsentLogs(): boolean { + return Boolean(this.buffer.length); + } + + async flush(): Promise { + if (!this.hasUnsentLogs()) { + return; + } + + try { + await fetch(this.api, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(this.buffer), + }); + } catch (err) { + console.error(err); + } finally { + // reset the buffer + this.buffer = []; + } + } +} diff --git a/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/flex-hooks/logger/index.ts b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/flex-hooks/logger/index.ts new file mode 100644 index 000000000..6966e6932 --- /dev/null +++ b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/flex-hooks/logger/index.ts @@ -0,0 +1,11 @@ +import Datadog from '../../destination/Datadog'; +import { getLogLevel, getApiKey, getIntakeRegion, getFlushTimeout } from '../../config'; + +export const loggerHook = function sendLogsToDataDog() { + return new Datadog({ + apiKey: getApiKey(), + intakeRegion: getIntakeRegion(), + flushTimeout: getFlushTimeout(), + minLogLevel: getLogLevel(), + }); +}; diff --git a/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/index.ts b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/index.ts new file mode 100644 index 000000000..8e6fa5ee8 --- /dev/null +++ b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/index.ts @@ -0,0 +1,9 @@ +import { FeatureDefinition } from '../../types/feature-loader'; +import { isFeatureEnabled } from './config'; +// @ts-ignore +import hooks from './flex-hooks/**/*.*'; + +export const register = (): FeatureDefinition => { + if (!isFeatureEnabled()) return {}; + return { name: 'datadog-log-integration', hooks: typeof hooks === 'undefined' ? [] : hooks }; +}; diff --git a/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/types/ServiceConfiguration.ts b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/types/ServiceConfiguration.ts new file mode 100644 index 000000000..793540e12 --- /dev/null +++ b/plugin-flex-ts-template-v2/src/feature-library/datadog-log-integration/types/ServiceConfiguration.ts @@ -0,0 +1,16 @@ +import { LogLevel } from 'utils/logger'; + +export default interface DatadogLogIntegrationConfig { + enabled: boolean; + log_level: LogLevel; + api_key: string; + intake_region: string; + flush_timeout: number | undefined; +} + +export interface DatadogDestinationConfig { + minLogLevel: LogLevel; + apiKey: string; + intakeRegion: string; + flushTimeout: number | undefined; +} diff --git a/plugin-flex-ts-template-v2/src/feature-library/enhanced-crm-container/custom-components/TabbedCRMTask/TabbedCRMTask.tsx b/plugin-flex-ts-template-v2/src/feature-library/enhanced-crm-container/custom-components/TabbedCRMTask/TabbedCRMTask.tsx index e18549221..2d713d141 100644 --- a/plugin-flex-ts-template-v2/src/feature-library/enhanced-crm-container/custom-components/TabbedCRMTask/TabbedCRMTask.tsx +++ b/plugin-flex-ts-template-v2/src/feature-library/enhanced-crm-container/custom-components/TabbedCRMTask/TabbedCRMTask.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Actions, ITask } from '@twilio/flex-ui'; -import { Flex, Tabs, TabList, Tab, TabPanels, TabPanel } from '@twilio-paste/core'; +import { Flex } from '@twilio-paste/core/flex'; +import { Tabs, TabList, Tab, TabPanels, TabPanel } from '@twilio-paste/core/tabs'; export interface Props { thisTask?: ITask; // task assigned to component diff --git a/plugin-flex-ts-template-v2/src/utils/feature-loader/index.ts b/plugin-flex-ts-template-v2/src/utils/feature-loader/index.ts index 6a9a6e16a..7d1889a71 100644 --- a/plugin-flex-ts-template-v2/src/utils/feature-loader/index.ts +++ b/plugin-flex-ts-template-v2/src/utils/feature-loader/index.ts @@ -19,7 +19,8 @@ import * as TaskRouterReplaceCompleteTask from '../serverless/TaskRouter/Complet import * as Logger from './logger'; import * as SendLogsToBrowserConsole from '../logger/sendLogsToBrowserConsole'; // @ts-ignore -import features from '../../feature-library/*'; +// eslint-disable-next-line import/no-useless-path-segments +import features from '../../feature-library/*/index.ts'; export const initFeatures = (flex: typeof Flex, manager: Flex.Manager) => { if (typeof features === 'undefined') {