diff --git a/public/locales/en/app.json b/public/locales/en/app.json index c40e0a833..6548f4633 100644 --- a/public/locales/en/app.json +++ b/public/locales/en/app.json @@ -33,6 +33,9 @@ "downloadCar": "Export CAR", "done": "Done" }, + "changeLogLevelForm": { + "placeholder": "Enter log level (debug) or enter with subsystems (all=debug, autotls=error, gc=warn)" + }, "cliModal": { "description": "Paste the following into your terminal to do this task in Kubo via the command line. Remember that you'll need to replace placeholders with your specific parameters." }, @@ -74,6 +77,7 @@ "latency": "Latency", "loading": "Loading", "location": "Location", + "logLevel": "Kubo RPC Log Level", "name": "Name", "node": "Node", "out": "Out", diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index b76ed9a7c..db6f62b22 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -23,6 +23,7 @@ "translationProjectLink": "Join the IPFS Translation Project" }, "apiDescription": "<0>If your node is configured with a <1>custom Kubo RPC API address, including a port other than the default 5001, enter it here.", + "changeLogLevelDescription": "<0>Change the <1>Logging Level.", "publicSubdomainGatewayDescription": "<0>Select a default <1>Subdomain Gateway for generating shareable links.", "publicPathGatewayDescription": "<0>Select a fallback <1>Path Gateway for generating shareable links for CIDs that exceed the 63-character DNS limit.", "cliDescription": "<0>Enable this option to display a \"view code\" <1> icon next to common IPFS commands. Clicking it opens a modal with that command's CLI code, so you can paste it into the IPFS command-line interface in your terminal.", diff --git a/src/App.js b/src/App.js index af0f0d854..3aa32d7e0 100644 --- a/src/App.js +++ b/src/App.js @@ -41,6 +41,12 @@ export class App extends Component { this.props.doTryInitIpfs() } + componentDidUpdate (prevProps) { + if (!prevProps.ipfsReady && this.props.ipfsReady) { + this.props.doInitLogLevel() + } + } + addFiles = async (filesPromise) => { const { doFilesWrite, doUpdateHash, routeInfo, filesPathInfo } = this.props const isFilesPage = routeInfo.pattern === '/files*' @@ -139,5 +145,6 @@ export default connect( 'doFilesWrite', 'doDisableTooltip', 'selectFilesPathInfo', + 'doInitLogLevel', withTranslation('app')(AppWithDropTarget) ) diff --git a/src/bundles/index.js b/src/bundles/index.js index 1021b308b..e37dbc44f 100644 --- a/src/bundles/index.js +++ b/src/bundles/index.js @@ -24,6 +24,7 @@ import experimentsBundle from './experiments.js' import cliTutorModeBundle from './cli-tutor-mode.js' import gatewayBundle from './gateway.js' import ipnsBundle from './ipns.js' +import logLevelBundle from './log-level.js' export default composeBundles( createCacheBundle({ @@ -52,5 +53,6 @@ export default composeBundles( repoStats, cliTutorModeBundle, createAnalyticsBundle({}), - ipnsBundle + ipnsBundle, + logLevelBundle ) diff --git a/src/bundles/log-level.js b/src/bundles/log-level.js new file mode 100644 index 000000000..3ddf12d29 --- /dev/null +++ b/src/bundles/log-level.js @@ -0,0 +1,192 @@ +import { readSetting, writeSetting } from './local-storage.js' + +/** + * @typedef {import('ipfs').IPFSService} IPFSService + * @typedef {Set | null} KuboSubsystems Valid subsystems to set log levels for. + * @typedef {'debug' | 'info' | 'warn' | 'error' | 'dpanic' | 'panic' | 'fatal'} LogLevel + * @typedef {'all' | '*'} AllSubsystemAliases + * + * @typedef {Object} Model + * @property {boolean} initialized + * @property {string} logLevel + * @property {boolean} error + * + * @typedef {Object} ParsedLogLevel + * @property {LogLevel} level + * @property {string} subsystem + * + * @typedef {Object} State + * @property {Model} logLevel + */ + +/** + * @type {AllSubsystemAliases} + */ +const ALL_SUBSYSTEMS_ALIASES = ['all', '*'] +/** + * @type {ReadonlyArray} + */ +const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'dpanic', 'panic', 'fatal'] +/** + * @type {LogLevel} + */ +const DEFAULT_LOG_LEVEL = 'error' + +/** + * @returns {Model} + */ +const init = () => ({ + initialized: false, + logLevel: readSetting('ipfsLogLevel') || DEFAULT_LOG_LEVEL, + error: false +}) + +/** + * @type {KuboSubsystems} + */ +let kuboSubsystems = null + +/** + * Validates a log level input string. + * - Verifies that both subsystem and level are valid. + * + * @param {string} logLevelInput + * @returns {boolean} + * @example + * checkValidLogLevel('debug') + * checkValidLogLevel('*=debug, gc=info') + * checkValidLogLevel('all=debug, gc=info, autotls=warn') + */ +export const checkValidLogLevel = (logLevelInput) => { + try { + const inputs = parseLogLevelToArray(logLevelInput) + if (inputs.length === 0) return false + for (const { level, subsystem } of inputs) { + if (!LOG_LEVELS.includes(level)) return false + if (!kuboSubsystems.has(subsystem) && !ALL_SUBSYSTEMS_ALIASES.includes(subsystem)) return false + } + return true + } catch (err) { + return false + } +} + +/** + * Parses the given log level input string into an array of objects. + * - lowercases the inputs + * - removes any whitespace + * @param {string} logLevel + * @returns {Array} + */ +const parseLogLevelToArray = (logLevel) => { + const inputs = logLevel.split(' ').join('').toLowerCase().split(',') + const result = [] + for (const input of inputs) { + let level, subsystem + if (input.includes('=')) { + const [a, b] = input.trim().split('=') + subsystem = a + level = b + } else { + subsystem = 'all' + level = input + } + result.push({ subsystem, level }) + } + return result +} + +/** + * Loops through each log level setting and sends request to Kubo. + * @param {string} logLevel + * @param {IPFSService} ipfs + * @returns {Promise} + */ +const setLogLevels = async (logLevel, ipfs) => { + const inputs = parseLogLevelToArray(logLevel) + for (const { subsystem, level } of inputs) { + const res = await ipfs.log.level(subsystem, level) + console.info(res?.message) + } +} + +const bundle = { + name: 'logLevel', + + reducer: (state = init(), { type, payload }) => { + switch (type) { + case 'SET_INITIALIZED': + return { ...state, initialized: payload } + case 'SET_LOG_LEVEL': + return { ...state, logLevel: payload } + case 'SET_ERROR': + return { ...state, error: payload } + default: + return state + } + }, + + /** + * Initializes the log level setting. + * - Reads logs level from local storage or uses default. + * - Reads subsystems from Kubo. + * @returns {Promise} + */ + doInitLogLevel: () => async ({ store, getIpfs, dispatch }) => { + try { + // set log levels + const ipfs = getIpfs() + await setLogLevels(store.selectIpfsLogLevel(), ipfs) + + // load subsystems + const subsystems = await ipfs.log.ls() + kuboSubsystems = new Set(subsystems) + dispatch({ type: 'SET_INITIALIZED', payload: true }) + } catch (err) { + console.error('log-level init failed: ', err) + dispatch({ type: 'SET_ERROR', payload: true }) + } + }, + + /** + * @param {string} logLevelInput + * @returns {function(Context):Promise} + */ + doUpdateLogLevel: (logLevelInput) => async ({ dispatch, getIpfs }) => { + try { + if (!checkValidLogLevel(logLevelInput)) { + throw new Error('Invalid log level input') + } + await setLogLevels(logLevelInput, getIpfs()) + await writeSetting('ipfsLogLevel', logLevelInput) + dispatch({ type: 'SET_LOG_LEVEL', payload: logLevelInput }) + dispatch({ type: 'SET_ERROR', payload: false }) + } catch (err) { + console.error('Error setting kubo log level: ', err) + dispatch({ type: 'SET_ERROR', payload: true }) + } + }, + + /** + * @param {State} state + */ + selectIpfsLogLevelInitialized: (state) => { + return state.logLevel.initialized + }, + + /** + * @param {State} state + */ + selectIpfsLogLevel: (state) => { + return state.logLevel.logLevel + }, + + /** + * @param {State} state + */ + selectIpfsLogLevelError: (state) => { + return state.logLevel.error + } +} + +export default bundle diff --git a/src/components/change-log-level-form/ChangeLogLevelForm.js b/src/components/change-log-level-form/ChangeLogLevelForm.js new file mode 100644 index 000000000..2638cfb3f --- /dev/null +++ b/src/components/change-log-level-form/ChangeLogLevelForm.js @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react' +import { connect } from 'redux-bundler-react' +import { withTranslation } from 'react-i18next' +import Button from '../button/button.tsx' +import { checkValidLogLevel } from '../../bundles/log-level.js' + +const ChangeLogLevelForm = ({ t, ipfsInitFailed, ipfsLogLevel, ipfsLogLevelError, doUpdateLogLevel }) => { + const [value, setValue] = useState(ipfsLogLevel) + const [isValidLogLevel, setIsValidLogLevel] = useState(true) + const [showFailState, setShowFailState] = useState(false) + + useEffect(() => { + setShowFailState(ipfsLogLevelError || ipfsInitFailed) + }, [ipfsInitFailed, ipfsLogLevelError]) + + const onChange = (event) => { + setValue(event.target.value) + const isValid = checkValidLogLevel(event.target.value) + setIsValidLogLevel(isValid) + setShowFailState(!isValid) + } + + const onSubmit = async (event) => { + event.preventDefault() + doUpdateLogLevel(value) + } + + const onKeyDown = (e) => { + if (e.key === 'Enter') { + onSubmit(e) + } + } + + return ( +
+ +
+ +
+
+ ) +} + +export default connect( + 'selectIpfsInitFailed', + 'selectIpfsLogLevel', + 'selectIpfsLogLevelInitialized', + 'selectIpfsLogLevelError', + 'doUpdateLogLevel', + withTranslation('app')(ChangeLogLevelForm) +) diff --git a/src/settings/SettingsPage.js b/src/settings/SettingsPage.js index 196a6db02..d156bcfdb 100644 --- a/src/settings/SettingsPage.js +++ b/src/settings/SettingsPage.js @@ -26,6 +26,7 @@ import Checkbox from '../components/checkbox/Checkbox.js' import ComponentLoader from '../loader/ComponentLoader.js' import StrokeCode from '../icons/StrokeCode.js' import { cliCmdKeys, cliCommandList } from '../bundles/files/consts.js' +import ChangeLogLevelForm from '../components/change-log-level-form/ChangeLogLevelForm.js' const PAUSE_AFTER_SAVE_MS = 3000 @@ -64,6 +65,18 @@ export const SettingsPage = ({ } + { isIpfsDesktop + ? null + : +
+ {t('app:terms.logLevel')} + +

Change the Logging Level.

+
+ +
+
} +
{t('app:terms.publicGateway')}