Skip to content

Commit

Permalink
Merge pull request #2 from 8Rav3n/feature/log
Browse files Browse the repository at this point in the history
Feature/log
  • Loading branch information
8Rav3n committed Aug 31, 2023
2 parents f6f4471 + 8204105 commit fdb38c7
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 13 deletions.
47 changes: 46 additions & 1 deletion docs/modules/ROOT/pages/developer-guide/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ will look like this:
"standaloneDashboardName": "My Dashboard",
"standaloneDashboardDatabase": "dashboards",
"standaloneDashboardURL": "",
"standaloneAllowLoad": true,
"loggingMode": "0",
"loggingDatabase": "logs",
"standaloneAllowLoad": false,
"standaloneLoadFromOtherDatabases": false
}
....
Expand Down Expand Up @@ -87,6 +89,21 @@ inside Neo4j and would like to run a standalone mode deployment with a
dashboard from a URL, set this parameter to the complete URL pointing to
the dashboard JSON.

|loggingMode |string |none |Determines whether neodash should create any
user activity logs. possible values include: `0` (no log is created),
`1` (user login are tracked), `2` (tracks when a specific dashboard is
accessed/loaded or saved by a user*).

⚠️ Logs are created in Neo4J DB using the current user credentials
(or standaloneUsername if configured); write access to the log database
must be granted to enble any user to create logs.

⚠️ * Load/Save from/to file are not logged (only from/to Database)

|loggingDatabase |string |neo4j |When loggingMode is set to anything
else than '0', the database to use for logging. Log records (nodes)
will be created in this database.

|standaloneAllowLoad |boolean |false |If set t yes the "Load Dashboard"
button will be enabled in standalone mode, allowing users to load
additional dashboards from Neo4J.
Expand Down Expand Up @@ -147,3 +164,31 @@ The `standaloneDashboardName` and `standaloneDashboardDatabase` config
parameters are used to define these.
** A standalone deployment that *reads the fixed dashboard from a URL*.
The `standaloneDashboardURL` config parameter is used to define this.

== Configuring Logging

NeoDash treats log records with an approach similar to dashboards metadata:
saving them in a Neo4J database as distinct nodes. Each node has a label
"_Neodash_Log" and a standard set of properties such as DateTime (date),
UserID (user), Databadse Name (dashboard) Dashboard Name or UUID (dashboard),
NeoDash mode (neodash_mmode), Action (action) and a short message (message).

⚠️ Logs are created using the credentials of the current user (either from
logon or, if configured, from standaloneUsername), therefore, for it to work,
administrator must ensure that every neodash user has write access to the
__Neodash_Log_ label in the database confugred in the loggingDatabase prameter
(which must also be available on the same Neo4J instance where dashboards and
data are stored).
This can be achieved by creating a standard NeoDashUser role in Neo4j with
such permissions and assigning this role to every neoDash user. For more
details on Neo4J granular access control please refer to the link:
https://neo4j.com/docs/operations-manual/current/authentication-authorization/access-control/[product documentation]
In case of any misconfiguration (e.g. user not having grants to write in the
loggingDatabase), the application will show an error notification every time
a log creation fails (e.g. for a loggingMode set to '2', at user connection
attempt and any subsequent attempt to load or save a dashboard); the error will
not block the application flow (users will still be able to connect, load and
save dashboards normally); additionally, to limit user discomfort in such
scenario, the notification is shown for 3 times only in a single user session.
After 3rd time, a line will still be logged in the javascript console of user
browser but interactive notifictions will be suppressed.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ docker run -it --rm -p 5005:5005 \
-e standaloneDatabase="neo4j" \
-e standaloneDashboardName="My Dashboard" \
-e standaloneDashboardDatabase="dashboards" \
...
neo4jlabs/neodash
....

