Commit f0b119c0 authored by Miguel Rincon's avatar Miguel Rincon

Add time window filter to logs

- Add new dropdown for logs screen
- Add new param start & end to requests to backend
- Use logs specific constants for time ranges
- Update translation pot file
parent f347c4bd
---
title: Add time filters to environments log view
merge_request: 22638
author:
type: added
...@@ -88,13 +88,15 @@ export default { ...@@ -88,13 +88,15 @@ export default {
* Returns pods logs for an environment with an optional pod and container * Returns pods logs for an environment with an optional pod and container
* *
* @param {Object} params * @param {Object} params
* @param {string} param.projectFullPath - Path of the project, in format `/<namespace>/<project-key>` * @param {string} params.projectFullPath - Path of the project, in format `/<namespace>/<project-key>`
* @param {number} param.environmentId - Id of the environment * @param {number} params.environmentId - Id of the environment
* @param {string=} params.podName - Pod name, if not set the backend assumes a default one * @param {string=} params.podName - Pod name, if not set the backend assumes a default one
* @param {string=} params.containerName - Container name, if not set the backend assumes a default one * @param {string=} params.containerName - Container name, if not set the backend assumes a default one
* @param {string=} params.start - Starting date to query the logs in ISO format
* @param {string=} params.end - Ending date to query the logs in ISO format
* @returns {Promise} Axios promise for the result of a GET request of logs * @returns {Promise} Axios promise for the result of a GET request of logs
*/ */
getPodLogs({ projectPath, environmentName, podName, containerName, search }) { getPodLogs({ projectPath, environmentName, podName, containerName, search, start, end }) {
const url = this.buildUrl(this.podLogsPath).replace(':project_full_path', projectPath); const url = this.buildUrl(this.podLogsPath).replace(':project_full_path', projectPath);
const params = { const params = {
...@@ -110,6 +112,12 @@ export default { ...@@ -110,6 +112,12 @@ export default {
if (search) { if (search) {
params.search = search; params.search = search;
} }
if (start) {
params.start = start;
}
if (end) {
params.end = end;
}
return axios.get(url, { params }); return axios.get(url, { params });
}, },
......
...@@ -45,7 +45,13 @@ export default { ...@@ -45,7 +45,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapState('environmentLogs', ['environments', 'logs', 'pods', 'enableAdvancedQuerying']), ...mapState('environmentLogs', [
'environments',
'timeWindow',
'logs',
'pods',
'enableAdvancedQuerying',
]),
...mapGetters('environmentLogs', ['trace']), ...mapGetters('environmentLogs', ['trace']),
showLoader() { showLoader() {
return this.logs.isLoading || !this.logs.isComplete; return this.logs.isLoading || !this.logs.isComplete;
...@@ -87,6 +93,7 @@ export default { ...@@ -87,6 +93,7 @@ export default {
...mapActions('environmentLogs', [ ...mapActions('environmentLogs', [
'setInitData', 'setInitData',
'setSearch', 'setSearch',
'setTimeWindow',
'showPodLogs', 'showPodLogs',
'showEnvironment', 'showEnvironment',
'fetchEnvironments', 'fetchEnvironments',
...@@ -119,7 +126,7 @@ export default { ...@@ -119,7 +126,7 @@ export default {
:label="s__('Environments|Environment')" :label="s__('Environments|Environment')"
label-size="sm" label-size="sm"
label-for="environments-dropdown" label-for="environments-dropdown"
:class="featureElasticEnabled ? 'col-4' : 'col-6'" :class="featureElasticEnabled ? 'col-3' : 'col-6'"
> >
<gl-dropdown <gl-dropdown
id="environments-dropdown" id="environments-dropdown"
...@@ -142,7 +149,7 @@ export default { ...@@ -142,7 +149,7 @@ export default {
:label="s__('Environments|Pod logs from')" :label="s__('Environments|Pod logs from')"
label-size="sm" label-size="sm"
label-for="pods-dropdown" label-for="pods-dropdown"
:class="featureElasticEnabled ? 'col-4' : 'col-6'" :class="featureElasticEnabled ? 'col-3' : 'col-6'"
> >
<gl-dropdown <gl-dropdown
id="pods-dropdown" id="pods-dropdown"
...@@ -166,7 +173,7 @@ export default { ...@@ -166,7 +173,7 @@ export default {
:label="s__('Environments|Search')" :label="s__('Environments|Search')"
label-size="sm" label-size="sm"
label-for="search" label-for="search"
class="col-4" class="col-3"
> >
<gl-search-box-by-click <gl-search-box-by-click
v-model.trim="searchQuery" v-model.trim="searchQuery"
...@@ -178,6 +185,29 @@ export default { ...@@ -178,6 +185,29 @@ export default {
@submit="setSearch(searchQuery)" @submit="setSearch(searchQuery)"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group
v-if="featureElasticEnabled"
id="dates-fg"
:label="s__('Environments|Show last')"
label-size="sm"
label-for="time-window-dropdown"
class="col-3"
>
<gl-dropdown
id="time-window-dropdown"
:text="timeWindow.options[timeWindow.current].label"
class="d-flex"
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-item
v-for="(option, key) in timeWindow.options"
:key="key"
@click="setTimeWindow(key)"
>
{{ option.label }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</div> </div>
<log-control-buttons <log-control-buttons
......
import { __ } from '~/locale';
export const defaultTimeWindow = 'thirtyMinutes';
export const timeWindows = {
thirtyMinutes: {
label: __('1 hour'),
seconds: 60 * 60,
},
threeHours: {
label: __('4 hours'),
seconds: 60 * 60 * 4,
},
oneDay: {
label: __('1 day'),
seconds: 60 * 60 * 24,
},
twoDays: {
label: __('2 days'),
seconds: 60 * 60 * 24 * 3,
},
pastWeek: {
label: __('Past week'),
seconds: 60 * 60 * 24 * 7,
},
pastMonth: {
label: __('Past month'),
seconds: 60 * 60 * 24 * 30,
},
};
...@@ -6,6 +6,8 @@ import flash from '~/flash'; ...@@ -6,6 +6,8 @@ import flash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { getTimeRange } from '../utils';
const requestLogsUntilData = params => const requestLogsUntilData = params =>
backOff((next, stop) => { backOff((next, stop) => {
Api.getPodLogs(params) Api.getPodLogs(params)
...@@ -38,6 +40,11 @@ export const setSearch = ({ dispatch, commit }, searchQuery) => { ...@@ -38,6 +40,11 @@ export const setSearch = ({ dispatch, commit }, searchQuery) => {
dispatch('fetchLogs'); dispatch('fetchLogs');
}; };
export const setTimeWindow = ({ dispatch, commit }, timeWindow) => {
commit(types.SET_TIME_WINDOW, timeWindow);
dispatch('fetchLogs');
};
export const showEnvironment = ({ dispatch, commit }, environmentName) => { export const showEnvironment = ({ dispatch, commit }, environmentName) => {
commit(types.SET_PROJECT_ENVIRONMENT, environmentName); commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, null); commit(types.SET_CURRENT_POD_NAME, null);
...@@ -66,6 +73,12 @@ export const fetchLogs = ({ commit, state }) => { ...@@ -66,6 +73,12 @@ export const fetchLogs = ({ commit, state }) => {
search: state.search, search: state.search,
}; };
if (state.timeWindow.current) {
const { start, end } = getTimeRange(state.timeWindow.current.seconds);
params.start = start;
params.end = end;
}
commit(types.REQUEST_PODS_DATA); commit(types.REQUEST_PODS_DATA);
commit(types.REQUEST_LOGS_DATA); commit(types.REQUEST_LOGS_DATA);
......
...@@ -2,6 +2,7 @@ export const SET_PROJECT_PATH = 'SET_PROJECT_PATH'; ...@@ -2,6 +2,7 @@ export const SET_PROJECT_PATH = 'SET_PROJECT_PATH';
export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT'; export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT';
export const SET_SEARCH = 'SET_SEARCH'; export const SET_SEARCH = 'SET_SEARCH';
export const ENABLE_ADVANCED_QUERYING = 'ENABLE_ADVANCED_QUERYING'; export const ENABLE_ADVANCED_QUERYING = 'ENABLE_ADVANCED_QUERYING';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
......
...@@ -15,6 +15,10 @@ export default { ...@@ -15,6 +15,10 @@ export default {
[types.ENABLE_ADVANCED_QUERYING](state, enableAdvancedQuerying) { [types.ENABLE_ADVANCED_QUERYING](state, enableAdvancedQuerying) {
state.enableAdvancedQuerying = enableAdvancedQuerying; state.enableAdvancedQuerying = enableAdvancedQuerying;
}, },
/** Time Range data */
[types.SET_TIME_WINDOW](state, timeWindow) {
state.timeWindow.current = timeWindow;
},
/** Environments data */ /** Environments data */
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) { [types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
......
import { defaultTimeWindow, timeWindows } from '../constants';
export default () => ({ export default () => ({
/** /**
* Current project path * Current project path
...@@ -14,6 +16,14 @@ export default () => ({ ...@@ -14,6 +16,14 @@ export default () => ({
*/ */
enableAdvancedQuerying: false, enableAdvancedQuerying: false,
/**
* Time range (Show last)
*/
timeWindow: {
options: { ...timeWindows },
current: defaultTimeWindow,
},
/** /**
* Environments list information * Environments list information
*/ */
......
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
/**
* Returns a time range (`start`, `end`) where `start` is the
* current time minus a given number of seconds and `end`
* is the current time (`now()`).
*
* @param {Number} seconds Seconds duration, defaults to 0.
* @returns {Object} range Time range
* @returns {String} range.start ISO String of current time minus given seconds
* @returns {String} range.end ISO String of current time
*/
export const getTimeRange = (seconds = 0) => {
const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds
const start = end - seconds;
return {
start: new Date(secondsToMilliseconds(start)).toISOString(),
end: new Date(secondsToMilliseconds(end)).toISOString(),
};
};
export default {};
...@@ -238,7 +238,7 @@ describe('EnvironmentLogs', () => { ...@@ -238,7 +238,7 @@ describe('EnvironmentLogs', () => {
const item = items.at(i); const item = items.at(i);
expect(item.text()).toBe(env.name); expect(item.text()).toBe(env.name);
}); });
expect(wrapper.find('#environments-dropdown-fg').attributes('class')).toEqual('col-4'); expect(wrapper.find('#environments-dropdown-fg').attributes('class')).toEqual('col-3');
}); });
it('populates pods dropdown', () => { it('populates pods dropdown', () => {
...@@ -250,7 +250,7 @@ describe('EnvironmentLogs', () => { ...@@ -250,7 +250,7 @@ describe('EnvironmentLogs', () => {
const item = items.at(i); const item = items.at(i);
expect(item.text()).toBe(pod); expect(item.text()).toBe(pod);
}); });
expect(wrapper.find('#pods-dropdown-fg').attributes('class')).toEqual('col-4'); expect(wrapper.find('#pods-dropdown-fg').attributes('class')).toEqual('col-3');
}); });
it('populates logs trace', () => { it('populates logs trace', () => {
...@@ -262,7 +262,7 @@ describe('EnvironmentLogs', () => { ...@@ -262,7 +262,7 @@ describe('EnvironmentLogs', () => {
it('displays an enabled search bar', () => { it('displays an enabled search bar', () => {
expect(findSearchBar().exists()).toEqual(true); expect(findSearchBar().exists()).toEqual(true);
expect(findSearchBar().attributes('disabled')).toEqual(undefined); expect(findSearchBar().attributes('disabled')).toEqual(undefined);
expect(wrapper.find('#search-fg').attributes('class')).toEqual('col-4'); expect(wrapper.find('#search-fg').attributes('class')).toEqual('col-3');
}); });
it('update control buttons state', () => { it('update control buttons state', () => {
......
...@@ -10,8 +10,9 @@ import { ...@@ -10,8 +10,9 @@ import {
fetchEnvironments, fetchEnvironments,
fetchLogs, fetchLogs,
} from 'ee/logs/stores/actions'; } from 'ee/logs/stores/actions';
import axios from '~/lib/utils/axios_utils'; import { getTimeRange } from 'ee/logs/utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { import {
...@@ -27,13 +28,20 @@ import { ...@@ -27,13 +28,20 @@ import {
} from '../mock_data'; } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('ee/logs/utils');
describe('Logs Store actions', () => { describe('Logs Store actions', () => {
let state; let state;
let mock; let mock;
const mockThirtyMinutes = {
start: '2020-01-09T18:06:20.000Z',
end: '2020-01-09T18:36:20.000Z',
};
beforeEach(() => { beforeEach(() => {
state = logsPageState(); state = logsPageState();
getTimeRange.mockReturnValue(mockThirtyMinutes);
}); });
afterEach(() => { afterEach(() => {
...@@ -139,7 +147,9 @@ describe('Logs Store actions', () => { ...@@ -139,7 +147,9 @@ describe('Logs Store actions', () => {
const endpoint = `/${mockProjectPath}/-/logs/k8s.json`; const endpoint = `/${mockProjectPath}/-/logs/k8s.json`;
mock mock
.onGet(endpoint, { params: { environment_name: mockEnvName, pod_name: mockPodName } }) .onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, ...mockThirtyMinutes },
})
.reply(200, { .reply(200, {
pod_name: mockPodName, pod_name: mockPodName,
pods: mockPods, pods: mockPods,
...@@ -166,6 +176,42 @@ describe('Logs Store actions', () => { ...@@ -166,6 +176,42 @@ describe('Logs Store actions', () => {
); );
}); });
it('should commit logs and pod data when there is pod name defined and a non-default date range', done => {
const mockOneDay = { start: '2020-01-08T18:41:39.000Z', end: '2020-01-09T18:41:39.000Z' };
getTimeRange.mockReturnValueOnce(mockOneDay);
state.projectPath = mockProjectPath;
state.environments.current = mockEnvName;
state.pods.current = mockPodName;
const endpoint = `/${mockProjectPath}/-/logs/k8s.json`;
mock
.onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, ...mockOneDay },
})
.reply(200, {
pod_name: mockPodName,
pods: mockPods,
logs: mockLogsResult,
});
testAction(
fetchLogs,
null,
state,
[
{ type: types.REQUEST_PODS_DATA },
{ type: types.REQUEST_LOGS_DATA },
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods },
{ type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult },
],
[],
done,
);
});
it('should commit logs and pod data when there is pod name and search', done => { it('should commit logs and pod data when there is pod name and search', done => {
state.projectPath = mockProjectPath; state.projectPath = mockProjectPath;
state.environments.current = mockEnvName; state.environments.current = mockEnvName;
...@@ -177,7 +223,12 @@ describe('Logs Store actions', () => { ...@@ -177,7 +223,12 @@ describe('Logs Store actions', () => {
mock mock
.onGet(endpoint, { .onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, search: mockSearch }, params: {
environment_name: mockEnvName,
pod_name: mockPodName,
search: mockSearch,
...mockThirtyMinutes,
},
}) })
.reply(200, { .reply(200, {
pod_name: mockPodName, pod_name: mockPodName,
...@@ -211,12 +262,14 @@ describe('Logs Store actions', () => { ...@@ -211,12 +262,14 @@ describe('Logs Store actions', () => {
const endpoint = `/${mockProjectPath}/-/logs/k8s.json`; const endpoint = `/${mockProjectPath}/-/logs/k8s.json`;
mock.onGet(endpoint, { params: { environment_name: mockEnvName } }).reply(200, { mock
pod_name: mockPodName, .onGet(endpoint, { params: { environment_name: mockEnvName, ...mockThirtyMinutes } })
pods: mockPods, .reply(200, {
logs: mockLogsResult, pod_name: mockPodName,
enable_advanced_querying: mockEnableAdvancedQuerying, pods: mockPods,
}); logs: mockLogsResult,
enable_advanced_querying: mockEnableAdvancedQuerying,
});
mock.onGet(endpoint).replyOnce(202); // mock reactive cache mock.onGet(endpoint).replyOnce(202); // mock reactive cache
testAction( testAction(
......
import { getTimeRange } from 'ee/logs/utils';
describe('logs/utils', () => {
describe('getTimeRange', () => {
const nowTimestamp = 1577836800000;
const nowString = '2020-01-01T00:00:00.000Z';
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => nowTimestamp);
});
afterEach(() => {
Date.now.mockRestore();
});
it('returns the right values', () => {
expect(getTimeRange(0)).toEqual({
start: '2020-01-01T00:00:00.000Z',
end: nowString,
});
expect(getTimeRange(60 * 30)).toEqual({
start: '2019-12-31T23:30:00.000Z',
end: nowString,
});
expect(getTimeRange(60 * 60 * 24 * 7 * 1)).toEqual({
start: '2019-12-25T00:00:00.000Z',
end: nowString,
});
expect(getTimeRange(60 * 60 * 24 * 7 * 4)).toEqual({
start: '2019-12-04T00:00:00.000Z',
end: nowString,
});
});
});
});
...@@ -638,6 +638,9 @@ msgstr "" ...@@ -638,6 +638,9 @@ msgstr ""
msgid "1st contribution!" msgid "1st contribution!"
msgstr "" msgstr ""
msgid "2 days"
msgstr ""
msgid "20-29 contributions" msgid "20-29 contributions"
msgstr "" msgstr ""
...@@ -665,6 +668,9 @@ msgstr "" ...@@ -665,6 +668,9 @@ msgstr ""
msgid "30+ contributions" msgid "30+ contributions"
msgstr "" msgstr ""
msgid "4 hours"
msgstr ""
msgid "403|Please contact your GitLab administrator to get permission." msgid "403|Please contact your GitLab administrator to get permission."
msgstr "" msgstr ""
...@@ -7183,6 +7189,9 @@ msgstr "" ...@@ -7183,6 +7189,9 @@ msgstr ""
msgid "Environments|Show all" msgid "Environments|Show all"
msgstr "" msgstr ""
msgid "Environments|Show last"
msgstr ""
msgid "Environments|Stop" msgid "Environments|Stop"
msgstr "" msgstr ""
...@@ -13082,6 +13091,12 @@ msgstr "" ...@@ -13082,6 +13091,12 @@ msgstr ""
msgid "Past due" msgid "Past due"
msgstr "" msgstr ""
msgid "Past month"
msgstr ""
msgid "Past week"
msgstr ""
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}" msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment