Show histogram for log counts in log viewer

pull/10616/head
Brandon Farmer 2018-05-25 13:45:48 -07:00
parent 6c0d7984e4
commit 58de6abd80
13 changed files with 355 additions and 64 deletions

View File

@ -56,6 +56,7 @@ export const saveToLocalStorage = ({
timeRange,
dataExplorer,
dashTimeV1: {ranges},
logs,
script,
}: LocalStorage): void => {
try {
@ -72,6 +73,7 @@ export const saveToLocalStorage = ({
dataExplorer,
dataExplorerQueryConfigs,
script,
logs,
})
)
} catch (err) {

View File

@ -1,13 +1,22 @@
import {Source, Namespace, TimeRange} from 'src/types'
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 {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}
export enum ActionTypes {
SetSource = 'LOGS_SET_SOURCE',
SetNamespaces = 'LOGS_SET_NAMESPACES',
SetTimeRange = 'LOGS_SET_TIMERANGE',
SetNamespace = 'LOGS_SET_NAMESPACE',
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM',
}
interface SetSourceAction {
@ -38,11 +47,36 @@ interface SetTimeRangeAction {
}
}
interface SetHistogramQueryConfig {
type: ActionTypes.SetHistogramQueryConfig
payload: {
queryConfig: QueryConfig
}
}
interface SetHistogramData {
type: ActionTypes.SetHistogramData
payload: {
data: object[]
}
}
interface ChangeZoomAction {
type: ActionTypes.ChangeZoom
payload: {
data: object[]
timeRange: TimeRange
}
}
export type Action =
| SetSourceAction
| SetNamespacesAction
| SetTimeRangeAction
| SetNamespaceAction
| SetHistogramQueryConfig
| SetHistogramData
| ChangeZoomAction
export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource,
@ -51,12 +85,75 @@ export const setSource = (source: Source): SetSourceAction => ({
},
})
export const setNamespace = (namespace: Namespace): SetNamespaceAction => ({
type: ActionTypes.SetNamespace,
export const executeHistogramQueryAsync = () => async (
dispatch,
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 query = buildQuery(timeRange, queryConfig)
const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch({
type: ActionTypes.SetHistogramData,
payload: {
namespace,
data: [{response}],
},
})
}
}
export const setHistogramQueryConfigAsync = () => 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 = buildHistogramQueryConfig(namespace, timeRange)
dispatch({
type: ActionTypes.SetHistogramQueryConfig,
payload: {queryConfig},
})
dispatch(executeHistogramQueryAsync())
}
}
export const setNamespaceAsync = (namespace: Namespace) => async (
dispatch
): Promise<void> => {
dispatch({
type: ActionTypes.SetNamespace,
payload: {namespace},
})
dispatch(setHistogramQueryConfigAsync())
}
export const setNamespaces = (
namespaces: Namespace[]
@ -67,27 +164,70 @@ export const setNamespaces = (
},
})
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
dispatch
): Promise<void> => {
dispatch({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
dispatch(setHistogramQueryConfigAsync())
}
export const getSourceAsync = (sourceID: string) => async dispatch => {
export const populateNamespacesAsync = (proxyLink: string) => async (
dispatch
): Promise<void> => {
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespaces(namespaces))
dispatch(setNamespaceAsync(namespaces[0]))
}
}
export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
dispatch
): Promise<void> => {
const response = await getSource(sourceID)
const source = response.data
const proxyLink = getDeep<string | null>(source, 'links.proxy', null)
if (proxyLink) {
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespaces(namespaces))
dispatch(setNamespace(namespaces[0]))
}
dispatch(setSource(source))
dispatch(populateNamespacesAsync(proxyLink))
}
}
export const changeZoomAsync = (timeRange: TimeRange) => async (
dispatch,
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
)
if (namespace && proxyLink) {
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
const query = buildQuery(timeRange, queryConfig)
const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch({
type: ActionTypes.ChangeZoom,
payload: {
data: [{response}],
timeRange,
},
})
}
}

24
ui/src/logs/api/index.ts Normal file
View File

@ -0,0 +1,24 @@
import {proxy} from 'src/utils/queryUrlGenerator'
import {Namespace} from 'src/types'
import {TimeSeriesResponse} from 'src/types/series'
export const executeQueryAsync = async (
proxyLink: string,
namespace: Namespace,
query: string
): Promise<TimeSeriesResponse> => {
try {
const {data} = await proxy({
source: proxyLink,
db: namespace.database,
rp: namespace.retentionPolicy,
query,
tempVars: [],
resolution: null,
})
return data
} catch (error) {
throw error
}
}

View File

