diff --git a/src/browser/modules/Stream/SysInfoFrame/SysInfoFrame.tsx b/src/browser/modules/Stream/SysInfoFrame/SysInfoFrame.tsx index dfe9ca9dc97..bd837b3b3a2 100644 --- a/src/browser/modules/Stream/SysInfoFrame/SysInfoFrame.tsx +++ b/src/browser/modules/Stream/SysInfoFrame/SysInfoFrame.tsx @@ -28,7 +28,6 @@ import { getUseDb } from 'shared/modules/connections/connectionsDuck' import FrameTemplate from 'browser/modules/Frame/FrameTemplate' -import FrameError from 'browser/modules/Frame/FrameError' import { StyledStatusBar, AutoRefreshToggle, @@ -45,6 +44,8 @@ import { SysInfoTable } from './SysInfoTable' import { Bus } from 'suber' import { GlobalState } from 'shared/globalState' import { Frame } from 'shared/modules/stream/streamDuck' +import { ExclamationTriangleIcon } from '../../../components/icons/Icons' +import { InlineError } from './styled' export type DatabaseMetric = { label: string; value?: string } export type SysInfoFrameState = { @@ -53,11 +54,12 @@ export type SysInfoFrameState = { idAllocation: DatabaseMetric[] pageCache: DatabaseMetric[] transactions: DatabaseMetric[] - error: string + errorMessage: string | null results: boolean - success: boolean autoRefresh: boolean autoRefreshInterval: number + namespacesEnabled: boolean + userConfiguredPrefix: string } type SysInfoFrameProps = { @@ -81,56 +83,107 @@ export class SysInfoFrame extends Component< idAllocation: [], pageCache: [], transactions: [], - error: '', + errorMessage: null, results: false, - success: false, autoRefresh: false, - autoRefreshInterval: 20 // seconds + autoRefreshInterval: 20, // seconds + namespacesEnabled: false, + userConfiguredPrefix: 'neo4j' } - helpers = this.props.hasMultiDbSupport ? helpers : legacyHelpers componentDidMount(): void { - this.getSysInfo() + this.getSettings() + .then(this.getSysInfo) + .catch(errorMessage => this.setState({ errorMessage })) } + getSettings = (): Promise => + new Promise((resolve, reject) => { + const { bus, isConnected } = this.props + + if (bus && isConnected) { + bus.self( + CYPHER_REQUEST, + { + query: 'CALL dbms.listConfig("metrics.")', + queryType: NEO4J_BROWSER_USER_ACTION_QUERY + }, + ({ success, result }) => { + if (success) { + const newState = result.records.reduce( + (newState: Partial, record: any) => { + const name = record.get('name') + const value = record.get('value') + if (name === 'metrics.prefix') { + return { ...newState, userConfiguredPrefix: value } + } + + if (name === 'metrics.namespaces.enabled') { + return { ...newState, namespacesEnabled: value === 'true' } + } + + return newState + }, + {} + ) + + this.setState(newState) + resolve() + } else { + reject('Failed to run listConfig') + } + } + ) + } else { + reject('Could not reach server') + } + }) + componentDidUpdate( - _prevProps: SysInfoFrameProps, + prevProps: SysInfoFrameProps, prevState: SysInfoFrameState ): void { if (prevState.autoRefresh !== this.state.autoRefresh) { if (this.state.autoRefresh) { this.timer = setInterval( - this.getSysInfo.bind(this), + this.getSysInfo, this.state.autoRefreshInterval * 1000 ) } else { this.timer && clearInterval(this.timer) } } + + if (prevProps.useDb !== this.props.useDb) { + this.getSysInfo() + } } - getSysInfo(): void { + getSysInfo = (): void => { + const { userConfiguredPrefix, namespacesEnabled } = this.state const { bus, isConnected, useDb } = this.props - const { sysinfoQuery, responseHandler } = this.helpers + const { sysinfoQuery, responseHandler } = this.props.hasMultiDbSupport + ? helpers + : legacyHelpers - if (bus && isConnected) { + if (bus && isConnected && useDb) { this.setState({ lastFetch: Date.now() }) bus.self( CYPHER_REQUEST, { - query: sysinfoQuery(useDb), + query: sysinfoQuery({ + databaseName: useDb, + namespacesEnabled, + userConfiguredPrefix + }), queryType: NEO4J_BROWSER_USER_ACTION_QUERY }, - responseHandler(newState => { - this.setState(newState) - }, useDb) + responseHandler(this.setState.bind(this)) ) - } else { - this.setState({ error: 'No connection available' }) } } - setAutoRefresh(autoRefresh: boolean): void { + setAutoRefresh = (autoRefresh: boolean): void => { this.setState({ autoRefresh }) if (autoRefresh) { @@ -141,21 +194,16 @@ export class SysInfoFrame extends Component< render(): ReactNode { const { autoRefresh, - error, + errorMessage, idAllocation, lastFetch, pageCache, storeSizes, - success, transactions } = this.state const { databases, frame, isConnected, isEnterprise } = this.props - const content = !isConnected ? ( - - ) : ( + const content = isConnected ? ( + ) : ( + ) return ( @@ -172,21 +224,20 @@ export class SysInfoFrame extends Component< contents={content} statusbar={ - {error && } - {success && ( - - {lastFetch && `Updated: ${new Date(lastFetch).toISOString()}`} - - {success} - - - this.setAutoRefresh(e.target.checked)} - /> - - - )} + + {lastFetch && `Updated: ${new Date(lastFetch).toISOString()}`} + {errorMessage && ( + + {errorMessage} + + )} + + this.setAutoRefresh(e.target.checked)} + /> + + } /> diff --git a/src/browser/modules/Stream/SysInfoFrame/helpers.tsx b/src/browser/modules/Stream/SysInfoFrame/helpers.tsx index 5cefd012224..1f077efb16f 100644 --- a/src/browser/modules/Stream/SysInfoFrame/helpers.tsx +++ b/src/browser/modules/Stream/SysInfoFrame/helpers.tsx @@ -21,71 +21,149 @@ import { flattenAttributes } from './sysinfo-utils' import { toHumanReadableBytes } from 'services/utils' -const jmxPrefix = 'neo4j.metrics:name=' - -export const sysinfoQuery = (useDb?: string | null): string => ` -// Store size. Per db -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.store.size.total") YIELD name, attributes -RETURN "Store Size" AS group, name, attributes -UNION ALL - -// Page cache. Per DBMS. -CALL dbms.queryJmx("${jmxPrefix}neo4j.page_cache.flushes") YIELD name, attributes -RETURN "Page Cache" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.page_cache.evictions") YIELD name, attributes -RETURN "Page Cache" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.page_cache.eviction_exceptions") YIELD name, attributes -RETURN "Page Cache" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.page_cache.hit_ratio") YIELD name, attributes -RETURN "Page Cache" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.page_cache.usage_ratio") YIELD name, attributes -RETURN "Page Cache" AS group, name, attributes -UNION ALL - -// Primitive counts. Per db. -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.ids_in_use.node") YIELD name, attributes -RETURN "Primitive Count" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.ids_in_use.property") YIELD name, attributes -RETURN "Primitive Count" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.ids_in_use.relationship") YIELD name, attributes -RETURN "Primitive Count" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.ids_in_use.relationship_type") YIELD name, attributes -RETURN "Primitive Count" AS group, name, attributes -UNION ALL - -// Transactions. Per db. -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.transaction.last_committed_tx_id") YIELD name, attributes -RETURN "Transactions" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.transaction.active") YIELD name, attributes -RETURN "Transactions" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.transaction.peak_concurrent") YIELD name, attributes -RETURN "Transactions" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.transaction.started") YIELD name, attributes -RETURN "Transactions" AS group, name, attributes -UNION ALL -CALL dbms.queryJmx("${jmxPrefix}neo4j.${useDb}.transaction.committed") YIELD name, attributes -RETURN "Transactions" AS group, name, attributes -` - -export const responseHandler = ( - setState: (newState: any) => void, - useDb?: string | null -) => +/* +The database provides a number of ways to monitor it's health, we use JMX MBeans. +JMX MBeans is a java extension that allows us to query the database for stats and is enabled by default since neo4j 4.2.2. +It's used through the `dbms.queryJmx(=)` where the searchprefix is '.metrics:name' + and the metric_name name has a few variations and depends on the following: +- If it's a "global" or "database" metric (global meaning the entire dbms in this context) +- What `metrics.prefix` is set to in neo4j.conf (default is neo4j) +- If `metrics.namespaces.enabled` is true of false in neo4j.conf (this setting was introduced when multidb was added) + +An example using the `store.size.total` metric with the following config: +- which is a "database" metric, +- with namespaces.enabled=false, +- against a database called foo and +- metrics.prefix=abc + +The metric name will be : +abc.foo.store.size.total + +And the full query: +CALL dbms.queryJmx("abc.metrics:name=abc.foo.store.size.total") + +When a query is malformed, or the specific metric is filtered out an empty array is returned but no error. +So to debug a jmx query make sure to read the docs on the exact syntax and check the metrics.filter setting. + +See docs for reference on what metrics exist & how to correctly query jmx: https://neo4j.com/docs/operations-manual/current/monitoring/metrics/reference/ +See docs for what metrics are filtered out by default and other for relevant settings: https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/#config_metrics.namespaces.enabled +*/ +type SysInfoMetrics = { + group: string + type: MetricType + baseMetricNames: string[] +} +const sysInfoMetrics: SysInfoMetrics[] = [ + { + group: 'Store Size', + type: 'database', + baseMetricNames: ['store.size.total'] + }, + { + group: 'Page Cache', + type: 'dbms', + baseMetricNames: [ + 'page_cache.hits', + 'page_cache.hit_ratio', + 'page_cache.usage_ratio', + 'page_cache.page_faults' + ] + }, + { + group: 'Primitive Count', + type: 'database', + baseMetricNames: [ + 'ids_in_use.node', + 'ids_in_use.property', + 'ids_in_use.relationship', + 'ids_in_use.relationship_type' + ] + }, + { + group: 'Transactions', + type: 'database', + baseMetricNames: [ + 'transaction.last_committed_tx_id', + 'transaction.peak_concurrent', + 'transaction.active_read', + 'transaction.active_write', + 'transaction.committed_read', + 'transaction.committed_write' + ] + } +] + +type MetricType = 'database' | 'dbms' +type MetricSettings = { + databaseName: string + namespacesEnabled: boolean + userConfiguredPrefix: string +} +type ConstructorParams = MetricSettings & { + baseMetricName: string + type: MetricType + group: string +} + +function constructQuery({ + userConfiguredPrefix, + namespacesEnabled, + databaseName, + baseMetricName, + type, + group +}: ConstructorParams) { + // Build full metric name of format: + // .[namespace?].[databaseName?]. + const parts = [] + if (namespacesEnabled) { + parts.push(type) + } + + if (type === 'database') { + parts.push(databaseName) + } + + parts.push(baseMetricName) + const metricName = parts.join('.') + + return `CALL dbms.queryJmx("${userConfiguredPrefix}.metrics:name=${userConfiguredPrefix}.${metricName}") YIELD name, attributes RETURN "${group}" AS group, name, attributes` +} + +export function sysinfoQuery({ + databaseName, + namespacesEnabled, + userConfiguredPrefix +}: MetricSettings): string { + const queries = sysInfoMetrics + .map(({ group, type, baseMetricNames }) => + baseMetricNames.map(baseMetricName => + constructQuery({ + databaseName, + namespacesEnabled, + userConfiguredPrefix, + baseMetricName, + group, + type + }) + ) + ) + .reduce(flatten, []) + const joinedToBigQuery = queries.join(' UNION ALL\n') + ';' + return joinedToBigQuery +} + +function flatten(acc: T[], curr: T[]): T[] { + return acc.concat(curr) +} + +export const responseHandler = (setState: (newState: any) => void) => function(res: any): void { if (!res || !res.result || !res.result.records) { - setState({ success: false }) + setState({ errorMessage: 'Call to dbms.queryJmx failed' }) return } + const intoGroups = res.result.records.reduce( (grouped: any, record: any) => { if (!grouped.hasOwnProperty(record.get('group'))) { @@ -95,7 +173,10 @@ export const responseHandler = ( } } const mappedRecord = { - name: record.get('name').replace(jmxPrefix, ''), + name: record + .get('name') + .split('.') + .pop(), value: ( record.get('attributes').Count || record.get('attributes').Value ).value @@ -111,26 +192,23 @@ export const responseHandler = ( const storeSizes = [ { label: 'Size', - value: toHumanReadableBytes(size[`neo4j.${useDb}.store.size.total`]) + value: size.total ? toHumanReadableBytes(size.total) : size.total } ] + const cache = flattenAttributes(intoGroups['Page Cache']) const pageCache = [ - { label: 'Flushes', value: cache['neo4j.page_cache.flushes'] }, - { label: 'Evictions', value: cache['neo4j.page_cache.evictions'] }, - { - label: 'Eviction Exceptions', - value: cache['neo4j.page_cache.eviction_exceptions'] - }, + { label: 'Hits', value: cache.hits }, + { label: 'Page Faults', value: cache.page_faults }, { label: 'Hit Ratio', - value: cache['neo4j.page_cache.hit_ratio'], + value: cache.hit_ratio, mapper: (v: number) => `${(v * 100).toFixed(2)}%`, optional: true }, { label: 'Usage Ratio', - value: cache['neo4j.page_cache.usage_ratio'], + value: cache.usage_ratio, mapper: (v: number) => `${(v * 100).toFixed(2)}%`, optional: true } @@ -139,18 +217,18 @@ export const responseHandler = ( // Primitive count const primitive = flattenAttributes(intoGroups['Primitive Count']) const idAllocation = [ - { label: 'Node ID', value: primitive[`neo4j.${useDb}.ids_in_use.node`] }, + { label: 'Node ID', value: primitive.node }, { label: 'Property ID', - value: primitive[`neo4j.${useDb}.ids_in_use.property`] + value: primitive.property }, { label: 'Relationship ID', - value: primitive[`neo4j.${useDb}.ids_in_use.relationship`] + value: primitive.relationship }, { label: 'Relationship Type ID', - value: primitive[`neo4j.${useDb}.ids_in_use.relationship_type`] + value: primitive.relationship_type } ] @@ -159,22 +237,34 @@ export const responseHandler = ( const transactions = [ { label: 'Last Tx Id', - value: tx[`neo4j.${useDb}.transaction.last_committed_tx_id`] + value: tx.last_committed_tx_id }, - { label: 'Current', value: tx[`neo4j.${useDb}.transaction.active`] }, + { label: 'Current Read', value: tx.active_read }, + { label: 'Current Write', value: tx.active_write }, { - label: 'Peak', - value: tx[`neo4j.${useDb}.transaction.peak_concurrent`] + label: 'Peak Transactions', + value: tx.peak_concurrent }, - { label: 'Opened', value: tx[`neo4j.${useDb}.transaction.started`] }, - { label: 'Committed', value: tx[`neo4j.${useDb}.transaction.committed`] } + { label: 'Committed Read', value: tx.committed_read }, + { label: 'Committed Write', value: tx.committed_write } ] + const valuesMissing = [ + storeSizes, + pageCache, + idAllocation, + transactions + ].some(metricType => + metricType.some((item: any) => item.value === undefined) + ) + setState({ pageCache, storeSizes, idAllocation, transactions, - success: true + errorMessage: valuesMissing + ? 'Some metrics missing, check neo4j.conf' + : null }) } diff --git a/src/browser/modules/Stream/SysInfoFrame/legacyHelpers.tsx b/src/browser/modules/Stream/SysInfoFrame/legacyHelpers.tsx index b966260cad9..98f615eee15 100644 --- a/src/browser/modules/Stream/SysInfoFrame/legacyHelpers.tsx +++ b/src/browser/modules/Stream/SysInfoFrame/legacyHelpers.tsx @@ -19,12 +19,8 @@ */ import React from 'react' -import { - getTableDataFromRecords, - mapLegacySysInfoRecords, - buildTableData -} from './sysinfo-utils' -import { toHumanReadableBytes, toKeyString } from 'services/utils' +import { getTableDataFromRecords, buildTableData } from './sysinfo-utils' +import { toHumanReadableBytes } from 'services/utils' import arrayHasItems from 'shared/utils/array-has-items' import Render from 'browser-components/Render' import { diff --git a/src/browser/modules/Stream/SysInfoFrame/styled.tsx b/src/browser/modules/Stream/SysInfoFrame/styled.tsx new file mode 100644 index 00000000000..98a6c1c6eec --- /dev/null +++ b/src/browser/modules/Stream/SysInfoFrame/styled.tsx @@ -0,0 +1,6 @@ +import styled from 'styled-components' + +export const InlineError = styled.span` + color: ${props => props.theme.error}; + padding-left: 15px; +` diff --git a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap index de46136b7d4..49f28da9660 100644 --- a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap +++ b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap @@ -10,13 +10,13 @@ exports[`SchemaFrame renders empty 1`] = ` class="sc-dqBHgY eVMXHH" > @@ -24,10 +24,10 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -35,13 +35,13 @@ exports[`SchemaFrame renders empty 1`] = `
Indexes
None
@@ -49,10 +49,10 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -92,43 +92,43 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` class="sc-dqBHgY eVMXHH" >
Constraints
None
@@ -136,42 +136,42 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = `
Index Name Type Uniqueness EntityType LabelsOrTypes Properties State
None
@@ -179,10 +179,10 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` @@ -222,13 +222,13 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` class="sc-dqBHgY eVMXHH" >
Constraints
None
@@ -236,10 +236,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` @@ -247,13 +247,13 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = `
Indexes
ON :Movie(released) ONLINE
@@ -261,10 +261,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = `
Constraints
ON ( book:Book ) ASSERT book.isbn IS UNIQUE