Display tabular log data and Calculate bin size for log viewer

pull/3538/head
Brandon Farmer 2018-05-29 13:20:54 -07:00
parent 4b0a31dfaa
commit 97acfc07bf
10 changed files with 417 additions and 60 deletions

View File

@ -63,6 +63,8 @@ export const saveToLocalStorage = ({
const appPersisted = {app: {persisted}}
const dashTimeV1 = {ranges: normalizer(ranges)}
const minimalLogs = _.omit(logs, ['tableData', 'histogramData'])
window.localStorage.setItem(
'state',
JSON.stringify({
@ -73,7 +75,7 @@ export const saveToLocalStorage = ({
dataExplorer,
dataExplorerQueryConfigs,
script,
logs,
logs: {...minimalLogs, histogramData: [], tableData: {}},
})
)
} catch (err) {

View File

@ -1,13 +1,36 @@
import _ from 'lodash'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {buildHistogramQueryConfig} from 'src/logs/utils'
import {buildHistogramQueryConfig, buildTableQueryConfig} from 'src/logs/utils'
import {getDeep} from 'src/utils/wrappers'
import buildQuery from 'src/utils/influxql'
import {executeQueryAsync} from 'src/logs/api'
import {LogsState} from 'src/types/localStorage'
type GetState = () => {logs: LogsState}
interface TableData {
columns: string[]
values: string[]
}
const defaultTableData = {
columns: [
'time',
'message',
'facility_code',
'procid',
'severity_code',
'timestamp',
'version',
],
values: [],
}
interface State {
logs: LogsState
}
type GetState = () => State
export enum ActionTypes {
SetSource = 'LOGS_SET_SOURCE',
@ -16,6 +39,8 @@ export enum ActionTypes {
SetNamespace = 'LOGS_SET_NAMESPACE',
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
SetTableData = 'LOGS_SET_TABLE_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM',
}
@ -61,6 +86,20 @@ interface SetHistogramData {
}
}
interface SetTableQueryConfig {
type: ActionTypes.SetTableQueryConfig
payload: {
queryConfig: QueryConfig
}
}
interface SetTableData {
type: ActionTypes.SetTableData
payload: {
data: object
}
}
interface ChangeZoomAction {
type: ActionTypes.ChangeZoom
payload: {
@ -77,12 +116,32 @@ export type Action =
| SetHistogramQueryConfig
| SetHistogramData
| ChangeZoomAction
| SetTableData
| SetTableQueryConfig
const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null)
const getNamespace = (state: State): Namespace | null =>
getDeep<Namespace | null>(state, 'logs.currentNamespace', null)
const getProxyLink = (state: State): string | null =>
getDeep<string | null>(state, 'logs.currentSource.links.proxy', null)
const getHistogramQueryConfig = (state: State): QueryConfig | null =>
getDeep<QueryConfig | null>(state, 'logs.histogramQueryConfig', null)
const getTableQueryConfig = (state: State): QueryConfig | null =>
getDeep<QueryConfig | null>(state, 'logs.tableQueryConfig', null)
export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource,
payload: {
source,
},
payload: {source},
})
const setHistogramData = (response): SetHistogramData => ({
type: ActionTypes.SetHistogramData,
payload: {data: [{response}]},
})
export const executeHistogramQueryAsync = () => async (
@ -90,36 +149,51 @@ export const executeHistogramQueryAsync = () => async (
getState: GetState
): Promise<void> => {
const state = getState()
const queryConfig = getDeep<QueryConfig | null>(
state,
'logs.histogramQueryConfig',
null
)
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const proxyLink = getDeep<string | null>(
state,
'logs.currentSource.links.proxy',
null
)
if (queryConfig && timeRange && namespace && proxyLink) {
const queryConfig = getHistogramQueryConfig(state)
const timeRange = getTimeRange(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildQuery(timeRange, queryConfig)
const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch({
type: ActionTypes.SetHistogramData,
payload: {
data: [{response}],
},
})
dispatch(setHistogramData(response))
}
}
const setTableData = (series: TableData): SetTableData => ({
type: ActionTypes.SetTableData,
payload: {data: {columns: series.columns, values: _.reverse(series.values)}},
})
export const executeTableQueryAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const queryConfig = getTableQueryConfig(state)
const timeRange = getTimeRange(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildQuery(timeRange, queryConfig)
const response = await executeQueryAsync(proxyLink, namespace, query)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
dispatch(setTableData(series))
}
}
export const executeQueriesAsync = () => async dispatch => {
dispatch(executeHistogramQueryAsync())
dispatch(executeTableQueryAsync())
}
export const setHistogramQueryConfigAsync = () => async (
dispatch,
getState: GetState
@ -144,6 +218,31 @@ export const setHistogramQueryConfigAsync = () => async (
}
}
export const setTableQueryConfig = (queryConfig: QueryConfig) => ({
type: ActionTypes.SetTableQueryConfig,
payload: {queryConfig},
})
export const setTableQueryConfigAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
if (timeRange && namespace) {
const queryConfig = buildTableQueryConfig(namespace, timeRange)
dispatch(setTableQueryConfig(queryConfig))
dispatch(executeTableQueryAsync())
}
}
export const setNamespaceAsync = (namespace: Namespace) => async (
dispatch
): Promise<void> => {
@ -153,6 +252,7 @@ export const setNamespaceAsync = (namespace: Namespace) => async (
})
dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync())
}
export const setNamespaces = (
@ -174,6 +274,7 @@ export const setTimeRangeAsync = (timeRange: TimeRange) => async (
},
})
dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync())
}
export const populateNamespacesAsync = (proxyLink: string) => async (
@ -206,16 +307,9 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const proxyLink = getDeep<string | null>(
state,
'logs.currentSource.links.proxy',
null
)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
if (namespace && proxyLink) {
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
@ -229,5 +323,8 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
timeRange,
},
})
await dispatch(setTimeRangeAsync(timeRange))
await dispatch(executeTableQueryAsync())
}
}

