2018-09-28 13:30:52 +00:00
|
|
|
import React, { Component, Fragment } from 'react';
|
2018-08-30 14:25:33 +00:00
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
import ReactTable from 'react-table';
|
|
|
|
import { saveAs } from 'file-saver/FileSaver';
|
2018-09-28 13:30:52 +00:00
|
|
|
import escapeRegExp from 'lodash/escapeRegExp';
|
|
|
|
import endsWith from 'lodash/endsWith';
|
2018-10-26 02:44:23 +00:00
|
|
|
import { Trans, withNamespaces } from 'react-i18next';
|
2019-07-22 12:32:12 +00:00
|
|
|
import { HashLink as Link } from 'react-router-hash-link';
|
2018-10-12 13:58:48 +00:00
|
|
|
|
2019-03-20 14:04:32 +00:00
|
|
|
import { formatTime, getClientName } from '../../helpers/helpers';
|
2019-08-30 13:03:36 +00:00
|
|
|
import { SERVICES, FILTERED_STATUS } from '../../helpers/constants';
|
2018-10-14 20:24:11 +00:00
|
|
|
import { getTrackerData } from '../../helpers/trackers/trackers';
|
2018-08-30 14:25:33 +00:00
|
|
|
import PageTitle from '../ui/PageTitle';
|
|
|
|
import Card from '../ui/Card';
|
|
|
|
import Loading from '../ui/Loading';
|
2018-10-30 14:27:47 +00:00
|
|
|
import PopoverFiltered from '../ui/PopoverFilter';
|
2018-10-12 16:52:19 +00:00
|
|
|
import Popover from '../ui/Popover';
|
2018-09-04 09:43:13 +00:00
|
|
|
import './Logs.css';
|
2018-08-30 14:25:33 +00:00
|
|
|
|
|
|
|
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
|
2019-08-30 13:03:36 +00:00
|
|
|
const FILTERED_REASON = 'Filtered';
|
|
|
|
const RESPONSE_FILTER = {
|
|
|
|
ALL: 'all',
|
|
|
|
FILTERED: 'filtered',
|
|
|
|
};
|
2018-08-30 14:25:33 +00:00
|
|
|
|
|
|
|
class Logs extends Component {
|
|
|
|
componentDidMount() {
|
2018-09-17 14:44:32 +00:00
|
|
|
this.getLogs();
|
2018-09-28 13:30:52 +00:00
|
|
|
this.props.getFilteringStatus();
|
2019-07-08 09:49:03 +00:00
|
|
|
this.props.getClients();
|
2018-08-30 14:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
// get logs when queryLog becomes enabled
|
|
|
|
if (this.props.dashboard.queryLogEnabled && !prevProps.dashboard.queryLogEnabled) {
|
|
|
|
this.props.getLogs();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-17 14:44:32 +00:00
|
|
|
getLogs = () => {
|
|
|
|
// get logs on initialization if queryLogIsEnabled
|
|
|
|
if (this.props.dashboard.queryLogEnabled) {
|
|
|
|
this.props.getLogs();
|
|
|
|
}
|
2019-08-30 13:03:36 +00:00
|
|
|
};
|
2018-09-17 14:44:32 +00:00
|
|
|
|
2019-07-18 11:52:47 +00:00
|
|
|
renderTooltip = (isFiltered, rule, filter, service) =>
|
|
|
|
isFiltered && <PopoverFiltered rule={rule} filter={filter} service={service} />;
|
2018-09-03 12:55:20 +00:00
|
|
|
|
2019-08-30 13:03:36 +00:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2018-09-28 13:30:52 +00:00
|
|
|
toggleBlocking = (type, domain) => {
|
|
|
|
const { userRules } = this.props.filtering;
|
2018-11-05 04:26:32 +00:00
|
|
|
const { t } = this.props;
|
2018-09-28 13:30:52 +00:00
|
|
|
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
|
2018-10-08 15:44:12 +00:00
|
|
|
const baseRule = `||${domain}^$important`;
|
2018-10-09 08:00:48 +00:00
|
|
|
const baseUnblocking = `@@${baseRule}`;
|
|
|
|
const blockingRule = type === 'block' ? baseUnblocking : baseRule;
|
|
|
|
const unblockingRule = type === 'block' ? baseRule : baseUnblocking;
|
2018-09-28 13:30:52 +00:00
|
|
|
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
|
|
|
|
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
|
|
|
|
|
|
|
|
if (userRules.match(preparedBlockingRule)) {
|
|
|
|
this.props.setRules(userRules.replace(`${blockingRule}`, ''));
|
2018-11-09 06:51:28 +00:00
|
|
|
this.props.addSuccessToast(`${t('rule_removed_from_custom_filtering_toast')}: ${blockingRule}`);
|
2018-09-28 13:30:52 +00:00
|
|
|
} else if (!userRules.match(preparedUnblockingRule)) {
|
|
|
|
this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
|
2018-11-09 06:51:28 +00:00
|
|
|
this.props.addSuccessToast(`${t('rule_added_to_custom_filtering_toast')}: ${unblockingRule}`);
|
2018-09-28 13:30:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.props.getFilteringStatus();
|
2019-08-30 13:03:36 +00:00
|
|
|
};
|
2018-09-28 13:30:52 +00:00
|
|
|
|
|
|
|
renderBlockingButton(isFiltered, domain) {
|
|
|
|
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
|
2018-11-12 01:32:49 +00:00
|
|
|
const buttonText = isFiltered ? 'unblock_btn' : 'block_btn';
|
2018-11-26 15:17:34 +00:00
|
|
|
const buttonType = isFiltered ? 'unblock' : 'block';
|
2018-09-28 13:30:52 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="logs__action">
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
className={`btn btn-sm ${buttonClass}`}
|
2018-11-26 15:17:34 +00:00
|
|
|
onClick={() => this.toggleBlocking(buttonType, domain)}
|
2019-01-16 11:51:17 +00:00
|
|
|
disabled={this.props.filtering.processingRules}
|
2018-09-28 13:30:52 +00:00
|
|
|
>
|
2018-10-26 02:44:23 +00:00
|
|
|
<Trans>{buttonText}</Trans>
|
2018-09-28 13:30:52 +00:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-08-30 13:03:36 +00:00
|
|
|
checkFiltered = reason => reason.indexOf(FILTERED_REASON) === 0;
|
2018-12-17 08:28:44 +00:00
|
|
|
|
2019-08-30 13:03:36 +00:00
|
|
|
checkRewrite = reason => reason === FILTERED_STATUS.REWRITE;
|
|
|
|
|
|
|
|
checkWhiteList = reason => reason === FILTERED_STATUS.NOT_FILTERED_WHITE_LIST;
|
|
|
|
|
|
|
|
getTimeCell = ({ value }) => (
|
|
|
|
<div className="logs__row">
|
|
|
|
<span className="logs__text" title={value}>
|
|
|
|
{formatTime(value)}
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
getDomainCell = (row) => {
|
|
|
|
const response = row.value;
|
|
|
|
const trackerData = getTrackerData(response);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="logs__row" title={response}>
|
|
|
|
<div className="logs__text">{response}</div>
|
|
|
|
{trackerData && <Popover data={trackerData} />}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
getResponseCell = ({ value: responses, original }) => {
|
|
|
|
const {
|
|
|
|
reason, filterId, rule, status,
|
|
|
|
} = original;
|
|
|
|
const { t, filtering } = this.props;
|
|
|
|
const { filters } = filtering;
|
|
|
|
|
|
|
|
const isFiltered = this.checkFiltered(reason);
|
|
|
|
const filterKey = reason.replace(FILTERED_REASON, '');
|
|
|
|
const parsedFilteredReason = t('query_log_filtered', { filter: filterKey });
|
|
|
|
const isRewrite = this.checkRewrite(reason);
|
|
|
|
const isWhiteList = this.checkWhiteList(reason);
|
|
|
|
const isBlockedService = reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE;
|
|
|
|
const currentService = SERVICES.find(service => service.id === original.serviceName);
|
|
|
|
const serviceName = currentService && currentService.name;
|
|
|
|
let filterName = '';
|
|
|
|
|
|
|
|
if (filterId === 0) {
|
|
|
|
filterName = t('custom_filter_rules');
|
|
|
|
} else {
|
|
|
|
const filterItem = Object.keys(filters).filter(key => filters[key].id === filterId)[0];
|
|
|
|
|
|
|
|
if (
|
|
|
|
typeof filterItem !== 'undefined' &&
|
|
|
|
typeof filters[filterItem] !== 'undefined'
|
|
|
|
) {
|
|
|
|
filterName = filters[filterItem].name;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!filterName) {
|
|
|
|
filterName = t('unknown_filter', { filterId });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="logs__row logs__row--column">
|
|
|
|
<div className="logs__text-wrap">
|
|
|
|
{(isFiltered || isBlockedService) && (
|
|
|
|
<span className="logs__text" title={parsedFilteredReason}>
|
|
|
|
{parsedFilteredReason}
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
{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>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
getClientCell = ({ original, value }) => {
|
|
|
|
const { dashboard } = this.props;
|
|
|
|
const { reason, domain } = original;
|
|
|
|
const isFiltered = this.checkFiltered(reason);
|
|
|
|
const isRewrite = this.checkRewrite(reason);
|
|
|
|
const clientName =
|
|
|
|
getClientName(dashboard.clients, value) || getClientName(dashboard.autoClients, value);
|
|
|
|
let client = value;
|
|
|
|
|
|
|
|
if (clientName) {
|
|
|
|
client = (
|
|
|
|
<span>
|
|
|
|
{clientName} <small>({value})</small>
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Fragment>
|
|
|
|
<div className="logs__row">{client}</div>
|
|
|
|
{isRewrite ? (
|
|
|
|
<div className="logs__action">
|
|
|
|
<Link to="/dns#rewrites" className="btn btn-sm btn-outline-primary">
|
|
|
|
<Trans>configure</Trans>
|
|
|
|
</Link>
|
2018-09-04 09:43:13 +00:00
|
|
|
</div>
|
2019-08-30 13:03:36 +00:00
|
|
|
) : (
|
|
|
|
this.renderBlockingButton(isFiltered, domain)
|
|
|
|
)}
|
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
renderLogs(logs) {
|
|
|
|
const { t } = this.props;
|
|
|
|
const columns = [
|
|
|
|
{
|
|
|
|
Header: t('time_table_header'),
|
|
|
|
accessor: 'time',
|
|
|
|
maxWidth: 90,
|
|
|
|
filterable: false,
|
|
|
|
Cell: this.getTimeCell,
|
2018-08-30 14:25:33 +00:00
|
|
|
},
|
2019-08-30 13:03:36 +00:00
|
|
|
{
|
|
|
|
Header: t('domain_name_table_header'),
|
|
|
|
accessor: 'domain',
|
|
|
|
minWidth: 180,
|
|
|
|
Cell: this.getDomainCell,
|
2018-10-02 15:14:41 +00:00
|
|
|
},
|
2019-08-30 13:03:36 +00:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
},
|
|
|
|
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,
|
2018-09-28 13:30:52 +00:00
|
|
|
},
|
2018-09-03 12:55:20 +00:00
|
|
|
];
|
2018-08-30 14:25:33 +00:00
|
|
|
|
|
|
|
if (logs) {
|
2019-08-30 13:03:36 +00:00
|
|
|
return (
|
|
|
|
<ReactTable
|
|
|
|
className="logs__table"
|
|
|
|
filterable
|
|
|
|
data={logs}
|
|
|
|
columns={columns}
|
|
|
|
showPagination={true}
|
|
|
|
defaultPageSize={50}
|
|
|
|
minRows={7}
|
|
|
|
previousText={t('previous_btn')}
|
|
|
|
nextText={t('next_btn')}
|
|
|
|
loadingText={t('loading_table_status')}
|
|
|
|
pageText={t('page_table_footer_text')}
|
|
|
|
ofText={t('of_table_footer_text')}
|
|
|
|
rowsText={t('rows_table_footer_text')}
|
|
|
|
noDataText={t('no_logs_found')}
|
|
|
|
defaultFilterMethod={(filter, row) => {
|
|
|
|
const id = filter.pivotId || filter.id;
|
|
|
|
return row[id] !== undefined
|
|
|
|
? String(row[id]).indexOf(filter.value) !== -1
|
|
|
|
: true;
|
|
|
|
}}
|
|
|
|
defaultSorted={[
|
|
|
|
{
|
|
|
|
id: 'time',
|
|
|
|
desc: true,
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
getTrProps={(_state, rowInfo) => {
|
|
|
|
if (!rowInfo) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const { reason } = rowInfo.original;
|
|
|
|
|
|
|
|
if (this.checkFiltered(reason)) {
|
|
|
|
return {
|
|
|
|
className: 'red',
|
|
|
|
};
|
|
|
|
} else if (this.checkWhiteList(reason)) {
|
|
|
|
return {
|
|
|
|
className: 'green',
|
|
|
|
};
|
|
|
|
} else if (this.checkRewrite(reason)) {
|
|
|
|
return {
|
|
|
|
className: 'blue',
|
|
|
|
};
|
|
|
|
}
|
2018-10-30 15:10:05 +00:00
|
|
|
|
2019-07-22 12:32:12 +00:00
|
|
|
return {
|
2019-08-30 13:03:36 +00:00
|
|
|
className: '',
|
2019-07-22 12:32:12 +00:00
|
|
|
};
|
2019-08-30 13:03:36 +00:00
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
2018-08-30 14:25:33 +00:00
|
|
|
}
|
2019-08-30 13:03:36 +00:00
|
|
|
|
|
|
|
return null;
|
2018-08-30 14:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleDownloadButton = async (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
const data = await this.props.downloadQueryLog();
|
|
|
|
const jsonStr = JSON.stringify(data);
|
|
|
|
const dataBlob = new Blob([jsonStr], { type: 'text/plain;charset=utf-8' });
|
|
|
|
saveAs(dataBlob, DOWNLOAD_LOG_FILENAME);
|
|
|
|
};
|
|
|
|
|
2019-01-16 11:51:17 +00:00
|
|
|
renderButtons(queryLogEnabled, logStatusProcessing) {
|
2018-10-02 15:14:41 +00:00
|
|
|
if (queryLogEnabled) {
|
|
|
|
return (
|
|
|
|
<Fragment>
|
2018-09-17 14:44:32 +00:00
|
|
|
<button
|
2018-10-02 15:14:41 +00:00
|
|
|
className="btn btn-gray btn-sm mr-2"
|
|
|
|
type="submit"
|
|
|
|
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
2019-01-16 11:51:17 +00:00
|
|
|
disabled={logStatusProcessing}
|
2019-08-30 13:03:36 +00:00
|
|
|
>
|
|
|
|
<Trans>disabled_log_btn</Trans>
|
|
|
|
</button>
|
2018-10-02 15:14:41 +00:00
|
|
|
<button
|
|
|
|
className="btn btn-primary btn-sm mr-2"
|
2018-09-17 14:44:32 +00:00
|
|
|
type="submit"
|
|
|
|
onClick={this.handleDownloadButton}
|
2019-08-30 13:03:36 +00:00
|
|
|
>
|
|
|
|
<Trans>download_log_file_btn</Trans>
|
|
|
|
</button>
|
2018-09-17 14:44:32 +00:00
|
|
|
<button
|
2018-10-02 15:14:41 +00:00
|
|
|
className="btn btn-outline-primary btn-sm"
|
2018-09-17 14:44:32 +00:00
|
|
|
type="submit"
|
|
|
|
onClick={this.getLogs}
|
2019-08-30 13:03:36 +00:00
|
|
|
>
|
|
|
|
<Trans>refresh_btn</Trans>
|
|
|
|
</button>
|
2018-10-02 15:14:41 +00:00
|
|
|
</Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
className="btn btn-success btn-sm mr-2"
|
|
|
|
type="submit"
|
|
|
|
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
|
2019-01-16 11:51:17 +00:00
|
|
|
disabled={logStatusProcessing}
|
2019-08-30 13:03:36 +00:00
|
|
|
>
|
|
|
|
<Trans>enabled_log_btn</Trans>
|
|
|
|
</button>
|
2018-10-02 15:14:41 +00:00
|
|
|
);
|
2018-08-30 14:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
2018-10-26 02:44:23 +00:00
|
|
|
const { queryLogs, dashboard, t } = this.props;
|
2018-08-30 14:25:33 +00:00
|
|
|
const { queryLogEnabled } = dashboard;
|
|
|
|
return (
|
2018-10-02 15:14:41 +00:00
|
|
|
<Fragment>
|
2019-08-30 13:03:36 +00:00
|
|
|
<PageTitle title={t('query_log')} subtitle={t('last_dns_queries')}>
|
2018-10-02 15:14:41 +00:00
|
|
|
<div className="page-title__actions">
|
2019-01-16 11:51:17 +00:00
|
|
|
{this.renderButtons(queryLogEnabled, dashboard.logStatusProcessing)}
|
2018-10-02 15:14:41 +00:00
|
|
|
</div>
|
|
|
|
</PageTitle>
|
2018-08-30 14:25:33 +00:00
|
|
|
<Card>
|
2019-08-30 13:03:36 +00:00
|
|
|
{queryLogEnabled &&
|
|
|
|
queryLogs.getLogsProcessing &&
|
|
|
|
dashboard.processingClients && <Loading />}
|
|
|
|
{queryLogEnabled &&
|
|
|
|
!queryLogs.getLogsProcessing &&
|
|
|
|
!dashboard.processingClients &&
|
|
|
|
this.renderLogs(queryLogs.logs)}
|
2018-08-30 14:25:33 +00:00
|
|
|
</Card>
|
2018-10-02 15:14:41 +00:00
|
|
|
</Fragment>
|
2018-08-30 14:25:33 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Logs.propTypes = {
|
2019-08-30 13:03:36 +00:00
|
|
|
getLogs: PropTypes.func.isRequired,
|
|
|
|
queryLogs: PropTypes.object.isRequired,
|
|
|
|
dashboard: PropTypes.object.isRequired,
|
|
|
|
toggleLogStatus: PropTypes.func.isRequired,
|
|
|
|
downloadQueryLog: PropTypes.func.isRequired,
|
|
|
|
getFilteringStatus: PropTypes.func.isRequired,
|
|
|
|
filtering: PropTypes.object.isRequired,
|
|
|
|
setRules: PropTypes.func.isRequired,
|
|
|
|
addSuccessToast: PropTypes.func.isRequired,
|
2019-07-08 09:49:03 +00:00
|
|
|
getClients: PropTypes.func.isRequired,
|
2019-08-30 13:03:36 +00:00
|
|
|
t: PropTypes.func.isRequired,
|
2018-08-30 14:25:33 +00:00
|
|
|
};
|
|
|
|
|
2018-10-26 02:44:23 +00:00
|
|
|
export default withNamespaces()(Logs);
|