diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 04545756c..ec2897067 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,7 +79,7 @@ We prefer lambda functions over the `function` keyword. Simple functions, that r const addTwo = (x: number): number => x + 2 ``` -:-1: Bad: +:-1: Bad: ```typescript= function addTwo (x: number): number { return x + 2 @@ -88,7 +88,7 @@ function addTwo (x: number): number { ### Function naming -Names of functions should +Names of functions should - be as short as possible while clearly communicating their purpose. - not include technical details, if not necessary. - avoid abbreviations. @@ -133,7 +133,7 @@ Example: ```typescript= /** * Calculates the divison of the given divisor and divident. - * + * * @param divisor The divisor for the calculation * @param divident The divident for the calculation * @return The calculated division. @@ -162,6 +162,16 @@ React components - should be named in [PascalCase](https://en.wikipedia.org/wiki/Pascal_case). +### Logging + +- Don't log directly to the console. Use our logging class `Logger` in "src/utils". +- Create one instance of `Logger` per file. Don't pass or share the instances. +- The first argument of the constructor is the scope. Use the name of the class or component whose behaviour you want to log or choose an explanatory name. +- If you want to add a sub scope (because e.g. you have two components that are similar or are used together, like the sub-classes of the iframe communicator), separate the main and sub scope with " > ". +- Scopes should be upper camel case. +- Log messages should never start with a lowercase letter. +- Log messages should never end with a colon or white space. + #### Example File: `increment-number-button.tsx`: ```typescript= @@ -172,16 +182,21 @@ export interface IncrementNumberButtonProps { prefix: string } +const logger = new Logger("IncrementNumberButton") + /** * Shows a button that contains a text and a number that gets incremented each time you click it. - * - * @param prefix A text that should be added before the number. + * + * @param prefix A text that should be added before the number. */ export const IncrementNumberButton: React.FC = ({ prefix }) => { const [counter, setCounter] = useState(0) - - const incrementCounter = useCallback(() => setCounter((lastCounter) => lastCounter + 1), []) - + + const incrementCounter = useCallback(() => { + setCounter((lastCounter) => lastCounter + 1) + logger.info("Increased counter") + }, []) + return } ``` diff --git a/src/components/application-loader/application-loader.tsx b/src/components/application-loader/application-loader.tsx index 1a4d5ed1f..8ce323ae7 100644 --- a/src/components/application-loader/application-loader.tsx +++ b/src/components/application-loader/application-loader.tsx @@ -11,6 +11,9 @@ import { createSetUpTaskList, InitTask } from './initializers' import { LoadingScreen } from './loading-screen' import { useCustomizeAssetsUrl } from '../../hooks/common/use-customize-assets-url' import { useFrontendAssetsUrl } from '../../hooks/common/use-frontend-assets-url' +import { Logger } from '../../utils/logger' + +const log = new Logger('ApplicationLoader') export const ApplicationLoader: React.FC = ({ children }) => { const frontendAssetsUrl = useFrontendAssetsUrl() @@ -36,7 +39,7 @@ export const ApplicationLoader: React.FC = ({ children }) => { useEffect(() => { for (const task of initTasks) { runTask(task.task).catch((reason: Error) => { - console.error(reason) + log.error('Error while initialising application', reason) setFailedTitle(task.name) }) } diff --git a/src/components/application-loader/initializers/fetch-and-set-banner.ts b/src/components/application-loader/initializers/fetch-and-set-banner.ts index c037c5059..2b3db5776 100644 --- a/src/components/application-loader/initializers/fetch-and-set-banner.ts +++ b/src/components/application-loader/initializers/fetch-and-set-banner.ts @@ -6,8 +6,10 @@ import { setBanner } from '../../../redux/banner/methods' import { defaultFetchConfig } from '../../../api/utils' +import { Logger } from '../../../utils/logger' export const BANNER_LOCAL_STORAGE_KEY = 'banner.lastModified' +const log = new Logger('Banner') export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise => { const cachedLastModified = window.localStorage.getItem(BANNER_LOCAL_STORAGE_KEY) @@ -42,7 +44,7 @@ export const fetchAndSetBanner = async (customizeAssetsUrl: string): Promise } +const log = new Logger('CopyOverlay') + export const CopyOverlay: React.FC = ({ content, clickComponent }) => { useTranslation() const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) @@ -27,9 +30,9 @@ export const CopyOverlay: React.FC = ({ content, clickComponen .then(() => { setError(false) }) - .catch(() => { + .catch((error: Error) => { setError(true) - console.error("couldn't copy") + log.error('Copy failed', error) }) .finally(() => { setShowCopiedTooltip(true) diff --git a/src/components/common/copyable/copyable-field/copyable-field.tsx b/src/components/common/copyable/copyable-field/copyable-field.tsx index 38bd23fc1..6001b0533 100644 --- a/src/components/common/copyable/copyable-field/copyable-field.tsx +++ b/src/components/common/copyable/copyable-field/copyable-field.tsx @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next' import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon' import { ShowIf } from '../../show-if/show-if' import { CopyOverlay } from '../copy-overlay' +import { Logger } from '../../../../utils/logger' export interface CopyableFieldProps { content: string @@ -17,6 +18,8 @@ export interface CopyableFieldProps { url?: string } +const log = new Logger('CopyableField') + export const CopyableField: React.FC = ({ content, nativeShareButton, url }) => { useTranslation() const copyButton = useRef(null) @@ -27,8 +30,8 @@ export const CopyableField: React.FC = ({ content, nativeSha text: content, url: url }) - .catch((err) => { - console.error('Native sharing failed: ', err) + .catch((error) => { + log.error('Native sharing failed', error) }) }, [content, url]) diff --git a/src/components/editor-page/document-bar/revisions/utils.ts b/src/components/editor-page/document-bar/revisions/utils.ts index b93605fd7..86f23d994 100644 --- a/src/components/editor-page/document-bar/revisions/utils.ts +++ b/src/components/editor-page/document-bar/revisions/utils.ts @@ -8,6 +8,9 @@ import { Revision } from '../../../../api/revisions/types' import { getUserById } from '../../../../api/users' import { UserResponse } from '../../../../api/users/types' import { download } from '../../../common/download/download' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('RevisionsUtils') export const downloadRevision = (noteId: string, revision: Revision | null): void => { if (!revision) { @@ -26,7 +29,7 @@ export const getUserDataForRevision = (authors: string[]): UserResponse[] => { .then((userData) => { users.push(userData) }) - .catch((error) => console.error(error)) + .catch((error) => log.error(error)) }) return users } diff --git a/src/components/editor-page/editor-page.tsx b/src/components/editor-page/editor-page.tsx index cd8e75dcb..8b4394eb7 100644 --- a/src/components/editor-page/editor-page.tsx +++ b/src/components/editor-page/editor-page.tsx @@ -29,6 +29,7 @@ import { useUpdateLocalHistoryEntry } from './hooks/useUpdateLocalHistoryEntry' import { useApplicationState } from '../../hooks/common/use-application-state' import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer' import { EditorToRendererCommunicatorContextProvider } from './render-context/editor-to-renderer-communicator-context-provider' +import { Logger } from '../../utils/logger' export interface EditorPagePathParams { id: string @@ -39,6 +40,8 @@ export enum ScrollSource { RENDERER } +const log = new Logger('EditorPage') + export const EditorPage: React.FC = () => { useTranslation() const scrollSource = useRef(ScrollSource.EDITOR) @@ -55,7 +58,7 @@ export const EditorPage: React.FC = () => { if (scrollSource.current === ScrollSource.RENDERER && editorSyncScroll) { setScrollState((old) => { const newState = { editorScrollState: newScrollState, rendererScrollState: old.rendererScrollState } - console.debug('[EditorPage] set scroll state because of renderer scroll', newState) + log.debug('Set scroll state because of renderer scroll', newState) return newState }) } @@ -68,7 +71,7 @@ export const EditorPage: React.FC = () => { if (scrollSource.current === ScrollSource.EDITOR && editorSyncScroll) { setScrollState((old) => { const newState = { rendererScrollState: newScrollState, editorScrollState: old.editorScrollState } - console.debug('[EditorPage] set scroll state because of editor scroll', newState) + log.debug('Set scroll state because of editor scroll', newState) return newState }) } @@ -87,12 +90,12 @@ export const EditorPage: React.FC = () => { const setRendererToScrollSource = useCallback(() => { scrollSource.current = ScrollSource.RENDERER - console.debug('[EditorPage] Make renderer scroll source') + log.debug('Make renderer scroll source') }, []) const setEditorToScrollSource = useCallback(() => { scrollSource.current = ScrollSource.EDITOR - console.debug('[EditorPage] Make editor scroll source') + log.debug('Make editor scroll source') }, []) useNotificationTest() diff --git a/src/components/editor-page/editor-pane/autocompletion/code-block.ts b/src/components/editor-page/editor-pane/autocompletion/code-block.ts index e2692ffbc..17f2cfe14 100644 --- a/src/components/editor-page/editor-pane/autocompletion/code-block.ts +++ b/src/components/editor-page/editor-pane/autocompletion/code-block.ts @@ -7,9 +7,11 @@ import { Editor, Hint, Hints, Pos } from 'codemirror' import { findWordAtCursor, generateHintListByPrefix, Hinter } from './index' import { showErrorNotification } from '../../../../redux/ui-notifications/methods' +import { Logger } from '../../../../utils/logger' type highlightJsImport = typeof import('../../../common/hljs/hljs') +const log = new Logger('Autocompletion > CodeBlock') const wordRegExp = /^```((\w|-|_|\+)*)$/ let allSupportedLanguages: string[] = [] @@ -22,7 +24,7 @@ const loadHighlightJs = async (): Promise => { return await import('../../../common/hljs/hljs') } catch (error) { showErrorNotification('common.errorWhileLoadingLibrary', { name: 'highlight.js' })(error as Error) - console.error("can't load highlight js", error) + log.error('Error while loading highlight.js', error) return null } } diff --git a/src/components/editor-page/editor-pane/autocompletion/emoji.ts b/src/components/editor-page/editor-pane/autocompletion/emoji.ts index 3369dcfa9..110f550b2 100644 --- a/src/components/editor-page/editor-pane/autocompletion/emoji.ts +++ b/src/components/editor-page/editor-pane/autocompletion/emoji.ts @@ -10,9 +10,11 @@ import { Emoji, EmojiClickEventDetail, NativeEmoji } from 'emoji-picker-element/ import { emojiPickerConfig } from '../tool-bar/emoji-picker/emoji-picker' import { getEmojiIcon, getEmojiShortCode } from '../tool-bar/utils/emojiUtils' import { findWordAtCursor, Hinter } from './index' +import { Logger } from '../../../../utils/logger' const emojiIndex = new Database(emojiPickerConfig) const emojiWordRegex = /^:([\w-_+]*)$/ +const log = new Logger('Autocompletion > Emoji') const findEmojiInDatabase = async (emojiIndex: Database, term: string): Promise => { try { @@ -26,7 +28,7 @@ const findEmojiInDatabase = async (emojiIndex: Database, term: string): Promise< return queryResult } } catch (error) { - console.error(error) + log.error('Error while searching for emoji', term, error) return [] } } diff --git a/src/components/editor-page/editor-pane/upload-handler.ts b/src/components/editor-page/editor-pane/upload-handler.ts index b45b2220b..2995d93d7 100644 --- a/src/components/editor-page/editor-pane/upload-handler.ts +++ b/src/components/editor-page/editor-pane/upload-handler.ts @@ -9,6 +9,9 @@ import i18n from 'i18next' import { uploadFile } from '../../../api/media' import { store } from '../../../redux' import { supportedMimeTypes } from '../../common/upload-image-mimetypes' +import { Logger } from '../../../utils/logger' + +const log = new Logger('File Uploader Handler') export const handleUpload = (file: File, editor: Editor): void => { if (!file) { @@ -30,7 +33,7 @@ export const handleUpload = (file: File, editor: Editor): void => { insertCode(`![](${link})`) }) .catch((error) => { - console.error('error while uploading file', error) + log.error('error while uploading file', error) insertCode('') }) } diff --git a/src/components/editor-page/hooks/useLoadNoteFromServer.ts b/src/components/editor-page/hooks/useLoadNoteFromServer.ts index d5a9bffe9..dbe7f7a09 100644 --- a/src/components/editor-page/hooks/useLoadNoteFromServer.ts +++ b/src/components/editor-page/hooks/useLoadNoteFromServer.ts @@ -9,6 +9,9 @@ import { useParams } from 'react-router' import { getNote } from '../../../api/notes' import { setNoteDataFromServer } from '../../../redux/note-details/methods' import { EditorPagePathParams } from '../editor-page' +import { Logger } from '../../../utils/logger' + +const log = new Logger('Load Note From Server') export const useLoadNoteFromServer = (): [boolean, boolean] => { const { id } = useParams() @@ -21,9 +24,9 @@ export const useLoadNoteFromServer = (): [boolean, boolean] => { .then((note) => { setNoteDataFromServer(note) }) - .catch((e) => { + .catch((error) => { setError(true) - console.error(e) + log.error('Error while fetching note from server', error) }) .finally(() => setLoading(false)) }, [id]) diff --git a/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts b/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts index c3b8a64ca..443e1c637 100644 --- a/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts +++ b/src/components/editor-page/renderer-pane/hooks/use-on-iframe-load.ts @@ -6,6 +6,9 @@ import { RefObject, useCallback, useRef } from 'react' import { EditorToRendererCommunicator } from '../../../render-page/window-post-message-communicator/editor-to-renderer-communicator' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('IframeLoader') export const useOnIframeLoad = ( frameReference: RefObject, @@ -29,7 +32,7 @@ export const useOnIframeLoad = ( return } else { onNavigateAway() - console.error('Navigated away from unknown URL') + log.error('Navigated away from unknown URL') frame.src = renderPageUrl sendToRenderPage.current = true } diff --git a/src/components/editor-page/sidebar/upload-input.tsx b/src/components/editor-page/sidebar/upload-input.tsx index 423f57417..ab585b25a 100644 --- a/src/components/editor-page/sidebar/upload-input.tsx +++ b/src/components/editor-page/sidebar/upload-input.tsx @@ -5,6 +5,9 @@ */ import React, { MutableRefObject, useCallback, useEffect, useRef } from 'react' +import { Logger } from '../../../utils/logger' + +const log = new Logger('UploadInput') export interface UploadInputProps { onLoad: (file: File) => Promise @@ -30,7 +33,7 @@ export const UploadInput: React.FC = ({ onLoad, acceptedFiles, fileInput.value = '' }) .catch((error) => { - console.error(error) + log.error('Error while uploading file', error) }) }) fileInput.click() diff --git a/src/components/editor-page/use-notification-test.tsx b/src/components/editor-page/use-notification-test.tsx index fedb463e4..376cef739 100644 --- a/src/components/editor-page/use-notification-test.tsx +++ b/src/components/editor-page/use-notification-test.tsx @@ -6,8 +6,10 @@ import { useEffect } from 'react' import { dispatchUiNotification } from '../../redux/ui-notifications/methods' +import { Logger } from '../../utils/logger' const localStorageKey = 'dontshowtestnotification' +const log = new Logger('Notification Test') /** * Spawns a notification to test the system. Only for tech demo show case. @@ -17,7 +19,7 @@ export const useNotificationTest = (): void => { if (window.localStorage.getItem(localStorageKey)) { return } - console.debug('[Notifications] Dispatched test notification') + log.debug('Dispatched test notification') void dispatchUiNotification('notificationTest.title', 'notificationTest.content', { icon: 'info-circle', buttons: [ diff --git a/src/components/error-boundary/error-boundary.tsx b/src/components/error-boundary/error-boundary.tsx index 9a59bf856..2a96f4c8b 100644 --- a/src/components/error-boundary/error-boundary.tsx +++ b/src/components/error-boundary/error-boundary.tsx @@ -10,6 +10,9 @@ import links from '../../links.json' import frontendVersion from '../../version.json' import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon' import { ExternalLink } from '../common/links/external-link' +import { Logger } from '../../utils/logger' + +const log = new Logger('ErrorBoundary') export class ErrorBoundary extends Component { state: { @@ -27,8 +30,7 @@ export class ErrorBoundary extends Component { } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { - console.error('error caught', error) - console.error('additional information', errorInfo) + log.error('Error catched', error, errorInfo) } refreshPage(): void { diff --git a/src/components/landing-layout/footer/language-picker.tsx b/src/components/landing-layout/footer/language-picker.tsx index e4f58dbe0..bb70d962f 100644 --- a/src/components/landing-layout/footer/language-picker.tsx +++ b/src/components/landing-layout/footer/language-picker.tsx @@ -8,7 +8,9 @@ import { Settings } from 'luxon' import React, { useCallback, useMemo } from 'react' import { Form } from 'react-bootstrap' import { useTranslation } from 'react-i18next' +import { Logger } from '../../../utils/logger' +const log = new Logger('LanguagePicker') const languages = { en: 'English', 'zh-CN': '简体中文', @@ -61,7 +63,7 @@ export const LanguagePicker: React.FC = () => { (event: React.ChangeEvent) => { const language = event.currentTarget.value Settings.defaultLocale = language - i18n.changeLanguage(language).catch((error) => console.error('Error while switching language', error)) + i18n.changeLanguage(language).catch((error) => log.error('Error while switching language', error)) }, [i18n] ) diff --git a/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts b/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts index e8f06a738..966edbe37 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/parser-debugger.ts @@ -5,11 +5,14 @@ */ import MarkdownIt from 'markdown-it/lib' +import { Logger } from '../../../utils/logger' + +const log = new Logger('MarkdownItParserDebugger') export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt) => { if (process.env.NODE_ENV !== 'production') { md.core.ruler.push('test', (state) => { - console.log(state) + log.debug('Current state', state) return false }) } diff --git a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx b/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx index 9af1585cc..b954a7fd9 100644 --- a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx +++ b/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx @@ -6,6 +6,9 @@ import React, { useEffect, useRef } from 'react' import './abc.scss' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('AbcFrame') export interface AbcFrameProps { code: string @@ -23,8 +26,8 @@ export const AbcFrame: React.FC = ({ code }) => { .then((imp) => { imp.renderAbc(actualContainer, code, {}) }) - .catch(() => { - console.error('error while loading abcjs') + .catch((error) => { + log.error('Error while loading abcjs', error) }) }, [code]) diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx index 2fac4d20c..130351303 100644 --- a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx +++ b/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx @@ -8,6 +8,9 @@ import React, { useEffect, useRef, useState } from 'react' import { Alert } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { useIsDarkModeActivated } from '../../../../../hooks/common/use-is-dark-mode-activated' +import { Logger } from '../../../../../utils/logger' + +const log = new Logger('FlowChart') export interface FlowChartProps { code: string @@ -43,7 +46,7 @@ export const FlowChart: React.FC = ({ code }) => { setError(true) } }) - .catch(() => console.error('error while loading flowchart.js')) + .catch((error) => log.error('Error while loading flowchart.js', error)) return () => { Array.from(currentDiagramRef.children).forEach((value) => value.remove()) diff --git a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx b/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx index 05ec361a5..e3118e3eb 100644 --- a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx +++ b/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx @@ -8,6 +8,9 @@ import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react import { Alert } from 'react-bootstrap' import { ShowIf } from '../../../common/show-if/show-if' import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('GraphvizFrame') export interface GraphvizFrameProps { code: string @@ -22,7 +25,7 @@ export const GraphvizFrame: React.FC = ({ code }) => { return } setError(error) - console.error(error) + log.error(error) container.current.querySelectorAll('svg').forEach((child) => child.remove()) }, []) @@ -53,8 +56,8 @@ export const GraphvizFrame: React.FC = ({ code }) => { showError(error as string) } }) - .catch(() => { - console.error('error while loading graphviz') + .catch((error) => { + log.error('Error while loading graphviz', error) }) }, [code, error, frontendBaseUrl, showError]) diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx index a136b15a2..ce0e9c02e 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx @@ -9,6 +9,9 @@ import convertHtmlToReact from '@hedgedoc/html-to-react' import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button' import '../../../utils/button-inside.scss' import './highlighted-code.scss' +import { Logger } from '../../../../../utils/logger' + +const log = new Logger('HighlightedCode') export interface HighlightedCodeProps { code: string @@ -55,8 +58,8 @@ export const HighlightedCode: React.FC = ({ code, language )) setDom(replacedDom) }) - .catch(() => { - console.error('error while loading highlight.js') + .catch((error) => { + log.error('Error while loading highlight.js', error) }) }, [code, language, startLineNumber]) diff --git a/src/components/markdown-renderer/replace-components/image/proxy-image-frame.tsx b/src/components/markdown-renderer/replace-components/image/proxy-image-frame.tsx index e30ad40ef..a2304cbad 100644 --- a/src/components/markdown-renderer/replace-components/image/proxy-image-frame.tsx +++ b/src/components/markdown-renderer/replace-components/image/proxy-image-frame.tsx @@ -7,6 +7,9 @@ import React, { useEffect, useState } from 'react' import { getProxiedUrl } from '../../../../api/media' import { useApplicationState } from '../../../../hooks/common/use-application-state' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('ProxyImageFrame') export const ProxyImageFrame: React.FC> = ({ src, title, alt, ...props }) => { const [imageUrl, setImageUrl] = useState('') @@ -18,7 +21,7 @@ export const ProxyImageFrame: React.FC } getProxiedUrl(src) .then((proxyResponse) => setImageUrl(proxyResponse.src)) - .catch((err) => console.error(err)) + .catch((err) => log.error(err)) }, [imageProxyEnabled, src]) return diff --git a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx b/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx index bb3e0551d..3455bbffa 100644 --- a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx +++ b/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx @@ -8,6 +8,9 @@ import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LockButton } from '../../../common/lock-button/lock-button' import '../../utils/button-inside.scss' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('MarkmapFrame') export interface MarkmapFrameProps { code: string @@ -54,11 +57,11 @@ export const MarkmapFrame: React.FC = ({ code }) => { actualContainer.appendChild(svg) markmapLoader(svg, code) } catch (error) { - console.error(error) + log.error(error) } }) - .catch(() => { - console.error('error while loading markmap') + .catch((error) => { + log.error('Error while loading markmap', error) }) }, [code]) diff --git a/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx b/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx index 534a036a2..f44ad9b5c 100644 --- a/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx +++ b/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx @@ -9,7 +9,9 @@ import { Alert } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { ShowIf } from '../../../common/show-if/show-if' import './mermaid.scss' +import { Logger } from '../../../../utils/logger' +const log = new Logger('MermaidChart') export interface MermaidChartProps { code: string } @@ -32,8 +34,8 @@ export const MermaidChart: React.FC = ({ code }) => { mermaid.default.initialize({ startOnLoad: false }) mermaidInitialized = true }) - .catch(() => { - console.error('error while loading mermaid') + .catch((error) => { + log.error('Error while loading mermaid', error) }) } }, []) @@ -41,7 +43,7 @@ export const MermaidChart: React.FC = ({ code }) => { const showError = useCallback( (error: string) => { setError(error) - console.error(error) + log.error(error) if (!diagramContainer.current) { return } diff --git a/src/components/markdown-renderer/replace-components/one-click-frame/one-click-embedding.tsx b/src/components/markdown-renderer/replace-components/one-click-frame/one-click-embedding.tsx index d6d32f422..2fd31608d 100644 --- a/src/components/markdown-renderer/replace-components/one-click-frame/one-click-embedding.tsx +++ b/src/components/markdown-renderer/replace-components/one-click-frame/one-click-embedding.tsx @@ -10,6 +10,9 @@ import { IconName } from '../../../common/fork-awesome/types' import { ShowIf } from '../../../common/show-if/show-if' import './one-click-embedding.scss' import { ProxyImageFrame } from '../image/proxy-image-frame' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('OneClickEmbedding') interface OneClickFrameProps { onImageFetch?: () => Promise @@ -52,7 +55,7 @@ export const OneClickEmbedding: React.FC = ({ setPreviewImageUrl(imageLink) }) .catch((message) => { - console.error(message) + log.error(message) }) }, [onImageFetch]) diff --git a/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx b/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx index ff600e062..ec72de9fa 100644 --- a/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx +++ b/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx @@ -9,6 +9,9 @@ import { Alert } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { VisualizationSpec } from 'vega-embed' import { ShowIf } from '../../../common/show-if/show-if' +import { Logger } from '../../../../utils/logger' + +const log = new Logger('VegaChart') export interface VegaChartProps { code: string @@ -23,7 +26,7 @@ export const VegaChart: React.FC = ({ code }) => { if (!diagramContainer.current) { return } - console.error(error) + log.error(error) setError(error) }, []) @@ -58,8 +61,8 @@ export const VegaChart: React.FC = ({ code }) => { showError(t('renderer.vega-lite.errorJson')) } }) - .catch(() => { - console.error('error while loading vega-light') + .catch((error) => { + log.error('Error while loading vega-light', error) }) }, [code, showError, t]) diff --git a/src/components/notifications/ui-notification-toast.tsx b/src/components/notifications/ui-notification-toast.tsx index c4467eee8..08208d57c 100644 --- a/src/components/notifications/ui-notification-toast.tsx +++ b/src/components/notifications/ui-notification-toast.tsx @@ -12,8 +12,10 @@ import { ShowIf } from '../common/show-if/show-if' import { IconName } from '../common/fork-awesome/types' import { dismissUiNotification } from '../../redux/ui-notifications/methods' import { Trans, useTranslation } from 'react-i18next' +import { Logger } from '../../utils/logger' const STEPS_PER_SECOND = 10 +const log = new Logger('UiNotificationToast') export interface UiNotificationProps extends UiNotification { notificationId: number @@ -42,7 +44,7 @@ export const UiNotificationToast: React.FC = ({ }, []) const dismissThisNotification = useCallback(() => { - console.debug(`[Notifications] Dismissed notification ${notificationId}`) + log.debug(`Dismissed notification ${notificationId}`) dismissUiNotification(notificationId) }, [notificationId]) @@ -50,7 +52,7 @@ export const UiNotificationToast: React.FC = ({ if (dismissed || !!interval.current) { return } - console.debug(`[Notifications] Show notification ${notificationId}`) + log.debug(`Show notification ${notificationId}`) setEta(durationInSecond * STEPS_PER_SECOND) interval.current = setInterval( () => diff --git a/src/components/profile-page/access-tokens/profile-access-tokens.tsx b/src/components/profile-page/access-tokens/profile-access-tokens.tsx index d9c674622..365203ff7 100644 --- a/src/components/profile-page/access-tokens/profile-access-tokens.tsx +++ b/src/components/profile-page/access-tokens/profile-access-tokens.tsx @@ -15,6 +15,9 @@ import { IconButton } from '../../common/icon-button/icon-button' import { CommonModal } from '../../common/modals/common-modal' import { DeletionModal } from '../../common/modals/deletion-modal' import { ShowIf } from '../../common/show-if/show-if' +import { Logger } from '../../../utils/logger' + +const log = new Logger('ProfileAccessTokens') export const ProfileAccessTokens: React.FC = () => { const { t } = useTranslation() @@ -37,7 +40,7 @@ export const ProfileAccessTokens: React.FC = () => { setNewTokenLabel('') }) .catch((error) => { - console.error(error) + log.error(error) setError(true) }) }, @@ -50,7 +53,7 @@ export const ProfileAccessTokens: React.FC = () => { setSelectedForDeletion(0) }) .catch((error) => { - console.error(error) + log.error(error) setError(true) }) .finally(() => { @@ -74,7 +77,7 @@ export const ProfileAccessTokens: React.FC = () => { setAccessTokens(tokens) }) .catch((err) => { - console.error(err) + log.error(err) setError(true) }) }, [showAddedModal]) diff --git a/src/components/register-page/register-page.tsx b/src/components/register-page/register-page.tsx index 19ea24741..114e8a22f 100644 --- a/src/components/register-page/register-page.tsx +++ b/src/components/register-page/register-page.tsx @@ -13,6 +13,9 @@ import { useApplicationState } from '../../hooks/common/use-application-state' import { TranslatedExternalLink } from '../common/links/translated-external-link' import { ShowIf } from '../common/show-if/show-if' import { fetchAndSetUser } from '../login-page/auth/utils' +import { Logger } from '../../utils/logger' + +const log = new Logger('RegisterPage') export enum RegisterError { NONE = 'none', @@ -37,7 +40,7 @@ export const RegisterPage: React.FC = () => { doInternalRegister(username, password) .then(() => fetchAndSetUser()) .catch((err: Error) => { - console.error(err) + log.error(err) setError(err.message === RegisterError.USERNAME_EXISTING ? err.message : RegisterError.OTHER) }) event.preventDefault() diff --git a/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts b/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts index 3910f23a6..f36a8d3de 100644 --- a/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts +++ b/src/components/render-page/window-post-message-communicator/editor-to-renderer-communicator.ts @@ -6,6 +6,7 @@ import { WindowPostMessageCommunicator } from './window-post-message-communicator' import { CommunicationMessages, EditorToRendererMessageType, RendererToEditorMessageType } from './rendering-message' +import { Logger } from '../../../utils/logger' /** * The communicator that is used to send messages from the editor to the renderer. @@ -15,7 +16,7 @@ export class EditorToRendererCommunicator extends WindowPostMessageCommunicator< EditorToRendererMessageType, CommunicationMessages > { - protected generateLogIdentifier(): string { - return 'E=>R' + protected createLogger(): Logger { + return new Logger('EditorToRendererCommunicator') } } diff --git a/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts b/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts index 287fcb31e..cfd0e1212 100644 --- a/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts +++ b/src/components/render-page/window-post-message-communicator/renderer-to-editor-communicator.ts @@ -6,6 +6,7 @@ import { WindowPostMessageCommunicator } from './window-post-message-communicator' import { CommunicationMessages, EditorToRendererMessageType, RendererToEditorMessageType } from './rendering-message' +import { Logger } from '../../../utils/logger' /** * The communicator that is used to send messages from the renderer to the editor. @@ -15,7 +16,7 @@ export class RendererToEditorCommunicator extends WindowPostMessageCommunicator< RendererToEditorMessageType, CommunicationMessages > { - protected generateLogIdentifier(): string { - return 'E<=R' + protected createLogger(): Logger { + return new Logger('RendererToEditorCommunicator') } } diff --git a/src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts b/src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts index 98a774267..1f84e2ac1 100644 --- a/src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts +++ b/src/components/render-page/window-post-message-communicator/window-post-message-communicator.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { Logger } from '../../../utils/logger' + /** * Error that will be thrown if a message couldn't be sent. */ @@ -33,12 +35,16 @@ export abstract class WindowPostMessageCommunicator< private targetOrigin?: string private communicationEnabled: boolean private handlers: HandlerMap = {} + private log constructor() { window.addEventListener('message', this.handleEvent.bind(this)) this.communicationEnabled = false + this.log = this.createLogger() } + protected abstract createLogger(): Logger + /** * Removes the message event listener from the {@link window} */ @@ -91,7 +97,7 @@ export abstract class WindowPostMessageCommunicator< `Communication isn't enabled. Maybe the other side is not ready?\nMessage was: ${JSON.stringify(message)}` ) } - console.debug('[WPMC ' + this.generateLogIdentifier() + '] Sent event', message) + this.log.debug('Sent event', message) this.messageTarget.postMessage(message, this.targetOrigin) } @@ -106,12 +112,6 @@ export abstract class WindowPostMessageCommunicator< this.handlers[messageType] = handler as Handler } - /** - * Generates a unique identifier that helps to separate log messages in the console from different communicators. - * @return the identifier - */ - protected abstract generateLogIdentifier(): string - /** * Receives the message events and calls the handler that is mapped to the correct type. * @@ -125,7 +125,7 @@ export abstract class WindowPostMessageCommunicator< if (!handler) { return true } - console.debug('[WPMC ' + this.generateLogIdentifier() + '] Received event ', data) + this.log.debug('Received event', data) handler(data as Extract>) return false } diff --git a/src/index.tsx b/src/index.tsx index 8eee119f0..a466636ab 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,7 @@ import * as serviceWorkerRegistration from './service-worker-registration' import './style/dark.scss' import './style/index.scss' import { isTestMode } from './utils/test-modes' +import { Logger } from './utils/logger' const EditorPage = React.lazy( () => import(/* webpackPrefetch: true */ /* webpackChunkName: "editor" */ './components/editor-page/editor-page') @@ -37,6 +38,9 @@ const DocumentReadOnlyPage = React.lazy( ) ) const baseUrl = new URL(document.head.baseURI).pathname +const log = new Logger('Index') + +log.info('Starting HedgeDoc!') ReactDOM.render( @@ -96,7 +100,7 @@ ReactDOM.render( ) if (isTestMode()) { - console.log('This build runs in test mode. This means:\n - no sandboxed iframe') + log.warn('This build runs in test mode. This means:\n - no sandboxed iframe') } // If you want your app to work offline and load faster, you can change diff --git a/src/redux/dark-mode/methods.ts b/src/redux/dark-mode/methods.ts index a9117f331..74363c8c5 100644 --- a/src/redux/dark-mode/methods.ts +++ b/src/redux/dark-mode/methods.ts @@ -6,6 +6,9 @@ import { store } from '..' import { DarkModeConfig, DarkModeConfigActionType, SetDarkModeConfigAction } from './types' +import { Logger } from '../../utils/logger' + +const log = new Logger('Redux > DarkMode') export const setDarkMode = (darkMode: boolean): void => { store.dispatch({ @@ -17,8 +20,8 @@ export const setDarkMode = (darkMode: boolean): void => { export const saveToLocalStorage = (darkModeConfig: DarkModeConfig): void => { try { window.localStorage.setItem('nightMode', String(darkModeConfig.darkMode)) - } catch (e) { - console.error('Saving dark-mode setting to local storage failed: ', e) + } catch (error) { + log.error('Saving to local storage failed', error) } } @@ -31,8 +34,8 @@ export const loadFromLocalStorage = (): DarkModeConfig | undefined => { return { darkMode: storedValue === 'true' } - } catch (e) { - console.error('Loading dark-mode setting from local storage failed: ', e) + } catch (error) { + log.error('Loading from local storage failed', error) return undefined } } @@ -43,8 +46,8 @@ export const determineDarkModeBrowserSetting = (): DarkModeConfig | undefined => return { darkMode: mediaQueryResult } - } catch (e) { - console.error('Can not determine dark-mode setting from browser: ', e) + } catch (error) { + log.error('Can not determine setting from browser', error) return undefined } } diff --git a/src/redux/editor/methods.ts b/src/redux/editor/methods.ts index 7b674e4cd..e67a4aab1 100644 --- a/src/redux/editor/methods.ts +++ b/src/redux/editor/methods.ts @@ -16,6 +16,9 @@ import { SetEditorSyncScrollAction, SetEditorViewModeAction } from './types' +import { Logger } from '../../utils/logger' + +const log = new Logger('Redux > Editor') export const loadFromLocalStorage = (): EditorConfig | undefined => { try { @@ -33,8 +36,8 @@ export const saveToLocalStorage = (editorConfig: EditorConfig): void => { try { const json = JSON.stringify(editorConfig) localStorage.setItem('editorConfig', json) - } catch (e) { - console.error('Can not persist editor config in local storage: ', e) + } catch (error) { + log.error('Error while saving editor config in local storage', error) } } diff --git a/src/redux/history/methods.ts b/src/redux/history/methods.ts index f9d6d2f66..ccda6b7b8 100644 --- a/src/redux/history/methods.ts +++ b/src/redux/history/methods.ts @@ -29,6 +29,9 @@ import { historyEntryToHistoryEntryPutDto, historyEntryToHistoryEntryUpdateDto } from '../../api/history/dto-methods' +import { Logger } from '../../utils/logger' + +const log = new Logger('Redux > History') export const setHistoryEntries = (entries: HistoryEntry[]): void => { store.dispatch({ @@ -163,7 +166,7 @@ const loadLocalHistory = (): HistoryEntry[] => { window.localStorage.removeItem('notehistory') return convertV1History(localV1History) } catch (error) { - console.error(`Error converting old history entries: ${String(error)}`) + log.error('Error while converting old history entries', error) return [] } } @@ -180,7 +183,7 @@ const loadLocalHistory = (): HistoryEntry[] => { }) return localHistory } catch (error) { - console.error(`Error parsing local stored history entries: ${String(error)}`) + log.error('Error while parsing locally stored history entries', error) return [] } } @@ -190,7 +193,7 @@ const loadRemoteHistory = async (): Promise => { const remoteHistory = await getHistory() return remoteHistory.map(historyEntryDtoToHistoryEntry) } catch (error) { - console.error(`Error fetching history entries from server: ${String(error)}`) + log.error('Error while fetching history entries from server', error) return [] } } diff --git a/src/redux/ui-notifications/methods.ts b/src/redux/ui-notifications/methods.ts index b543b8057..fb50f231f 100644 --- a/src/redux/ui-notifications/methods.ts +++ b/src/redux/ui-notifications/methods.ts @@ -8,6 +8,9 @@ import i18n, { TOptions } from 'i18next' import { store } from '../index' import { DismissUiNotificationAction, DispatchOptions, UiNotificationActionType } from './types' import { DateTime } from 'luxon' +import { Logger } from '../../utils/logger' + +const log = new Logger('Redux > Notifications') export const DEFAULT_DURATION_IN_SECONDS = 10 @@ -70,7 +73,7 @@ export const dismissUiNotification = (notificationId: number): void => { export const showErrorNotification = (messageI18nKey: string, messageI18nOptions?: TOptions | string) => (error: Error): void => { - console.error(i18n.t(messageI18nKey, messageI18nOptions), error) + log.error(i18n.t(messageI18nKey, messageI18nOptions), error) void dispatchUiNotification('common.errorOccurred', messageI18nKey, { contentI18nOptions: messageI18nOptions, icon: 'exclamation-triangle' diff --git a/src/service-worker-registration.ts b/src/service-worker-registration.ts index 2ff69063d..a42104c99 100644 --- a/src/service-worker-registration.ts +++ b/src/service-worker-registration.ts @@ -15,6 +15,9 @@ // To learn more about the benefits of this model and instructions on how to // opt-in, read https://cra.link/PWA +import { Logger } from './utils/logger' + +const log = new Logger('ServiceWorker > Registration') const localhostRegex = /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ @@ -53,12 +56,12 @@ export function register(config?: Config): void { // service worker/PWA documentation. navigator.serviceWorker.ready .then(() => { - console.log( + log.info( 'This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://cra.link/PWA' ) }) - .catch((error: Error) => console.error(error)) + .catch((error: Error) => log.error(error)) } else { // Is not localhost. Just register service worker registerValidSW(swUrl, config) @@ -82,7 +85,7 @@ function registerValidSW(swUrl: string, config?: Config) { // At this point, the updated precached content has been fetched, // but the previous service worker will still serve the older // content until all client tabs are closed. - console.log( + log.info( 'New content is available and will be used when all ' + 'tabs for this page are closed. See https://cra.link/PWA.' ) @@ -95,7 +98,7 @@ function registerValidSW(swUrl: string, config?: Config) { // At this point, everything has been precached. // It's the perfect time to display a // "Content is cached for offline use." message. - console.log('Content is cached for offline use.') + log.info('Content is cached for offline use.') // Execute callback if (config && config.onSuccess) { @@ -107,7 +110,7 @@ function registerValidSW(swUrl: string, config?: Config) { } }) .catch((error) => { - console.error('Error during service worker registration:', error) + log.error('Error during service worker registration', error) }) } @@ -128,16 +131,16 @@ function checkValidServiceWorker(swUrl: string, config?: Config) { .then(() => { window.location.reload() }) - .catch((error: Error) => console.error(error)) + .catch((error: Error) => log.error(error)) }) - .catch((error: Error) => console.error(error)) + .catch((error: Error) => log.error(error)) } else { // Service worker found. Proceed as normal. registerValidSW(swUrl, config) } }) .catch(() => { - console.log('No internet connection found. App is running in offline mode.') + log.info('No internet connection found. App is running in offline mode.') }) } @@ -145,10 +148,10 @@ export function unregister(): void { if ('serviceWorker' in navigator) { navigator.serviceWorker.ready .then((registration) => { - registration.unregister().catch((error: Error) => console.error(error)) + registration.unregister().catch((error: Error) => log.error(error)) }) .catch((error: Error) => { - console.error(error.message) + log.error(error.message) }) } } diff --git a/src/service-worker.ts b/src/service-worker.ts index a83d6740f..728fd0102 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -19,6 +19,9 @@ import { ExpirationPlugin } from 'workbox-expiration' import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' import { registerRoute } from 'workbox-routing' import { StaleWhileRevalidate } from 'workbox-strategies' +import { Logger } from './utils/logger' + +const log = new Logger('ServiceWorker') declare const self: ServiceWorkerGlobalScope @@ -80,7 +83,7 @@ registerRoute( self.addEventListener('message', (event) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting().catch((e) => console.error(e)) + self.skipWaiting().catch((e) => log.error(e)) } }) diff --git a/src/utils/logger.test.ts b/src/utils/logger.test.ts new file mode 100644 index 000000000..cbf3e3a58 --- /dev/null +++ b/src/utils/logger.test.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Logger, LogLevel } from './logger' +import { Settings } from 'luxon' + +describe('Logger', () => { + let consoleMock: jest.SpyInstance + let originalNow: () => number + let dateShift = 0 + + function mockConsole(methodToMock: LogLevel, onResult: (result: string) => void) { + consoleMock = jest.spyOn(console, methodToMock).mockImplementation((...data: string[]) => { + const result = data.reduce((state, current) => state + ' ' + current) + onResult(result) + }) + } + + beforeEach(() => { + originalNow = Settings.now + Settings.now = () => new Date(2021, 9, 25, dateShift, 1 + dateShift, 2 + dateShift, 3 + dateShift).valueOf() + }) + + afterEach(() => { + Settings.now = originalNow + consoleMock.mockReset() + }) + + it('logs a debug message into the console', (done) => { + dateShift = 0 + mockConsole(LogLevel.DEBUG, (result) => { + expect(consoleMock).toBeCalled() + expect(result).toEqual('%c[2021-10-25 00:01:02] %c(prefix) color: yellow color: orange beans') + done() + }) + new Logger('prefix').debug('beans') + }) + + it('logs a info message into the console', (done) => { + dateShift = 1 + mockConsole(LogLevel.INFO, (result) => { + expect(consoleMock).toBeCalled() + expect(result).toEqual('%c[2021-10-25 01:02:03] %c(prefix) color: yellow color: orange toast') + done() + }) + new Logger('prefix').info('toast') + }) + + it('logs a warn message into the console', (done) => { + dateShift = 2 + mockConsole(LogLevel.WARN, (result) => { + expect(consoleMock).toBeCalled() + expect(result).toEqual('%c[2021-10-25 02:03:04] %c(prefix) color: yellow color: orange eggs') + done() + }) + new Logger('prefix').warn('eggs') + }) + + it('logs a error message into the console', (done) => { + dateShift = 3 + mockConsole(LogLevel.ERROR, (result) => { + expect(consoleMock).toBeCalled() + expect(result).toEqual('%c[2021-10-25 03:04:05] %c(prefix) color: yellow color: orange bacon') + done() + }) + new Logger('prefix').error('bacon') + }) +}) diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 000000000..c6a67d818 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { DateTime } from 'luxon' + +export enum LogLevel { + DEBUG = 'debug', + INFO = 'info', + WARN = 'warn', + ERROR = 'error' +} + +type OutputFunction = (...data: unknown[]) => void + +/** + * Simple logger that prefixes messages with a timestamp and a name + */ +export class Logger { + private readonly scope: string + + constructor(scope: string) { + this.scope = scope + } + + /** + * Logs a debug message + * @param data data to log + */ + debug(...data: unknown[]): void { + this.log(LogLevel.DEBUG, ...data) + } + + /** + * Logs a normal informative message + * @param data data to log + */ + info(...data: unknown[]): void { + this.log(LogLevel.INFO, ...data) + } + + /** + * Logs a warning + * @param data data to log + */ + warn(...data: unknown[]): void { + this.log(LogLevel.WARN, ...data) + } + + /** + * Logs an error + * @param data data to log + */ + error(...data: unknown[]): void { + this.log(LogLevel.ERROR, ...data) + } + + private log(loglevel: LogLevel, ...data: unknown[]) { + const preparedData = [...this.prefix(), ...data] + const logOutput = Logger.getLogOutput(loglevel) + logOutput(...preparedData) + } + + private static getLogOutput(logLevel: LogLevel): OutputFunction { + switch (logLevel) { + case LogLevel.INFO: + return console.info + case LogLevel.DEBUG: + return console.debug + case LogLevel.ERROR: + return console.error + case LogLevel.WARN: + return console.warn + } + } + + private prefix(): string[] { + const timestamp = DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss') + return [`%c[${timestamp}] %c(${this.scope})`, 'color: yellow', 'color: orange'] + } +}