View File

@ -1,17 +1,178 @@
import moment from 'moment'
import React, {PureComponent} from 'react'
import {Grid, AutoSizer} from 'react-virtualized'
import {getDeep} from 'src/utils/wrappers'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
interface Props {
thing: string
data: {
columns: string[]
values: string[]
}
}
class LogsTableContainer extends PureComponent<Props> {
const FACILITY_CODES = [
'kern',
'user',
'mail',
'daemon',
'auth',
'syslog',
'lpr',
'news',
'uucp',
'clock',
'authpriv',
'ftp',
'NTP',
'log audit',
'log alert',
'cron',
'local0',
'local1',
'local2',
'local3',
'local4',
'local5',
'local6',
'local7',
]
class LogsTable extends PureComponent<Props> {
public render() {
const rowCount = getDeep(this.props, 'data.values.length', 0)
const columnCount = getDeep(this.props, 'data.columns.length', 1) - 1
return (
<div className="logs-viewer--table-container">
<p>{this.props.thing}</p>
<AutoSizer>
{({width}) => (
<Grid
height={40}
rowHeight={40}
rowCount={1}
width={width}
cellRenderer={this.headerRenderer}
columnCount={columnCount}
columnWidth={this.getColumnWidth}
/>
)}
</AutoSizer>
<AutoSizer>
{({width, height}) => (
<FancyScrollbar
style={{width, height, marginTop: '40px'}}
autoHide={false}
>
<Grid
height={height}
rowHeight={40}
rowCount={rowCount}
width={width}
cellRenderer={this.cellRenderer}
columnCount={columnCount}
columnWidth={this.getColumnWidth}
/>
</FancyScrollbar>
)}
</AutoSizer>
</div>
)
}
private severityLevel(value: number): string {
switch (value) {
case 0:
return 'Emergency'
case 1:
return 'Alert'
case 2:
return 'Critical'
case 3:
return 'Error'
case 4:
return 'Warning'
case 5:
return 'Notice'
case 6:
return 'Informational'
default:
return 'Debug'
}
}
private getColumnWidth = ({index}: {index: number}) => {
const column = getDeep<string>(this.props, `data.columns.${index + 1}`, '')
switch (column) {
case 'message':
return 700
case 'timestamp':
return 400
default:
return 200
}
}
private header(key: string): string {
return getDeep<string>(
{
timestamp: 'Timestamp',
facility_code: 'Facility',
procid: 'Proc ID',
severity_code: 'Severity',
message: 'Message',
},
key,
''
)
}
private facility(key: number): string {
return getDeep<string>(FACILITY_CODES, key, '')
}
private headerRenderer = ({key, style, columnIndex}) => {
const value = getDeep<string>(
this.props,
`data.columns.${columnIndex + 1}`,
''
)
return (
<div style={style} key={key}>
{this.header(value)}
</div>
)
}
private cellRenderer = ({key, style, rowIndex, columnIndex}) => {
const column = getDeep<string>(
this.props,
`data.columns.${columnIndex + 1}`,
''
)
let value = this.props.data.values[rowIndex][columnIndex + 1]
switch (column) {
case 'timestamp':
value = moment(+value / 1000000).format('YYYY/MM/DD HH:mm:ss')
break
case 'severity_code':
value = this.severityLevel(+value)
break
case 'facility_code':
value = this.facility(+value)
break
}
return (
<div style={style} key={key}>
{value}
</div>
)
}
}
export default LogsTableContainer
export default LogsTable

View File

@ -130,10 +130,9 @@ class TimeRangeDropdown extends Component<Props, State> {
}
private get dropdownClassName(): string {
const {
isOpen,
customTimeRange: {lower, upper},
} = this.state
const {isOpen} = this.state
const {lower, upper} = _.get(this.props, 'selected', {upper: '', lower: ''})
const absoluteTimeRange = !_.isEmpty(lower) && !_.isEmpty(upper)

View File

@ -4,16 +4,17 @@ import {
getSourceAndPopulateNamespacesAsync,
setTimeRangeAsync,
setNamespaceAsync,
executeHistogramQueryAsync,
executeQueriesAsync,
changeZoomAsync,
} from 'src/logs/actions'
import {getSourcesAsync} from 'src/shared/actions/sources'
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
import Graph from 'src/logs/components/LogsGraph'
import Table from 'src/logs/components/LogsTable'
import SearchBar from 'src/logs/components/LogsSearchBar'
import FilterBar from 'src/logs/components/LogsFilterBar'
import LogViewerChart from 'src/logs/components/LogViewerChart'
import LogsTable from 'src/logs/components/LogsTable'
import {getDeep} from 'src/utils/wrappers'
import {Source, Namespace, TimeRange} from 'src/types'
@ -35,9 +36,13 @@ interface Props {
setTimeRangeAsync: (timeRange: TimeRange) => void
setNamespaceAsync: (namespace: Namespace) => void
changeZoomAsync: (timeRange: TimeRange) => void
executeHistogramQueryAsync: () => void
executeQueriesAsync: () => void
timeRange: TimeRange
histogramData: object[]
tableData: {
columns: string[]
values: string[]
}
}
interface State {
@ -73,11 +78,17 @@ class LogsPage extends PureComponent<Props, State> {
public componentDidMount() {
this.props.getSources()
if (this.props.currentNamespace) {
this.props.executeQueriesAsync()
}
}
public render() {
const {searchString, filters} = this.state
const count = getDeep(this.props, 'tableData.values.length', 0)
return (
<div className="page">
{this.header}
@ -89,11 +100,11 @@ class LogsPage extends PureComponent<Props, State> {
onSearch={this.handleSubmitSearch}
/>
<FilterBar
numResults={300}
numResults={count}
filters={filters}
onUpdateFilters={this.handleUpdateFilters}
/>
<Table thing="snooo" />
<LogsTable data={this.props.tableData} />
</div>
</div>
)
@ -149,7 +160,7 @@ class LogsPage extends PureComponent<Props, State> {
private handleChooseTimerange = (timeRange: TimeRange) => {
this.props.setTimeRangeAsync(timeRange)
this.props.executeHistogramQueryAsync()
this.props.executeQueriesAsync()
}
private handleChooseSource = (sourceID: string) => {
@ -175,6 +186,7 @@ const mapStateToProps = ({
timeRange,
currentNamespace,
histogramData,
tableData,
},
}) => ({
sources,
@ -183,6 +195,7 @@ const mapStateToProps = ({
timeRange,
currentNamespace,
histogramData,
tableData,
})
const mapDispatchToProps = {
@ -190,7 +203,7 @@ const mapDispatchToProps = {
getSources: getSourcesAsync,
setTimeRangeAsync,
setNamespaceAsync,
executeHistogramQueryAsync,
executeQueriesAsync,
changeZoomAsync,
}

View File

@ -7,6 +7,8 @@ const defaultState: LogsState = {
timeRange: {lower: 'now() - 1m', upper: null},
currentNamespace: null,
histogramQueryConfig: null,
tableQueryConfig: null,
tableData: [],
histogramData: [],
}
@ -24,6 +26,10 @@ export default (state: LogsState = defaultState, action: Action) => {
return {...state, histogramQueryConfig: action.payload.queryConfig}
case ActionTypes.SetHistogramData:
return {...state, histogramData: action.payload.data}
case ActionTypes.SetTableQueryConfig:
return {...state, tableQueryConfig: action.payload.queryConfig}
case ActionTypes.SetTableData:
return {...state, tableData: action.payload.data}
case ActionTypes.ChangeZoom:
const {timeRange, data} = action.payload
return {...state, timeRange, histogramData: data}

View File

@ -1,7 +1,10 @@
import moment from 'moment'
import uuid from 'uuid'
import {TimeRange, Namespace, QueryConfig} from 'src/types'
const fields = [
const BIN_COUNT = 30
const histogramFields = [
{
alias: '',
args: [
@ -16,27 +19,96 @@ const fields = [
},
]
const tableFields = [
{
alias: 'timestamp',
type: 'field',
value: 'timestamp',
},
{
alias: 'facility_code',
type: 'field',
value: 'facility_code',
},
{
alias: 'procid',
type: 'field',
value: 'procid',
},
{
alias: 'severity_code',
type: 'field',
value: 'severity_code',
},
{
alias: 'message',
type: 'field',
value: 'message',
},
]
const defaultQueryConfig = {
areTagsAccepted: false,
fields,
fill: '0',
groupBy: {time: '2m', tags: []},
measurement: 'syslog',
rawText: null,
shifts: [],
tags: {},
}
const computeSeconds = (range: TimeRange) => {
const {upper, lower, seconds} = range
if (seconds) {
return seconds
} else if (upper && lower) {
return moment(upper).unix() - moment(lower).unix()
} else {
return 120
}
}
const createGroupBy = (range: TimeRange) => {
const seconds = computeSeconds(range)
const time = `${Math.floor(seconds / BIN_COUNT)}s`
const tags = []
return {time, tags}
}
export const buildHistogramQueryConfig = (
namespace: Namespace,
range: TimeRange
): QueryConfig => {
const id = uuid.v4()
const {database, retentionPolicy} = namespace
return {
...defaultQueryConfig,
id,
range,
database: namespace.database,
retentionPolicy: namespace.retentionPolicy,
database,
retentionPolicy,
groupBy: createGroupBy(range),
fields: histogramFields,
}
}
export const buildTableQueryConfig = (
namespace: Namespace,
range: TimeRange
): QueryConfig => {
const id = uuid.v4()
const {database, retentionPolicy} = namespace
return {
...defaultQueryConfig,
id,
range,
database,
retentionPolicy,
groupBy: {tags: []},
fields: tableFields,
fill: null,
}
}

View File

@ -7,6 +7,8 @@ export interface LogsState {
timeRange: TimeRange
histogramQueryConfig: QueryConfig | null
histogramData: object[]
tableQueryConfig: QueryConfig | null
tableData: object[]
}
export interface LocalStorage {

View File

@ -86,6 +86,7 @@ export interface Status {
export interface TimeRange {
lower: string
upper?: string | null
seconds?: number
}
export interface DurationRange {

View File

@ -1,5 +1,9 @@
import _ from 'lodash'
export function getDeep<T = any>(obj: any, path: string, fallback: T): T {
export function getDeep<T = any>(
obj: any,
path: string | number,
fallback: T
): T {
return _.get<T>(obj, path, fallback)
}