+ client: add response information to the filtered query log items

This commit is contained in:
Ildar Kamalov 2019-08-30 16:03:36 +03:00
parent 3b98461a2a
commit 428706399a
5 changed files with 315 additions and 249 deletions

View File

@ -176,6 +176,8 @@
"rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules", "rule_added_to_custom_filtering_toast": "Rule added to the custom filtering rules",
"query_log_disabled_toast": "Query log disabled", "query_log_disabled_toast": "Query log disabled",
"query_log_enabled_toast": "Query log enabled", "query_log_enabled_toast": "Query log enabled",
"query_log_response_status": "Status: {{value}}",
"query_log_filtered": "Filtered by {{filter}}",
"source_label": "Source", "source_label": "Source",
"found_in_known_domain_db": "Found in the known domains database.", "found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category", "category_label": "Category",

View File

@ -9,13 +9,12 @@
justify-content: center; justify-content: center;
} }
.logs__row--overflow { .logs__row--column {
overflow: hidden; align-items: flex-start;
flex-direction: column;
} }
.logs__row--column { .logs__row--overflow {
flex-direction: column;
align-items: flex-start;
overflow: hidden; overflow: hidden;
} }
@ -103,3 +102,20 @@
border-color: #1991eb; border-color: #1991eb;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25); box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
} }
.logs__text-wrap {
display: flex;
align-items: center;
max-width: 100%;
}
.logs__list-wrap {
display: flex;
max-width: 100%;
}
.logs__list-item {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

View File

@ -8,7 +8,7 @@ import { Trans, withNamespaces } from 'react-i18next';
import { HashLink as Link } from 'react-router-hash-link'; import { HashLink as Link } from 'react-router-hash-link';
import { formatTime, getClientName } from '../../helpers/helpers'; import { formatTime, getClientName } from '../../helpers/helpers';
import { SERVICES } from '../../helpers/constants'; import { SERVICES, FILTERED_STATUS } from '../../helpers/constants';
import { getTrackerData } from '../../helpers/trackers/trackers'; import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle'; import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card'; import Card from '../ui/Card';
@ -18,6 +18,11 @@ import Popover from '../ui/Popover';
import './Logs.css'; import './Logs.css';
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt'; const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
const FILTERED_REASON = 'Filtered';
const RESPONSE_FILTER = {
ALL: 'all',
FILTERED: 'filtered',
};
class Logs extends Component { class Logs extends Component {
componentDidMount() { componentDidMount() {
@ -38,11 +43,29 @@ class Logs extends Component {
if (this.props.dashboard.queryLogEnabled) { if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs(); this.props.getLogs();
} }
} };
renderTooltip = (isFiltered, rule, filter, service) => renderTooltip = (isFiltered, rule, filter, service) =>
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />; isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
renderResponseList = (response, status) => {
if (response.length > 0) {
const listItems = response.map((response, index) => (
<li key={index} title={response} className="logs__list-item">
{response}
</li>
));
return <ul className="list-unstyled">{listItems}</ul>;
}
return (
<div>
<Trans values={{ value: status }}>query_log_response_status</Trans>
</div>
);
};
toggleBlocking = (type, domain) => { toggleBlocking = (type, domain) => {
const { userRules } = this.props.filtering; const { userRules } = this.props.filtering;
const { t } = this.props; const { t } = this.props;
@ -63,7 +86,7 @@ class Logs extends Component {
} }
this.props.getFilteringStatus(); this.props.getFilteringStatus();
} };
renderBlockingButton(isFiltered, domain) { renderBlockingButton(isFiltered, domain) {
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger'; const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
@ -84,56 +107,58 @@ class Logs extends Component {
); );
} }
renderLogs(logs) { checkFiltered = reason => reason.indexOf(FILTERED_REASON) === 0;
const { t, dashboard } = this.props;
const columns = [{ checkRewrite = reason => reason === FILTERED_STATUS.REWRITE;
Header: t('time_table_header'),
accessor: 'time', checkWhiteList = reason => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST;
maxWidth: 110,
filterable: false, getTimeCell = ({ value }) => (
Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>), <div className="logs__row">
}, { <span className="logs__text" title={value}>
Header: t('domain_name_table_header'), {formatTime(value)}
accessor: 'domain', </span>
Cell: (row) => { </div>
);
getDomainCell = (row) => {
const response = row.value; const response = row.value;
const trackerData = getTrackerData(response); const trackerData = getTrackerData(response);
return ( return (
<div className="logs__row" title={response}> <div className="logs__row" title={response}>
<div className="logs__text"> <div className="logs__text">{response}</div>
{response} {trackerData && <Popover data={trackerData} />}
</div>
{trackerData && <Popover data={trackerData}/>}
</div> </div>
); );
}, };
}, {
Header: t('type_table_header'), getResponseCell = ({ value: responses, original }) => {
accessor: 'type', const {
maxWidth: 60, reason, filterId, rule, status,
}, { } = original;
Header: t('response_table_header'), const { t, filtering } = this.props;
accessor: 'response', const { filters } = filtering;
Cell: (row) => {
const responses = row.value; const isFiltered = this.checkFiltered(reason);
const { reason } = row.original; const filterKey = reason.replace(FILTERED_REASON, '');
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false; const parsedFilteredReason = t('query_log_filtered', { filter: filterKey });
const parsedFilteredReason = reason.replace('Filtered', 'Filtered by '); const isRewrite = this.checkRewrite(reason);
const rule = row && row.original && row.original.rule; const isWhiteList = this.checkWhiteList(reason);
const { filterId } = row.original; const isBlockedService = reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
const { filters } = this.props.filtering; const currentService = SERVICES.find(service => service.id === original.serviceName);
const isRewrite = reason && reason === 'Rewrite'; const serviceName = currentService && currentService.name;
let filterName = ''; let filterName = '';
if (reason === 'FilteredBlackList' || reason === 'NotFilteredWhiteList') {
if (filterId === 0) { if (filterId === 0) {
filterName = t('custom_filter_rules'); filterName = t('custom_filter_rules');
} else { } else {
const filterItem = Object.keys(filters) const filterItem = Object.keys(filters).filter(key => filters[key].id === filterId)[0];
.filter(key => filters[key].id === filterId);
if (typeof filterItem !== 'undefined' && typeof filters[filterItem] !== 'undefined') { if (
typeof filterItem !== 'undefined' &&
typeof filters[filterItem] !== 'undefined'
) {
filterName = filters[filterItem].name; filterName = filters[filterItem].name;
} }
@ -141,137 +166,148 @@ class Logs extends Component {
filterName = t('unknown_filter', { filterId }); filterName = t('unknown_filter', { filterId });
} }
} }
}
if (reason === 'FilteredBlockedService') {
const getService = SERVICES
.find(service => service.id === row.original.serviceName);
const serviceName = getService && getService.name;
return ( return (
<div className="logs__row"> <div className="logs__row logs__row--column">
<div className="logs__text-wrap">
{(isFiltered || isBlockedService) && (
<span className="logs__text" title={parsedFilteredReason}> <span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason} {parsedFilteredReason}
</span> </span>
{this.renderTooltip(isFiltered, '', '', serviceName)} )}
{isBlockedService
? this.renderTooltip(isFiltered, '', '', serviceName)
: this.renderTooltip(isFiltered, rule, filterName)}
{isRewrite && (
<strong>
<Trans>rewrite_applied</Trans>
</strong>
)}
</div>
<div className="logs__list-wrap">
{this.renderResponseList(responses, status)}
{isWhiteList && this.renderTooltip(isWhiteList, rule, filterName)}
</div>
</div> </div>
); );
} };
if (isFiltered) { getClientCell = ({ original, value }) => {
return ( const { dashboard } = this.props;
<div className="logs__row"> const { reason, domain } = original;
<span className="logs__text" title={parsedFilteredReason}> const isFiltered = this.checkFiltered(reason);
{parsedFilteredReason} const isRewrite = this.checkRewrite(reason);
</span> const clientName =
{this.renderTooltip(isFiltered, rule, filterName)} getClientName(dashboard.clients, value) || getClientName(dashboard.autoClients, value);
</div> let client = value;
);
}
if (responses.length > 0) {
const liNodes = responses.map((response, index) =>
(<li key={index} title={response}>{response}</li>));
const isRenderTooltip = reason === 'NotFilteredWhiteList';
return (
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
<ul className="list-unstyled">{liNodes}</ul>
{this.renderTooltip(isRenderTooltip, rule, filterName)}
</div>
);
}
return (
<div className={`logs__row ${isRewrite && 'logs__row--column'}`}>
{isRewrite && <strong><Trans>rewrite_applied</Trans></strong>}
<span><Trans>empty_response_status</Trans></span>
{this.renderTooltip(isFiltered, rule, filterName)}
</div>
);
},
filterMethod: (filter, row) => {
if (filter.value === 'filtered') {
// eslint-disable-next-line no-underscore-dangle
return row._original.reason.indexOf('Filtered') === 0 || row._original.reason === 'NotFilteredWhiteList';
}
return true;
},
Filter: ({ filter, onChange }) =>
<select
onChange={event => onChange(event.target.value)}
className="form-control"
value={filter ? filter.value : 'all'}
>
<option value="all">{ t('show_all_filter_type') }</option>
<option value="filtered">{ t('show_filtered_type') }</option>
</select>,
}, {
Header: t('client_table_header'),
accessor: 'client',
maxWidth: 250,
Cell: (row) => {
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
const isRewrite = reason && reason === 'Rewrite';
const clientName = getClientName(dashboard.clients, row.value)
|| getClientName(dashboard.autoClients, row.value);
let client;
if (clientName) { if (clientName) {
client = <span>{clientName} <small>({row.value})</small></span>; client = (
} else { <span>
client = row.value; {clientName} <small>({value})</small>
</span>
);
} }
if (isRewrite) {
return ( return (
<Fragment> <Fragment>
<div className="logs__row"> <div className="logs__row">{client}</div>
{client} {isRewrite ? (
</div>
<div className="logs__action"> <div className="logs__action">
<Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary"> <Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
<Trans>configure</Trans> <Trans>configure</Trans>
</Link> </Link>
</div> </div>
) : (
this.renderBlockingButton(isFiltered, domain)
)}
</Fragment> </Fragment>
); );
};
renderLogs(logs) {
const { t } = this.props;
const columns = [
{
Header: t('time_table_header'),
accessor: 'time',
maxWidth: 90,
filterable: false,
Cell: this.getTimeCell,
},
{
Header: t('domain_name_table_header'),
accessor: 'domain',
minWidth: 180,
Cell: this.getDomainCell,
},
{
Header: t('type_table_header'),
accessor: 'type',
maxWidth: 60,
},
{
Header: t('response_table_header'),
accessor: 'response',
minWidth: 250,
Cell: this.getResponseCell,
filterMethod: (filter, row) => {
if (filter.value === RESPONSE_FILTER.FILTERED) {
// eslint-disable-next-line no-underscore-dangle
const { reason } = row._original;
return (
this.checkFiltered(reason) ||
this.checkWhiteList(reason)
);
} }
return true;
return (
<Fragment>
<div className="logs__row">
{client}
</div>
{this.renderBlockingButton(isFiltered, row.original.domain)}
</Fragment>
);
}, },
Filter: ({ filter, onChange }) => (
<select
className="form-control"
onChange={event => onChange(event.target.value)}
value={filter ? filter.value : RESPONSE_FILTER.ALL}
>
<option value={RESPONSE_FILTER.ALL}>
<Trans>show_all_filter_type</Trans>
</option>
<option value={RESPONSE_FILTER.FILTERED}>
<Trans>show_filtered_type</Trans>
</option>
</select>
),
},
{
Header: t('client_table_header'),
accessor: 'client',
maxWidth: 220,
minWidth: 220,
Cell: this.getClientCell,
}, },
]; ];
if (logs) { if (logs) {
return (<ReactTable return (
className='logs__table' <ReactTable
className="logs__table"
filterable filterable
data={logs} data={logs}
columns={columns} columns={columns}
showPagination={true} showPagination={true}
defaultPageSize={50} defaultPageSize={50}
minRows={7} minRows={7}
// Text previousText={t('previous_btn')}
previousText={ t('previous_btn') } nextText={t('next_btn')}
nextText={ t('next_btn') } loadingText={t('loading_table_status')}
loadingText={ t('loading_table_status') } pageText={t('page_table_footer_text')}
pageText={ t('page_table_footer_text') } ofText={t('of_table_footer_text')}
ofText={ t('of_table_footer_text') } rowsText={t('rows_table_footer_text')}
rowsText={ t('rows_table_footer_text') } noDataText={t('no_logs_found')}
noDataText={ t('no_logs_found') }
defaultFilterMethod={(filter, row) => { defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id; const id = filter.pivotId || filter.id;
return row[id] !== undefined ? return row[id] !== undefined
String(row[id]).indexOf(filter.value) !== -1 : true; ? String(row[id]).indexOf(filter.value) !== -1
: true;
}} }}
defaultSorted={[ defaultSorted={[
{ {
@ -280,20 +316,21 @@ class Logs extends Component {
}, },
]} ]}
getTrProps={(_state, rowInfo) => { getTrProps={(_state, rowInfo) => {
// highlight filtered requests
if (!rowInfo) { if (!rowInfo) {
return {}; return {};
} }
if (rowInfo.original.reason.indexOf('Filtered') === 0) { const { reason } = rowInfo.original;
if (this.checkFiltered(reason)) {
return { return {
className: 'red', className: 'red',
}; };
} else if (rowInfo.original.reason === 'NotFilteredWhiteList') { } else if (this.checkWhiteList(reason)) {
return { return {
className: 'green', className: 'green',
}; };
} else if (rowInfo.original.reason === 'Rewrite') { } else if (this.checkRewrite(reason)) {
return { return {
className: 'blue', className: 'blue',
}; };
@ -303,9 +340,11 @@ class Logs extends Component {
className: '', className: '',
}; };
}} }}
/>); />
);
} }
return undefined;
return null;
} }
handleDownloadButton = async (e) => { handleDownloadButton = async (e) => {
@ -325,17 +364,23 @@ class Logs extends Component {
type="submit" type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)} onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing} disabled={logStatusProcessing}
><Trans>disabled_log_btn</Trans></button> >
<Trans>disabled_log_btn</Trans>
</button>
<button <button
className="btn btn-primary btn-sm mr-2" className="btn btn-primary btn-sm mr-2"
type="submit" type="submit"
onClick={this.handleDownloadButton} onClick={this.handleDownloadButton}
><Trans>download_log_file_btn</Trans></button> >
<Trans>download_log_file_btn</Trans>
</button>
<button <button
className="btn btn-outline-primary btn-sm" className="btn btn-outline-primary btn-sm"
type="submit" type="submit"
onClick={this.getLogs} onClick={this.getLogs}
><Trans>refresh_btn</Trans></button> >
<Trans>refresh_btn</Trans>
</button>
</Fragment> </Fragment>
); );
} }
@ -346,7 +391,9 @@ class Logs extends Component {
type="submit" type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)} onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
disabled={logStatusProcessing} disabled={logStatusProcessing}
><Trans>enabled_log_btn</Trans></button> >
<Trans>enabled_log_btn</Trans>
</button>
); );
} }
@ -355,24 +402,19 @@ class Logs extends Component {
const { queryLogEnabled } = dashboard; const { queryLogEnabled } = dashboard;
return ( return (
<Fragment> <Fragment>
<PageTitle title={ t('query_log') } subtitle={ t('last_dns_queries') }> <PageTitle title={t('query_log')} subtitle={t('last_dns_queries')}>
<div className="page-title__actions"> <div className="page-title__actions">
{this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)} {this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)}
</div> </div>
</PageTitle> </PageTitle>
<Card> <Card>
{ {queryLogEnabled &&
queryLogEnabled queryLogs.getLogsProcessing &&
&& queryLogs.getLogsProcessing dashboard.processingClients && <Loading />}
&& dashboard.processingClients {queryLogEnabled &&
&& <Loading /> !queryLogs.getLogsProcessing &&
} !dashboard.processingClients &&
{ this.renderLogs(queryLogs.logs)}
queryLogEnabled
&& !queryLogs.getLogsProcessing
&& !dashboard.processingClients
&& this.renderLogs(queryLogs.logs)
}
</Card> </Card>
</Fragment> </Fragment>
); );
@ -380,20 +422,17 @@ class Logs extends Component {
} }
Logs.propTypes = { Logs.propTypes = {
getLogs: PropTypes.func, getLogs: PropTypes.func.isRequired,
queryLogs: PropTypes.object, queryLogs: PropTypes.object.isRequired,
dashboard: PropTypes.object, dashboard: PropTypes.object.isRequired,
toggleLogStatus: PropTypes.func, toggleLogStatus: PropTypes.func.isRequired,
downloadQueryLog: PropTypes.func, downloadQueryLog: PropTypes.func.isRequired,
getFilteringStatus: PropTypes.func, getFilteringStatus: PropTypes.func.isRequired,
filtering: PropTypes.object, filtering: PropTypes.object.isRequired,
userRules: PropTypes.string, setRules: PropTypes.func.isRequired,
setRules: PropTypes.func, addSuccessToast: PropTypes.func.isRequired,
addSuccessToast: PropTypes.func,
processingRules: PropTypes.bool,
logStatusProcessing: PropTypes.bool,
t: PropTypes.func,
getClients: PropTypes.func.isRequired, getClients: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
}; };
export default withNamespaces()(Logs); export default withNamespaces()(Logs);

View File

@ -253,3 +253,10 @@ export const ENCRYPTION_SOURCE = {
PATH: 'path', PATH: 'path',
CONTENT: 'content', CONTENT: 'content',
}; };
export const FILTERED_STATUS = {
FILTERED_BLACK_LIST: 'FilteredBlackList',
NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList',
FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService',
REWRITE: 'Rewrite',
};

View File

@ -28,6 +28,7 @@ export const normalizeLogs = logs => logs.map((log) => {
filterId, filterId,
rule, rule,
service_name, service_name,
status,
} = log; } = log;
const { host: domain, type } = question; const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => { const responsesArray = response ? response.map((response) => {
@ -44,6 +45,7 @@ export const normalizeLogs = logs => logs.map((log) => {
filterId, filterId,
rule, rule,
serviceName: service_name, serviceName: service_name,
status,
}; };
}); });