Skip to content

Commit

Permalink
MM-61991 Show server hostname in about modal (mattermost#29413)
Browse files Browse the repository at this point in the history
This introduces a new entry in the `Main Menu -> About` modal with the hostname of the currently connected websocket. This will be used to aid debugging issues in clustered environments by showing which node in the cluster is servicing requests for a particular websocket.

This information is only visible in self-managed instances. It will not be visible on cloud instances.
  • Loading branch information
davidkrauser authored Dec 6, 2024
1 parent 59b8532 commit 3224e0d
Show file tree
Hide file tree
Showing 16 changed files with 121 additions and 3 deletions.
10 changes: 10 additions & 0 deletions server/channels/app/platform/web_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -776,6 +777,15 @@ func (wc *WebConn) createHelloMessage() *model.WebSocketEvent {
wc.Platform.ClientConfigHash(),
ee))
msg.Add("connection_id", wc.connectionID.Load())

hostname, err := os.Hostname()
if err != nil {
wc.Platform.logger.Error("Could not get hostname", mlog.Err(err))
// return without the hostname in the message
return msg
}

msg.Add("server_hostname", hostname)
return msg
}

Expand Down
8 changes: 8 additions & 0 deletions webapp/channels/src/actions/websocket_actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,7 @@ export function handleStatusChangedEvent(msg) {
function handleHelloEvent(msg) {
dispatch(setServerVersion(msg.data.server_version));
dispatch(setConnectionId(msg.data.connection_id));
dispatch(setServerHostname(msg.data.server_hostname));
}

function handleReactionAddedEvent(msg) {
Expand All @@ -1333,6 +1334,13 @@ function setConnectionId(connectionId) {
};
}

function setServerHostname(serverHostname) {
return {
type: GeneralTypes.SET_SERVER_HOSTNAME,
payload: {serverHostname},
};
}

function handleAddEmoji(msg) {
const data = JSON.parse(msg.data.emoji);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@ describe('components/AboutBuildModal', () => {

let config: Partial<ClientConfig> = {};
let license: ClientLicense = {};
let socketStatus = {
connected: false,
serverHostname: '',
};

afterEach(() => {
global.Date = RealDate;
config = {};
license = {};
socketStatus = {
connected: false,
serverHostname: '',
};
});

beforeEach(() => {
Expand All @@ -51,17 +59,22 @@ describe('components/AboutBuildModal', () => {
IsLicensed: 'true',
Company: 'Mattermost Inc',
};
socketStatus = {
connected: true,
serverHostname: 'mock.localhost',
};
});

test('should match snapshot for enterprise edition', () => {
renderAboutBuildModal({config, license});
renderAboutBuildModal({config, license, socketStatus});
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.0');
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123456');
expect(screen.getByText('Mattermost Enterprise Edition')).toBeInTheDocument();
expect(screen.getByText('Modern communication from behind your firewall.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com'})).toHaveAttribute('href', 'https://mattermost.com/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.getByText('EE Build Hash: 0123456789abcdef', {exact: false})).toBeInTheDocument();
expect(screen.queryByText('Hostname: mock.localhost', {exact: false})).toBeInTheDocument();

expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
Expand All @@ -75,14 +88,15 @@ describe('components/AboutBuildModal', () => {
BuildHashEnterprise: '',
};

renderAboutBuildModal({config: teamConfig, license: {}});
renderAboutBuildModal({config: teamConfig, license: {}, socketStatus: {connected: false}});
expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: 3.6.0');
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
expect(screen.getByTestId('aboutModalBuildNumber')).toHaveTextContent('Build Number: 123456');
expect(screen.getByText('Mattermost Team Edition')).toBeInTheDocument();
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
expect(screen.queryByText('Hostname: disconnected', {exact: false})).toBeInTheDocument();

expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
Expand Down Expand Up @@ -123,7 +137,7 @@ describe('components/AboutBuildModal', () => {
BuildNumber: 'dev',
};

renderAboutBuildModal({config: sameBuildConfig, license: {}});
renderAboutBuildModal({config: sameBuildConfig, license: {}, socketStatus: {connected: true}});

expect(screen.getByTestId('aboutModalVersion')).toHaveTextContent('Mattermost Version: dev');
expect(screen.getByTestId('aboutModalDBVersionString')).toHaveTextContent('Database Schema Version: 77');
Expand All @@ -132,6 +146,7 @@ describe('components/AboutBuildModal', () => {
expect(screen.getByText('All your team communication in one place, instantly searchable and accessible anywhere.')).toBeInTheDocument();
expect(screen.getByRole('link', {name: 'mattermost.com/community/'})).toHaveAttribute('href', 'https://mattermost.com/community/?utm_source=mattermost&utm_medium=in-product&utm_content=about_build_modal&uid=&sid=');
expect(screen.queryByText('EE Build Hash: 0123456789abcdef')).not.toBeInTheDocument();
expect(screen.queryByText('Hostname: server did not provide hostname', {exact: false})).toBeInTheDocument();

expect(screen.getByRole('link', {name: 'server'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-server/blob/master/NOTICE.txt');
expect(screen.getByRole('link', {name: 'desktop'})).toHaveAttribute('href', 'https://github.com/mattermost/desktop/blob/master/NOTICE.txt');
Expand All @@ -158,6 +173,7 @@ describe('components/AboutBuildModal', () => {
<AboutBuildModal
config={config}
license={license}
socketStatus={socketStatus}
onExited={onExited}
/>,
state,
Expand Down Expand Up @@ -185,6 +201,7 @@ describe('components/AboutBuildModal', () => {
<AboutBuildModal
config={config}
license={license}
socketStatus={socketStatus}
onExited={jest.fn()}
/>,
state,
Expand All @@ -207,6 +224,7 @@ describe('components/AboutBuildModal', () => {
onExited,
config,
license,
socketStatus,
...props,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import {AboutLinks} from 'utils/constants';

import AboutBuildModalCloud from './about_build_modal_cloud/about_build_modal_cloud';

type SocketStatus = {
connected: boolean;
serverHostname: string | undefined;
}

type Props = {

/**
Expand All @@ -31,6 +36,8 @@ type Props = {
* Global license object
*/
license: ClientLicense;

socketStatus: SocketStatus;
};

type State = {
Expand Down Expand Up @@ -182,6 +189,48 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {

const mmversion: string | undefined = config.BuildNumber === 'dev' ? config.BuildNumber : config.Version;

let serverHostname;
if (!this.props.socketStatus.connected) {
serverHostname = (
<div>
<FormattedMessage
id='about.serverHostname'
defaultMessage='Hostname:'
/>
<Nbsp/>
<FormattedMessage
id='about.serverDisconnected'
defaultMessage='disconnected'
/>
</div>
);
} else if (this.props.socketStatus.serverHostname) {
serverHostname = (
<div>
<FormattedMessage
id='about.serverHostname'
defaultMessage='Hostname:'
/>
<Nbsp/>
{this.props.socketStatus.serverHostname}
</div>
);
} else {
serverHostname = (
<div>
<FormattedMessage
id='about.serverHostname'
defaultMessage='Hostname:'
/>
<Nbsp/>
<FormattedMessage
id='about.serverUnknown'
defaultMessage='server did not provide hostname'
/>
</div>
);
}

return (
<Modal
dialogClassName='a11y__modal about-modal'
Expand Down Expand Up @@ -246,6 +295,7 @@ export default class AboutBuildModal extends React.PureComponent<Props, State> {
/>
{'\u00a0' + config.SQLDriverName}
</div>
{serverHostname}
</div>
{licensee}
</div>
Expand Down
3 changes: 3 additions & 0 deletions webapp/channels/src/components/about_build_modal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {connect} from 'react-redux';

import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';

import {getSocketStatus} from 'selectors/views/websocket';

import type {GlobalState} from 'types/store';

import AboutBuildModal from './about_build_modal';
Expand All @@ -13,6 +15,7 @@ function mapStateToProps(state: GlobalState) {
return {
config: getConfig(state),
license: getLicense(state),
socketStatus: getSocketStatus(state),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
Expand Down Expand Up @@ -163,6 +164,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('ProductNoticesModal', () => {
connectionId: '',
lastConnectAt: 1599760193593,
lastDisconnectAt: 0,
serverHostname: '',
},
actions: {
getInProductNotices: jest.fn().mockResolvedValue({data: noticesData}),
Expand Down
3 changes: 3 additions & 0 deletions webapp/channels/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"about.licensed": "Licensed to:",
"about.notice": "Mattermost is made possible by the open source software used in our <linkServer>server</linkServer>, <linkDesktop>desktop</linkDesktop> and <linkMobile>mobile</linkMobile> apps.",
"about.privacy": "Privacy Policy",
"about.serverDisconnected": "disconnected",
"about.serverHostname": "Hostname:",
"about.serverUnknown": "server did not provide hostname",
"about.teamEditionLearn": "Join the Mattermost community at ",
"about.teamEditionSt": "All your team communication in one place, instantly searchable and accessible anywhere.",
"about.teamEditiont0": "Team Edition",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default keyMirror({
WEBSOCKET_FAILURE: null,
WEBSOCKET_CLOSED: null,
SET_CONNECTION_ID: null,
SET_SERVER_HOSTNAME: null,

SET_CONFIG_AND_LICENSE: null,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function getInitialState() {
lastConnectAt: 0,
lastDisconnectAt: 0,
connectionId: '',
serverHostname: '',
};
}

Expand All @@ -26,6 +27,7 @@ export default function reducer(state = getInitialState(), action: AnyAction) {
...state,
connected: false,
lastDisconnectAt: action.timestamp,
serverHostname: '',
};
}

Expand All @@ -44,5 +46,12 @@ export default function reducer(state = getInitialState(), action: AnyAction) {
};
}

if (action.type === GeneralTypes.SET_SERVER_HOSTNAME) {
return {
...state,
serverHostname: action.payload.serverHostname,
};
}

return state;
}
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ const state: GlobalState = {
lastConnectAt: 0,
lastDisconnectAt: 0,
connectionId: '',
serverHostname: '',
},
};
export default state;
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ exports[`plugins/Pluggable should match snapshot with extended component 1`] = `
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
Expand Down Expand Up @@ -262,6 +263,7 @@ exports[`plugins/Pluggable should match snapshot with extended component with pl
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
Expand Down Expand Up @@ -534,6 +536,7 @@ exports[`plugins/Pluggable should match snapshot with null pluggableId 1`] = `
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
Expand Down Expand Up @@ -673,6 +676,7 @@ exports[`plugins/Pluggable should match snapshot with valid pluggableId 1`] = `
"reconnectListeners": Set {},
"responseCallbacks": Object {},
"responseSequence": 1,
"serverHostname": "",
"serverSequence": 0,
}
}
Expand Down
1 change: 1 addition & 0 deletions webapp/channels/src/sass/responsive/_mobile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,7 @@

.about-modal__content {
display: block;
overflow-wrap: anywhere;
}

.about-modal__hash {
Expand Down
1 change: 1 addition & 0 deletions webapp/channels/src/sass/routes/_about-modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
display: flex;
flex-direction: row;
padding: 1em 0 3em;
overflow-wrap: anywhere;
}

.about-modal__copyright {
Expand Down
5 changes: 5 additions & 0 deletions webapp/platform/client/src/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default class WebSocketClient {
private closeListeners = new Set<CloseListener>();

private connectionId: string | null;
private serverHostname: string | null;
private postedAck: boolean;

constructor() {
Expand All @@ -78,6 +79,7 @@ export default class WebSocketClient {
this.connectFailCount = 0;
this.responseCallbacks = {};
this.connectionId = '';
this.serverHostname = '';
this.postedAck = false;
}

Expand Down Expand Up @@ -210,6 +212,9 @@ export default class WebSocketClient {
// If it's a fresh connection, we have to set the connectionId regardless.
// And if it's an existing connection, setting it again is harmless, and keeps the code simple.
this.connectionId = msg.data.connection_id;

// Also update the server hostname
this.serverHostname = msg.data.server_hostname;
}

// Now we check for sequence number, and if it does not match,
Expand Down
Loading

0 comments on commit 3224e0d

Please sign in to comment.