Expand Down
2 changes: 2 additions & 0 deletions docs/modules/ROOT/pages/developer-guide/state-management.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ standalone mode.
"standaloneDatabase": "neo4j",
"standaloneDashboardName": "My Dashboard",
"standaloneDashboardDatabase": "dashboards",
"loggingMode": "0",
"loggingDatabase": "logging",
"standaloneAllowLoad": false,
"standaloneLoadFromOtherDatabases ": false,
"notificationIsDismissable": null
Expand Down
2 changes: 2 additions & 0 deletions public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"standaloneDashboardName": "My Dashboard",
"standaloneDashboardDatabase": "dashboards",
"standaloneDashboardURL": "",
"loggingMode": "0",
"loggingDatabase": "logs",
"standaloneAllowLoad": false,
"standaloneLoadFromOtherDatabases": false
}
2 changes: 2 additions & 0 deletions scripts/config-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ echo " \
\"standaloneDashboardName\": \"${standaloneDashboardName:='My Dashboard'}\", \
\"standaloneDashboardDatabase\": \"${standaloneDashboardDatabase:='neo4j'}\", \
\"standaloneDashboardURL\": \"${standaloneDashboardURL:=}\", \
\"loggingMode\": \"${loggingMode:='0'}\", \
\"loggingMode\": \"${loggingDatabase:='logs'}\", \
\"standaloneAllowLoad\": \"${standaloneAllowLoad:=false}\", \
\"standaloneLoadFromOtherDatabases\": \"${standaloneLoadFromOtherDatabases:=false}\" \
}" > /usr/share/nginx/html/config.json
18 changes: 18 additions & 0 deletions src/application/ApplicationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ export const setStandaloneDashboardDatabase = (dashboardDatabase: string) => ({
payload: { dashboardDatabase },
});

export const SET_LOGGING_MODE = 'APPLICATION/SET_LOGGING_MODE';
export const setLoggingMode = (loggingMode: string) => ({
type: SET_LOGGING_MODE,
payload: { loggingMode },
});

export const SET_LOGGING_DATABASE = 'APPLICATION/SET_LOGGING_DATABASE';
export const setLoggingDatabase = (loggingDatabase: string) => ({
type: SET_LOGGING_DATABASE,
payload: { loggingDatabase },
});

export const SET_LOG_ERROR_NOTIFICATION = 'APPLICATION/SET_LOG_ERROR_NOTIFICATION';
export const setLogErrorNotification = (logErrorNotification: any) => ({
type: SET_LOG_ERROR_NOTIFICATION,
payload: { logErrorNotification },
});