@ -0,0 +1,33 @@
import React, {PureComponent} from 'react'
import LineGraph from 'src/shared/components/LineGraph'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {TimeRange} from 'src/types'
interface Props {
onZoom: (lower: string, upper: string) => void
timeRange: TimeRange
data: object[]
}
class LogViewerChart extends PureComponent<Props> {
public render() {
const {timeRange, data, onZoom} = this.props
return (
<LineGraph
onZoom={onZoom}
queries={[]}
data={data}
displayOptions={{animatedZooms: false}}
setResolution={this.setResolution}
isBarGraph={true}
timeRange={timeRange}
colors={DEFAULT_LINE_COLORS}
/>
)
}
private setResolution = () => {}
}
export default LogViewerChart

View File

@ -1,14 +1,10 @@
import React, {PureComponent} from 'react'
interface Props {
thing: string
}
class LogsGraphContainer extends PureComponent<Props> {
class LogsGraphContainer extends PureComponent {
public render() {
return (
<div className="logs-viewer--graph-container">
<p>{this.props.thing}</p>
<div className="logs-viewer--graph">{this.props.children}</div>
</div>
)
}

View File

@ -1,12 +1,20 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {getSourceAsync, setTimeRange, setNamespace} from 'src/logs/actions'
import {
getSourceAndPopulateNamespacesAsync,
setTimeRangeAsync,
setNamespaceAsync,
executeHistogramQueryAsync,
changeZoomAsync,
} from 'src/logs/actions'
import {getSourcesAsync} from 'src/shared/actions/sources'
import {Source, Namespace, TimeRange} from 'src/types'
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
import LogViewerChart from 'src/logs/components/LogViewerChart'
import GraphContainer from 'src/logs/components/LogsGraphContainer'
import TableContainer from 'src/logs/components/LogsTableContainer'
import {Source, Namespace, TimeRange} from 'src/types'
interface Props {
sources: Source[]
currentSource: Source | null
@ -14,9 +22,12 @@ interface Props {
currentNamespace: Namespace
getSource: (sourceID: string) => void
getSources: () => void
setTimeRange: (timeRange: TimeRange) => void
setNamespace: (namespace: Namespace) => void
setTimeRangeAsync: (timeRange: TimeRange) => void
setNamespaceAsync: (namespace: Namespace) => void
changeZoomAsync: (timeRange: TimeRange) => void
executeHistogramQueryAsync: () => void
timeRange: TimeRange
histogramData: object[]
}
class LogsPage extends PureComponent<Props> {
@ -42,20 +53,31 @@ class LogsPage extends PureComponent<Props> {
</div>
</div>
<div className="page-contents logs-viewer">
<GraphContainer thing="wooo" />
<GraphContainer>{this.chart}</GraphContainer>
<TableContainer thing="snooo" />
</div>
</div>
)
}
private get chart(): JSX.Element {
const {histogramData, timeRange} = this.props
return (
<LogViewerChart
timeRange={timeRange}
data={histogramData}
onZoom={this.handleChartZoom}
/>
)
}
private get header(): JSX.Element {
const {
sources,
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
timeRange,
} = this.props
return (
@ -73,7 +95,8 @@ class LogsPage extends PureComponent<Props> {
}
private handleChooseTimerange = (timeRange: TimeRange) => {
this.props.setTimeRange(timeRange)
this.props.setTimeRangeAsync(timeRange)
this.props.executeHistogramQueryAsync()
}
private handleChooseSource = (sourceID: string) => {
@ -81,27 +104,41 @@ class LogsPage extends PureComponent<Props> {
}
private handleChooseNamespace = (namespace: Namespace) => {
// Do flip
this.props.setNamespace(namespace)
this.props.setNamespaceAsync(namespace)
}
private handleChartZoom = (lower, upper) => {
if (lower) {
this.props.changeZoomAsync({lower, upper})
}
}
}
const mapStateToProps = ({
sources,
logs: {currentSource, currentNamespaces, timeRange, currentNamespace},
logs: {
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
histogramData,
},
}) => ({
sources,
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
histogramData,
})
const mapDispatchToProps = {
getSource: getSourceAsync,
getSource: getSourceAndPopulateNamespacesAsync,
getSources: getSourcesAsync,
setTimeRange,
setNamespace,
setTimeRangeAsync,
setNamespaceAsync,
executeHistogramQueryAsync,
changeZoomAsync,
}
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)

View File

@ -1,18 +1,13 @@
import {Source, Namespace, TimeRange} from 'src/types'
import {ActionTypes, Action} from 'src/logs/actions'
import {LogsState} from 'src/types/localStorage'
interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
}
const defaultState = {
const defaultState: LogsState = {
currentSource: null,
currentNamespaces: [],
timeRange: {lower: 'now() - 1m', upper: null},
currentNamespace: null,
histogramQueryConfig: null,
histogramData: [],
}
export default (state: LogsState = defaultState, action: Action) => {
@ -25,6 +20,13 @@ export default (state: LogsState = defaultState, action: Action) => {
return {...state, timeRange: action.payload.timeRange}
case ActionTypes.SetNamespace:
return {...state, currentNamespace: action.payload.namespace}
case ActionTypes.SetHistogramQueryConfig:
return {...state, histogramQueryConfig: action.payload.queryConfig}
case ActionTypes.SetHistogramData:
return {...state, histogramData: action.payload.data}
case ActionTypes.ChangeZoom:
const {timeRange, data} = action.payload
return {...state, timeRange, histogramData: data}
default:
return state
}

View File

@ -0,0 +1,42 @@
import uuid from 'uuid'
import {TimeRange, Namespace, QueryConfig} from 'src/types'
const fields = [
{
alias: '',
args: [
{
alias: '',
type: 'field',
value: 'message',
},
],
type: 'func',
value: 'count',
},
]
const defaultQueryConfig = {
areTagsAccepted: false,
fields,
fill: '0',
groupBy: {time: '2m', tags: []},
measurement: 'syslog',
rawText: null,
shifts: [],
tags: {},
}
export const buildHistogramQueryConfig = (
namespace: Namespace,
range: TimeRange
): QueryConfig => {
const id = uuid.v4()
return {
...defaultQueryConfig,
id,
range,
database: namespace.database,
retentionPolicy: namespace.retentionPolicy,
}
}

View File

@ -88,8 +88,8 @@ class Dygraph extends Component {
this.dygraph = new Dygraphs(graphRef, timeSeries, {
...defaultOptions,
...options,
...OPTIONS,
...options,
})
const {w} = this.dygraph.getArea()

View File

@ -199,6 +199,7 @@ LineGraph.propTypes = {
displayOptions: shape({
stepPlot: bool,
stackedGraph: bool,
animatedZooms: bool,
}),
activeQueryIndex: number,
ruleValues: shape({}),

View File

@ -15,9 +15,10 @@ $logs-viewer-gutter: 60px;
}
.logs-viewer--graph-container {
padding: 30px $logs-viewer-gutter 18px $logs-viewer-gutter;
padding: 22px ($logs-viewer-gutter - 16px) 10px ($logs-viewer-gutter - 16px);
height: $logs-viewer-graph-height;
@include gradient-v($g2-kevlar, $g0-obsidian);
display: flex;
}
.logs-viewer--search-container {
@ -31,3 +32,10 @@ $logs-viewer-gutter: 60px;
height: calc(100% - #{$logs-viewer-graph-height + $logs-viewer-search-height});
background-color: $g3-castle;
}
.logs-viewer--graph {
position: relative;
width: 100%;
height: 100%;
padding: 8px 16px;
}

View File

@ -1,4 +1,13 @@
import {QueryConfig, TimeRange} from 'src/types'
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
export interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
histogramQueryConfig: QueryConfig | null
histogramData: object[]
}
export interface LocalStorage {
VERSION: VERSION
@ -8,6 +17,7 @@ export interface LocalStorage {
dataExplorerQueryConfigs: DataExplorerQueryConfigs
timeRange: TimeRange
script: string
logs: LogsState
}
export type VERSION = string

View File

@ -8,15 +8,9 @@ import {
TYPE_IFQL,
} from 'src/dashboards/constants'
import {shiftTimeRange} from 'src/shared/query/helpers'
import {QueryConfig, Field, GroupBy, TimeShift} from 'src/types'
import {QueryConfig, Field, GroupBy, TimeShift, TimeRange} from 'src/types'
export const quoteIfTimestamp = ({
lower,
upper,
}: {
lower: string
upper: string
}): {lower: string; upper: string} => {
export const quoteIfTimestamp = ({lower, upper}: TimeRange): TimeRange => {
if (lower && lower.includes('Z') && !lower.includes("'")) {
lower = `'${lower}'`
}
@ -29,7 +23,7 @@ export const quoteIfTimestamp = ({
}
export default function buildInfluxQLQuery(
timeRange,
timeRange: TimeRange,
config: QueryConfig,
shift: string = ''
): string {
@ -66,7 +60,7 @@ function buildSelect(
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
export const buildQuery = (
type: string,
timeRange: object,
timeRange: TimeRange,
config: QueryConfig,
shift: TimeShift | null = null
): string => {
@ -201,5 +195,7 @@ function buildFill(fill: string): string {
return ` FILL(${fill})`
}
export const buildRawText = (config: QueryConfig, timeRange: object): string =>
config.rawText || buildInfluxQLQuery(timeRange, config) || ''
export const buildRawText = (
config: QueryConfig,
timeRange: TimeRange
): string => config.rawText || buildInfluxQLQuery(timeRange, config) || ''