Skip to content

Add setting to adjust kubo log level #2376

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down Expand Up @@ -74,6 +77,7 @@
"latency": "Latency",
"loading": "Loading",
"location": "Location",
"logLevel": "Kubo RPC Log Level",
"name": "Name",
"node": "Node",
"out": "Out",
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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</1>, including a port other than the default 5001, enter it here.</0>",
"changeLogLevelDescription": "<0>Change the <1>Logging Level</1>.</0>",
"publicSubdomainGatewayDescription": "<0>Select a default <1>Subdomain Gateway</1> for generating shareable links.</0>",
"publicPathGatewayDescription": "<0>Select a fallback <1>Path Gateway</1> for generating shareable links for CIDs that exceed the 63-character DNS limit.</0>",
"cliDescription": "<0>Enable this option to display a \"view code\" <1></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.</0>",
Expand Down
7 changes: 7 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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*'
Expand Down Expand Up @@ -139,5 +145,6 @@ export default connect(
'doFilesWrite',
'doDisableTooltip',
'selectFilesPathInfo',
'doInitLogLevel',
withTranslation('app')(AppWithDropTarget)
)
4 changes: 3 additions & 1 deletion src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -52,5 +53,6 @@ export default composeBundles(
repoStats,
cliTutorModeBundle,
createAnalyticsBundle({}),
ipnsBundle
ipnsBundle,
logLevelBundle
)
192 changes: 192 additions & 0 deletions src/bundles/log-level.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { readSetting, writeSetting } from './local-storage.js'

/**
* @typedef {import('ipfs').IPFSService} IPFSService
* @typedef {Set<string> | 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<LogLevel>}
*/
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<ParsedLogLevel>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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
66 changes: 66 additions & 0 deletions src/components/change-log-level-form/ChangeLogLevelForm.js
Original file line number Diff line number Diff line change
@@ -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 (
<form onSubmit={onSubmit}>
<input
id='api-address'
aria-label={t('terms.apiAddress')}
placeholder={t('changeLogLevelForm.placeholder')}
type='text'
className={`w-100 lh-copy monospace f5 pl1 pv1 mb2 charcoal input-reset ba b--black-20 br1 ${showFailState ? 'focus-outline-red b--red-muted' : 'focus-outline-green b--green-muted'}`}
onChange={onChange}
onKeyDown={onKeyDown}
value={value}
/>
<div className='tr'>
<Button
minWidth={100}
height={40}
className='mt2 mt0-l ml2-l tc'
disabled={!isValidLogLevel || value === ipfsLogLevel}>
{t('actions.submit')}
</Button>
</div>
</form>
)
}

export default connect(
'selectIpfsInitFailed',
'selectIpfsLogLevel',
'selectIpfsLogLevelInitialized',
'selectIpfsLogLevelError',
'doUpdateLogLevel',
withTranslation('app')(ChangeLogLevelForm)
)
13 changes: 13 additions & 0 deletions src/settings/SettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -64,6 +65,18 @@ export const SettingsPage = ({
</div>
</Box> }

{ isIpfsDesktop
? null
: <Box className='mb3 pa4-l pa2 joyride-settings-customapi'>
<div className='lh-copy charcoal'>
<Title>{t('app:terms.logLevel')}</Title>
<Trans i18nKey='changeLogLevelDescription' t={t}>
<p>Change the <a className='link blue' href='https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-log-level' target='_blank' rel='noopener noreferrer'>Logging Level</a>.</p>
</Trans>
<ChangeLogLevelForm />
</div>
</Box> }

<Box className='mb3 pa4-l pa2'>
<div className='lh-copy charcoal'>
<Title>{t('app:terms.publicGateway')}</Title>
Expand Down