export const SET_SSO_ENABLED = 'APPLICATION/SET_SSO_ENABLED';
export const setSSOEnabled = (enabled: boolean, discoveryUrl: string) => ({
type: SET_SSO_ENABLED,
Expand Down
20 changes: 20 additions & 0 deletions src/application/ApplicationReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
SET_STANDALONE_DASHBOARD_DATEBASE,
SET_STANDALONE_ENABLED,
SET_STANDALONE_MODE,
SET_LOGGING_MODE,
SET_LOGGING_DATABASE,
SET_LOG_ERROR_NOTIFICATION,
SET_WAIT_FOR_SSO,
SET_WELCOME_SCREEN_OPEN,
} from './ApplicationActions';
Expand All @@ -50,6 +53,8 @@ const initialState = {
dashboardToLoadAfterConnecting: null,
waitForSSO: false,
standalone: false,
loggingMode: '0',
logErrorNotification: '3',
};
export const applicationReducer = (state = initialState, action: { type: any; payload: any }) => {
const { type, payload } = action;
Expand Down Expand Up @@ -107,6 +112,21 @@ export const applicationReducer = (state = initialState, action: { type: any; pa
state = update(state, { standalone: standalone });
return state;
}
case SET_LOGGING_MODE: {
const { loggingMode } = payload;
state = update(state, { loggingMode: loggingMode });
return state;
}
case SET_LOGGING_DATABASE: {
const { loggingDatabase } = payload;
state = update(state, { loggingDatabase: loggingDatabase });
return state;
}
case SET_LOG_ERROR_NOTIFICATION: {
const { logErrorNotification } = payload;
state = update(state, { logErrorNotification: logErrorNotification });
return state;
}
case SET_SSO_ENABLED: {
const { enabled, discoveryUrl } = payload;
state = update(state, { ssoEnabled: enabled, ssoDiscoveryUrl: discoveryUrl });
Expand Down
16 changes: 16 additions & 0 deletions src/application/ApplicationSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export const applicationGetConnectionDatabase = (state: any) => {
return state.application.connection.database;
};

export const applicationGetConnectionUser = (state: any) => {
return state.application.connection.username;
}

export const applicationGetShareDetails = (state: any) => {
return state.application.shareDetails;
};
Expand All @@ -41,6 +45,10 @@ export const applicationIsStandalone = (state: any) => {
return state.application.standalone;
};

export const applicationGetLoggingMode = (state: any) => {
return state.application.loggingMode;
};

export const applicationHasNeo4jDesktopConnection = (state: any) => {
return state.application.desktopConnection != null;
};
Expand Down Expand Up @@ -86,6 +94,14 @@ export const applicationGetStandaloneSettings = (state: any) => {
};
};

export const applicationGetLoggingSettings = (state: any) => {
return {
loggingMode: state.application.loggingMode,
loggingDatabase: state.application.loggingDatabase,
logErrorNotification: state.application.logErrorNotification,
};
};

export const applicationHasWelcomeScreenOpen = (state: any) => {
return state.application.welcomeScreenOpen;
};
Expand Down
113 changes: 112 additions & 1 deletion src/application/ApplicationThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@ import {
setAboutModalOpen,
setStandaloneMode,
setStandaloneDashboardDatabase,
setLoggingMode,
setLoggingDatabase,
setLogErrorNotification,
setWaitForSSO,
setParametersToLoadAfterConnecting,
setReportHelpModalOpen,
} from './ApplicationActions';
import { version } from '../modal/AboutModal';
import { applicationGetLoggingSettings, applicationIsStandalone } from './ApplicationSelectors';
import { createUUID } from '../utils/uuid';
import { valueIsRelationship } from '../chart/ChartUtils';

/**
* Application Thunks (https://redux.js.org/usage/writing-logic-thunks) handle complex state manipulations.
Expand All @@ -56,6 +62,9 @@ import { version } from '../modal/AboutModal';
*/
export const createConnectionThunk =
(protocol, url, port, database, username, password) => (dispatch: any, getState: any) => {
const loggingState = getState();
const loggingSettings = applicationGetLoggingSettings(loggingState)
const neodashMode = applicationIsStandalone(loggingState) ? 'Standalone' : 'Editor'
try {
const driver = createDriver(protocol, url, port, username, password, { userAgent: `neodash/v${version}` });
// eslint-disable-next-line no-console
Expand All @@ -65,13 +74,41 @@ export const createConnectionThunk =
console.log('Confirming connection was established...');
if (records && records[0] && records[0].error) {
dispatch(createNotificationThunk('Unable to establish connection', records[0].error));
} else if (records && records[0] && records[0].keys[0] == 'connected') {
if (loggingSettings.loggingMode>'0'){
dispatch(
createLogThunk(
driver,
loggingSettings.loggingDatabase,
neodashMode,
username,
'ERR - connect to DB',
database,
'',
'Error while trying to establish connection to Neo4j DB in ' +neodashMode+' mode at '+Date(Date.now()).substring(0,33)
)
);
}
} else if (records && records[0] && records[0].keys[0] == 'connected') {
dispatch(setConnectionProperties(protocol, url, port, database, username, password));
dispatch(setConnectionModalOpen(false));
dispatch(setConnected(true));
dispatch(updateSessionParameterThunk('session_uri', `${protocol}://${url}:${port}`));
dispatch(updateSessionParameterThunk('session_database', database));
dispatch(updateSessionParameterThunk('session_username', username));
if (loggingSettings.loggingMode>'0'){
dispatch(
createLogThunk(
driver,
loggingSettings.loggingDatabase,
neodashMode,
username,
'INF - connect to DB',
database,
'',
username +'established connection to Neo4j DB in ' +neodashMode+' mode at '+Date(Date.now()).substring(0,33)
)
);
}
// If we have remembered to load a specific dashboard after connecting to the database, take care of it here.
const { application } = getState();
if (
Expand Down Expand Up @@ -349,6 +386,9 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState:
standaloneDashboardName: 'My Dashboard',
standaloneDashboardDatabase: 'dashboards',
standaloneDashboardURL: '',
loggingMode: '0',
loggingDatabase: 'logs',
logErrorNotification: '3',
standaloneAllowLoad: false,
standaloneLoadFromOtherDatabases: false,
};
Expand Down Expand Up @@ -397,6 +437,11 @@ export const loadApplicationConfigThunk = () => async (dispatch: any, getState:
config.standaloneLoadFromOtherDatabases,
)
);

dispatch(setLoggingMode(config.loggingMode));
dispatch(setLoggingDatabase(config.loggingDatabase));
dispatch(setLogErrorNotification('3'));

dispatch(setConnectionModalOpen(false));

// Auto-upgrade the dashboard version if an old version is cached.
Expand Down Expand Up @@ -599,3 +644,69 @@ export const initializeApplicationAsStandaloneThunk =
dispatch(setConnectionModalOpen(true));
}
};

// Thunk to handle log events.
export const createLogThunk =
(loggingDriver, loggingDatabase, neodashMode, logUser, logAction, logDatabase, logDashboard = '', logMessage) =>
(dispatch: any, getState: any) => {
try {
const uuid = createUUID();
// Generate a cypher query to save the log.
const query = 'CREATE (n:_Neodash_Log) SET n.uuid = $uuid, n.user = $user, n.date = datetime(), n.neodash_mode = $neodashMode, n.action = $logAction, n.database = $logDatabase, n.dashboard = $logDashboard, n.message = $logMessage RETURN $uuid as uuid'

const parameters = {
uuid: uuid,
user: logUser,
logAction: logAction,
logDatabase: logDatabase,
neodashMode: neodashMode,
logDashboard: logDashboard,
logMessage: logMessage
};
runCypherQuery(
loggingDriver,
loggingDatabase,
query,
parameters,
1,
() => {},
(records) => {
if (records && records[0] && records[0]._fields && records[0]._fields[0] && records[0]._fields[0] == uuid) {
console.log('log created: '+uuid);
} else {
//we only show error notification one time
const state = getState()
const loggingSettings = applicationGetLoggingSettings (state);
var LogErrorNotificationNum = Number(loggingSettings.logErrorNotification)
console.log('Error creating log for '+((LogErrorNotificationNum-4)*(-1))+' times')
if (LogErrorNotificationNum > 0 ) {
dispatch(
createNotificationThunk(
'Error creating log',
LogErrorNotificationNum>1 ? `Please check logging configuration with your Neodash administrator`:`Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session`
)
);
}
LogErrorNotificationNum = LogErrorNotificationNum-1
dispatch(setLogErrorNotification (LogErrorNotificationNum.toString()));
}
}
);
} catch (e) {
//we only show error notification 3 times
const state = getState()
const loggingSettings = applicationGetLoggingSettings (state);
var LogErrorNotificationNum = Number(loggingSettings.logErrorNotification)
console.log('Error creating log for '+((LogErrorNotificationNum-4)*(-1))+' times')
if (LogErrorNotificationNum > 0 ) {
dispatch(
createNotificationThunk(
'Error creating log',
LogErrorNotificationNum>1 ? `Please check logging configuration with your Neodash administrator`:`Please check logging configuration with your Neodash administrator - This message will not be displayed anymore in the current session`
)
);
}
LogErrorNotificationNum = LogErrorNotificationNum-1
dispatch(setLogErrorNotification (LogErrorNotificationNum.toString()));
}
};
Loading

0 comments on commit fdb38c7

Please sign in to comment.