Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
N
nexpie-grafana-theme
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Registry
Registry
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Kornkitt Poolsup
nexpie-grafana-theme
Commits
20307828
Unverified
Commit
20307828
authored
Nov 08, 2018
by
David
Committed by
GitHub
Nov 08, 2018
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13942 from grafana/davkal/explore-logging-graph
Explore: Logging graph overview and view options
parents
662808f0
6b8d7c89
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
642 additions
and
80 deletions
+642
-80
public/app/core/components/Switch/Switch.tsx
+8
-3
public/app/core/logs_model.ts
+71
-12
public/app/core/utils/colors.ts
+10
-10
public/app/features/explore/Explore.tsx
+33
-11
public/app/features/explore/Graph.tsx
+29
-5
public/app/features/explore/Logs.tsx
+110
-4
public/app/features/explore/TimePicker.tsx
+24
-5
public/app/plugins/datasource/logging/datasource.ts
+14
-8
public/app/plugins/datasource/logging/result_transformer.test.ts
+137
-4
public/app/plugins/datasource/logging/result_transformer.ts
+139
-12
public/sass/components/_gf-form.scss
+5
-0
public/sass/components/_switch.scss
+14
-1
public/sass/pages/_explore.scss
+48
-5
No files found.
public/app/core/components/Switch/Switch.tsx
View file @
20307828
...
...
@@ -5,6 +5,7 @@ export interface Props {
label
:
string
;
checked
:
boolean
;
labelClass
?:
string
;
small
?:
boolean
;
switchClass
?:
string
;
onChange
:
(
event
)
=>
any
;
}
...
...
@@ -24,10 +25,14 @@ export class Switch extends PureComponent<Props, State> {
};
render
()
{
const
{
labelClass
,
switchClass
,
label
,
checked
}
=
this
.
props
;
const
{
labelClass
=
''
,
switchClass
=
''
,
label
,
checked
,
small
}
=
this
.
props
;
const
labelId
=
`check-
${
this
.
state
.
id
}
`
;
const
labelClassName
=
`gf-form-label
${
labelClass
}
pointer`
;
const
switchClassName
=
`gf-form-switch
${
switchClass
}
`
;
let
labelClassName
=
`gf-form-label
${
labelClass
}
pointer`
;
let
switchClassName
=
`gf-form-switch
${
switchClass
}
`
;
if
(
small
)
{
labelClassName
+=
' gf-form-label--small'
;
switchClassName
+=
' gf-form-switch--small'
;
}
return
(
<
div
className=
"gf-form"
>
...
...
public/app/core/logs_model.ts
View file @
20307828
import
_
from
'lodash'
;
import
{
TimeSeries
}
from
'app/core/core'
;
import
colors
from
'app/core/utils/colors'
;
export
enum
LogLevel
{
crit
=
'crit'
,
...
...
@@ -8,8 +10,20 @@ export enum LogLevel {
info
=
'info'
,
debug
=
'debug'
,
trace
=
'trace'
,
none
=
'none'
,
}
export
const
LogLevelColor
=
{
[
LogLevel
.
crit
]:
colors
[
7
],
[
LogLevel
.
warn
]:
colors
[
1
],
[
LogLevel
.
err
]:
colors
[
4
],
[
LogLevel
.
error
]:
colors
[
4
],
[
LogLevel
.
info
]:
colors
[
0
],
[
LogLevel
.
debug
]:
colors
[
3
],
[
LogLevel
.
trace
]:
colors
[
3
],
[
LogLevel
.
none
]:
'#eee'
,
};
export
interface
LogSearchMatch
{
start
:
number
;
length
:
number
;
...
...
@@ -17,27 +31,72 @@ export interface LogSearchMatch {
}
export
interface
LogRow
{
key
:
string
;
entry
:
string
;
key
:
string
;
// timestamp + labels
labels
:
string
;
logLevel
:
LogLevel
;
timestamp
:
string
;
searchWords
?:
string
[];
timestamp
:
string
;
// ISO with nanosec precision
timeFromNow
:
string
;
timeEpochMs
:
number
;
timeLocal
:
string
;
searchWords
?:
string
[];
uniqueLabels
?:
string
;
}
export
interface
LogsMetaItem
{
label
:
string
;
value
:
string
;
}
export
interface
LogsModel
{
meta
?:
LogsMetaItem
[];
rows
:
LogRow
[];
series
?:
TimeSeries
[];
}
export
interface
LogsStream
{
labels
:
string
;
entries
:
LogsStreamEntry
[];
search
?:
string
;
parsedLabels
?:
LogsStreamLabels
;
uniqueLabels
?:
string
;
}
export
function
mergeStreams
(
streams
:
LogsModel
[],
limit
?:
number
):
LogsModel
{
const
combinedEntries
=
streams
.
reduce
((
acc
,
stream
)
=>
{
return
[...
acc
,
...
stream
.
rows
];
export
interface
LogsStreamEntry
{
line
:
string
;
timestamp
:
string
;
}
export
interface
LogsStreamLabels
{
[
key
:
string
]:
string
;
}
export
function
makeSeriesForLogs
(
rows
:
LogRow
[],
intervalMs
:
number
):
TimeSeries
[]
{
// Graph time series by log level
const
seriesByLevel
=
{};
rows
.
forEach
(
row
=>
{
if
(
!
seriesByLevel
[
row
.
logLevel
])
{
seriesByLevel
[
row
.
logLevel
]
=
{
lastTs
:
null
,
datapoints
:
[],
alias
:
row
.
logLevel
};
}
const
levelSeries
=
seriesByLevel
[
row
.
logLevel
];
// Bucket to nearest minute
const
time
=
Math
.
round
(
row
.
timeEpochMs
/
intervalMs
/
10
)
*
intervalMs
*
10
;
// Entry for time
if
(
time
===
levelSeries
.
lastTs
)
{
levelSeries
.
datapoints
[
levelSeries
.
datapoints
.
length
-
1
][
0
]
++
;
}
else
{
levelSeries
.
datapoints
.
push
([
1
,
time
]);
levelSeries
.
lastTs
=
time
;
}
});
return
Object
.
keys
(
seriesByLevel
).
reduce
((
acc
,
level
)
=>
{
if
(
seriesByLevel
[
level
])
{
const
gs
=
new
TimeSeries
(
seriesByLevel
[
level
]);
gs
.
setColor
(
LogLevelColor
[
level
]);
acc
.
push
(
gs
);
}
return
acc
;
},
[]);
const
sortedEntries
=
_
.
chain
(
combinedEntries
)
.
sortBy
(
'timestamp'
)
.
reverse
()
.
slice
(
0
,
limit
||
combinedEntries
.
length
)
.
value
();
return
{
rows
:
sortedEntries
};
}
public/app/core/utils/colors.ts
View file @
20307828
...
...
@@ -10,16 +10,16 @@ export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
export
const
REGION_FILL_ALPHA
=
0.09
;
const
colors
=
[
'#7EB26D'
,
'#EAB839'
,
'#6ED0E0'
,
'#EF843C'
,
'#E24D42'
,
'#1F78C1'
,
'#BA43A9'
,
'#705DA0'
,
'#508642'
,
'#CCA300'
,
'#7EB26D'
,
// 0: pale green
'#EAB839'
,
// 1: mustard
'#6ED0E0'
,
// 2: light blue
'#EF843C'
,
// 3: orange
'#E24D42'
,
// 4: red
'#1F78C1'
,
// 5: ocean
'#BA43A9'
,
// 6: purple
'#705DA0'
,
// 7: violet
'#508642'
,
// 8: dark green
'#CCA300'
,
// 9: dark sand
'#447EBC'
,
'#C15C17'
,
'#890F02'
,
...
...
public/app/features/explore/Explore.tsx
View file @
20307828
...
...
@@ -25,10 +25,20 @@ import ErrorBoundary from './ErrorBoundary';
import
TimePicker
from
'./TimePicker'
;
import
{
ensureQueries
,
generateQueryKey
,
hasQuery
}
from
'./utils/query'
;
import
{
DataSource
}
from
'app/types/datasources'
;
import
{
mergeStreams
}
from
'app/core/logs_model'
;
const
MAX_HISTORY_ITEMS
=
100
;
function
getIntervals
(
range
:
RawTimeRange
,
datasource
,
resolution
:
number
):
{
interval
:
string
;
intervalMs
:
number
}
{
if
(
!
datasource
||
!
resolution
)
{
return
{
interval
:
'1s'
,
intervalMs
:
1000
};
}
const
absoluteRange
:
RawTimeRange
=
{
from
:
parseDate
(
range
.
from
,
false
),
to
:
parseDate
(
range
.
to
,
true
),
};
return
kbn
.
calculateInterval
(
absoluteRange
,
resolution
,
datasource
.
interval
);
}
function
makeTimeSeriesList
(
dataList
,
options
)
{
return
dataList
.
map
((
seriesData
,
index
)
=>
{
const
datapoints
=
seriesData
.
datapoints
||
[];
...
...
@@ -471,12 +481,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
targetOptions
:
{
format
:
string
;
hinting
?:
boolean
;
instant
?:
boolean
}
)
{
const
{
datasource
,
range
}
=
this
.
state
;
const
resolution
=
this
.
el
.
offsetWidth
;
const
absoluteRange
:
RawTimeRange
=
{
from
:
parseDate
(
range
.
from
,
false
),
to
:
parseDate
(
range
.
to
,
true
),
};
const
{
interval
}
=
kbn
.
calculateInterval
(
absoluteRange
,
resolution
,
datasource
.
interval
);
const
{
interval
,
intervalMs
}
=
getIntervals
(
range
,
datasource
,
this
.
el
.
offsetWidth
);
const
targets
=
[
{
...
targetOptions
,
...
...
@@ -491,6 +496,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
return
{
interval
,
intervalMs
,
targets
,
range
:
queryRange
,
};
...
...
@@ -759,6 +765,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const
tableButtonActive
=
showingBoth
||
showingTable
?
'active'
:
''
;
const
exploreClass
=
split
?
'explore explore-split'
:
'explore'
;
const
selectedDatasource
=
datasource
?
exploreDatasources
.
find
(
d
=>
d
.
label
===
datasource
.
name
)
:
undefined
;
const
graphRangeIntervals
=
getIntervals
(
graphRange
,
datasource
,
this
.
el
?
this
.
el
.
offsetWidth
:
0
);
const
graphLoading
=
queryTransactions
.
some
(
qt
=>
qt
.
resultType
===
'Graph'
&&
!
qt
.
done
);
const
tableLoading
=
queryTransactions
.
some
(
qt
=>
qt
.
resultType
===
'Table'
&&
!
qt
.
done
);
const
logsLoading
=
queryTransactions
.
some
(
qt
=>
qt
.
resultType
===
'Logs'
&&
!
qt
.
done
);
...
...
@@ -770,9 +777,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
new
TableModel
(),
...
queryTransactions
.
filter
(
qt
=>
qt
.
resultType
===
'Table'
&&
qt
.
done
&&
qt
.
result
).
map
(
qt
=>
qt
.
result
)
);
const
logsResult
=
mergeStreams
(
queryTransactions
.
filter
(
qt
=>
qt
.
resultType
===
'Logs'
&&
qt
.
done
&&
qt
.
result
).
map
(
qt
=>
qt
.
result
)
);
const
logsResult
=
datasource
&&
datasource
.
mergeStreams
?
datasource
.
mergeStreams
(
_
.
flatten
(
queryTransactions
.
filter
(
qt
=>
qt
.
resultType
===
'Logs'
&&
qt
.
done
&&
qt
.
result
).
map
(
qt
=>
qt
.
result
)
),
graphRangeIntervals
.
intervalMs
)
:
undefined
;
const
loading
=
queryTransactions
.
some
(
qt
=>
!
qt
.
done
);
const
showStartPages
=
StartPage
&&
queryTransactions
.
length
===
0
;
const
viewModeCount
=
[
supportsGraph
,
supportsLogs
,
supportsTable
].
filter
(
m
=>
m
).
length
;
...
...
@@ -894,6 +907,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
height=
{
graphHeight
}
loading=
{
graphLoading
}
id=
{
`explore-graph-${position}`
}
onChangeTime=
{
this
.
onChangeTime
}
range=
{
graphRange
}
split=
{
split
}
/>
...
...
@@ -903,7 +917,15 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
<
Table
data=
{
tableResult
}
loading=
{
tableLoading
}
onClickCell=
{
this
.
onClickTableCell
}
/>
</
div
>
)
:
null
}
{
supportsLogs
&&
showingLogs
?
<
Logs
data=
{
logsResult
}
loading=
{
logsLoading
}
/>
:
null
}
{
supportsLogs
&&
showingLogs
?
(
<
Logs
data=
{
logsResult
}
loading=
{
logsLoading
}
position=
{
position
}
onChangeTime=
{
this
.
onChangeTime
}
range=
{
range
}
/>
)
:
null
}
</>
)
}
</
ErrorBoundary
>
...
...
public/app/features/explore/Graph.tsx
View file @
20307828
...
...
@@ -5,6 +5,8 @@ import { withSize } from 'react-sizeme';
import
'vendor/flot/jquery.flot'
;
import
'vendor/flot/jquery.flot.time'
;
import
'vendor/flot/jquery.flot.selection'
;
import
'vendor/flot/jquery.flot.stack'
;
import
{
RawTimeRange
}
from
'app/types/series'
;
import
*
as
dateMath
from
'app/core/utils/datemath'
;
...
...
@@ -62,10 +64,10 @@ const FLOT_OPTIONS = {
margin
:
{
left
:
0
,
right
:
0
},
labelMarginX
:
0
,
},
//
selection: {
//
mode: 'x',
//
color: '#666',
//
},
selection
:
{
mode
:
'x'
,
color
:
'#666'
,
},
// crosshair: {
// mode: 'x',
// },
...
...
@@ -79,6 +81,8 @@ interface GraphProps {
range
:
RawTimeRange
;
split
?:
boolean
;
size
?:
{
width
:
number
;
height
:
number
};
userOptions
?:
any
;
onChangeTime
?:
(
range
:
RawTimeRange
)
=>
void
;
}
interface
GraphState
{
...
...
@@ -86,6 +90,8 @@ interface GraphState {
}
export
class
Graph
extends
PureComponent
<
GraphProps
,
GraphState
>
{
$el
:
any
;
state
=
{
showAllTimeSeries
:
false
,
};
...
...
@@ -98,6 +104,8 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
componentDidMount
()
{
this
.
draw
();
this
.
$el
=
$
(
`#
${
this
.
props
.
id
}
`
);
this
.
$el
.
bind
(
'plotselected'
,
this
.
onPlotSelected
);
}
componentDidUpdate
(
prevProps
:
GraphProps
)
{
...
...
@@ -112,6 +120,20 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
}
}
componentWillUnmount
()
{
this
.
$el
.
unbind
(
'plotselected'
,
this
.
onPlotSelected
);
}
onPlotSelected
=
(
event
,
ranges
)
=>
{
if
(
this
.
props
.
onChangeTime
)
{
const
range
=
{
from
:
moment
(
ranges
.
xaxis
.
from
),
to
:
moment
(
ranges
.
xaxis
.
to
),
};
this
.
props
.
onChangeTime
(
range
);
}
};
onShowAllTimeSeries
=
()
=>
{
this
.
setState
(
{
...
...
@@ -122,7 +144,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
};
draw
()
{
const
{
range
,
size
}
=
this
.
props
;
const
{
range
,
size
,
userOptions
=
{}
}
=
this
.
props
;
const
data
=
this
.
getGraphData
();
const
$el
=
$
(
`#
${
this
.
props
.
id
}
`
);
...
...
@@ -153,12 +175,14 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
max
:
max
,
label
:
'Datetime'
,
ticks
:
ticks
,
timezone
:
'browser'
,
timeformat
:
time_format
(
ticks
,
min
,
max
),
},
};
const
options
=
{
...
FLOT_OPTIONS
,
...
dynamicOptions
,
...
userOptions
,
};
$
.
plot
(
$el
,
series
,
options
);
}
...
...
public/app/features/explore/Logs.tsx
View file @
20307828
import
React
,
{
Fragment
,
PureComponent
}
from
'react'
;
import
Highlighter
from
'react-highlight-words'
;
import
{
RawTimeRange
}
from
'app/types/series'
;
import
{
LogsModel
}
from
'app/core/logs_model'
;
import
{
findHighlightChunksInText
}
from
'app/core/utils/text'
;
import
{
Switch
}
from
'app/core/components/Switch/Switch'
;
import
Graph
from
'./Graph'
;
const
graphOptions
=
{
series
:
{
bars
:
{
show
:
true
,
lineWidth
:
5
,
// barWidth: 10,
},
// stack: true,
},
yaxis
:
{
tickDecimals
:
0
,
},
};
interface
LogsProps
{
className
?:
string
;
data
:
LogsModel
;
loading
:
boolean
;
position
:
string
;
range
?:
RawTimeRange
;
onChangeTime
?:
(
range
:
RawTimeRange
)
=>
void
;
}
interface
LogsState
{
showLabels
:
boolean
;
showLocalTime
:
boolean
;
showUtc
:
boolean
;
}
export
default
class
Logs
extends
PureComponent
<
LogsProps
,
{}
>
{
export
default
class
Logs
extends
PureComponent
<
LogsProps
,
LogsState
>
{
state
=
{
showLabels
:
true
,
showLocalTime
:
true
,
showUtc
:
false
,
};
onChangeLabels
=
(
event
:
React
.
SyntheticEvent
)
=>
{
const
target
=
event
.
target
as
HTMLInputElement
;
this
.
setState
({
showLabels
:
target
.
checked
,
});
};
onChangeLocalTime
=
(
event
:
React
.
SyntheticEvent
)
=>
{
const
target
=
event
.
target
as
HTMLInputElement
;
this
.
setState
({
showLocalTime
:
target
.
checked
,
});
};
onChangeUtc
=
(
event
:
React
.
SyntheticEvent
)
=>
{
const
target
=
event
.
target
as
HTMLInputElement
;
this
.
setState
({
showUtc
:
target
.
checked
,
});
};
render
()
{
const
{
className
=
''
,
data
,
loading
=
false
}
=
this
.
props
;
const
{
className
=
''
,
data
,
loading
=
false
,
position
,
range
}
=
this
.
props
;
const
{
showLabels
,
showLocalTime
,
showUtc
}
=
this
.
state
;
const
hasData
=
data
&&
data
.
rows
&&
data
.
rows
.
length
>
0
;
const
cssColumnSizes
=
[
'4px'
];
if
(
showUtc
)
{
cssColumnSizes
.
push
(
'minmax(100px, max-content)'
);
}
if
(
showLocalTime
)
{
cssColumnSizes
.
push
(
'minmax(100px, max-content)'
);
}
if
(
showLabels
)
{
cssColumnSizes
.
push
(
'minmax(100px, 25%)'
);
}
cssColumnSizes
.
push
(
'1fr'
);
const
logEntriesStyle
=
{
gridTemplateColumns
:
cssColumnSizes
.
join
(
' '
),
};
return
(
<
div
className=
{
`${className} logs`
}
>
<
div
className=
"logs-graph"
>
<
Graph
data=
{
data
.
series
}
height=
"100px"
range=
{
range
}
id=
{
`explore-logs-graph-${position}`
}
onChangeTime=
{
this
.
props
.
onChangeTime
}
userOptions=
{
graphOptions
}
/>
</
div
>
<
div
className=
"panel-container logs-options"
>
<
div
className=
"logs-controls"
>
<
Switch
label=
"Timestamp"
checked=
{
showUtc
}
onChange=
{
this
.
onChangeUtc
}
small
/>
<
Switch
label=
"Local time"
checked=
{
showLocalTime
}
onChange=
{
this
.
onChangeLocalTime
}
small
/>
<
Switch
label=
"Labels"
checked=
{
showLabels
}
onChange=
{
this
.
onChangeLabels
}
small
/>
{
hasData
&&
data
.
meta
&&
(
<
div
className=
"logs-meta"
>
{
data
.
meta
.
map
(
item
=>
(
<
div
className=
"logs-meta-item"
key=
{
item
.
label
}
>
<
span
className=
"logs-meta-item__label"
>
{
item
.
label
}
:
</
span
>
<
span
className=
"logs-meta-item__value"
>
{
item
.
value
}
</
span
>
</
div
>
))
}
</
div
>
)
}
</
div
>
</
div
>
<
div
className=
"panel-container"
>
{
loading
&&
<
div
className=
"explore-panel__loader"
/>
}
<
div
className=
"logs-entries"
>
<
div
className=
"logs-entries"
style=
{
logEntriesStyle
}
>
{
hasData
&&
data
.
rows
.
map
(
row
=>
(
<
Fragment
key=
{
row
.
key
}
>
<
div
className=
{
row
.
logLevel
?
`logs-row-level logs-row-level-${row.logLevel}`
:
''
}
/>
<
div
title=
{
`${row.timestamp} (${row.timeFromNow})`
}
>
{
row
.
timeLocal
}
</
div
>
{
showUtc
&&
<
div
title=
{
`Local: ${row.timeLocal} (${row.timeFromNow})`
}
>
{
row
.
timestamp
}
</
div
>
}
{
showLocalTime
&&
<
div
title=
{
`${row.timestamp} (${row.timeFromNow})`
}
>
{
row
.
timeLocal
}
</
div
>
}
{
showLabels
&&
(
<
div
className=
"max-width"
title=
{
row
.
labels
}
>
{
row
.
labels
}
</
div
>
)
}
<
div
>
<
Highlighter
textToHighlight=
{
row
.
entry
}
...
...
public/app/features/explore/TimePicker.tsx
View file @
20307828
...
...
@@ -16,6 +16,9 @@ export const DEFAULT_RANGE = {
* @param value Epoch or relative time
*/
export
function
parseTime
(
value
:
string
,
isUtc
=
false
):
string
{
if
(
moment
.
isMoment
(
value
))
{
return
value
;
}
if
(
value
.
indexOf
(
'now'
)
!==
-
1
)
{
return
value
;
}
...
...
@@ -39,7 +42,8 @@ interface TimePickerState {
isOpen
:
boolean
;
isUtc
:
boolean
;
rangeString
:
string
;
refreshInterval
:
string
;
refreshInterval
?:
string
;
initialRange
?:
RawTimeRange
;
// Input-controlled text, keep these in a shape that is human-editable
fromRaw
:
string
;
...
...
@@ -52,6 +56,22 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
isOpen
:
props
.
isOpen
,
isUtc
:
props
.
isUtc
,
rangeString
:
''
,
fromRaw
:
''
,
toRaw
:
''
,
initialRange
:
DEFAULT_RANGE
,
refreshInterval
:
''
,
};
}
static
getDerivedStateFromProps
(
props
,
state
)
{
if
(
state
.
initialRange
&&
state
.
initialRange
===
props
.
range
)
{
return
state
;
}
const
from
=
props
.
range
?
props
.
range
.
from
:
DEFAULT_RANGE
.
from
;
const
to
=
props
.
range
?
props
.
range
.
to
:
DEFAULT_RANGE
.
to
;
...
...
@@ -63,13 +83,12 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
to
:
toRaw
,
};
this
.
state
=
{
return
{
...
state
,
fromRaw
,
toRaw
,
isOpen
:
props
.
isOpen
,
isUtc
:
props
.
isUtc
,
initialRange
:
props
.
range
,
rangeString
:
rangeUtil
.
describeTimeRange
(
range
),
refreshInterval
:
''
,
};
}
...
...
public/app/plugins/datasource/logging/datasource.ts
View file @
20307828
...
...
@@ -3,9 +3,10 @@ import _ from 'lodash';
import
*
as
dateMath
from
'app/core/utils/datemath'
;
import
LanguageProvider
from
'./language_provider'
;
import
{
processStreams
}
from
'./result_transformer'
;
import
{
mergeStreamsToLogs
}
from
'./result_transformer'
;
import
{
LogsStream
,
LogsModel
,
makeSeriesForLogs
}
from
'app/core/logs_model'
;
const
DEFAULT_LIMIT
=
1
00
;
export
const
DEFAULT_LIMIT
=
10
00
;
const
DEFAULT_QUERY_PARAMS
=
{
direction
:
'BACKWARD'
,
...
...
@@ -67,6 +68,12 @@ export default class LoggingDatasource {
return
this
.
backendSrv
.
datasourceRequest
(
req
);
}
mergeStreams
(
streams
:
LogsStream
[],
intervalMs
:
number
):
LogsModel
{
const
logs
=
mergeStreamsToLogs
(
streams
);
logs
.
series
=
makeSeriesForLogs
(
logs
.
rows
,
intervalMs
);
return
logs
;
}
prepareQueryTarget
(
target
,
options
)
{
const
interpolated
=
this
.
templateSrv
.
replace
(
target
.
expr
);
const
start
=
this
.
getTime
(
options
.
range
.
from
,
false
);
...
...
@@ -79,7 +86,7 @@ export default class LoggingDatasource {
};
}
query
(
options
)
{
query
(
options
)
:
Promise
<
{
data
:
LogsStream
[]
}
>
{
const
queryTargets
=
options
.
targets
.
filter
(
target
=>
target
.
expr
)
.
map
(
target
=>
this
.
prepareQueryTarget
(
target
,
options
));
...
...
@@ -91,17 +98,16 @@ export default class LoggingDatasource {
return
Promise
.
all
(
queries
).
then
((
results
:
any
[])
=>
{
// Flatten streams from multiple queries
const
allStreams
=
results
.
reduce
((
acc
,
response
,
i
)
=>
{
const
streams
=
response
.
data
.
streams
||
[];
const
allStreams
:
LogsStream
[]
=
results
.
reduce
((
acc
,
response
,
i
)
=>
{
const
streams
:
LogsStream
[]
=
response
.
data
.
streams
||
[];
// Inject search for match highlighting
const
search
=
queryTargets
[
i
].
regexp
;
const
search
:
string
=
queryTargets
[
i
].
regexp
;
streams
.
forEach
(
s
=>
{
s
.
search
=
search
;
});
return
[...
acc
,
...
streams
];
},
[]);
const
model
=
processStreams
(
allStreams
,
DEFAULT_LIMIT
);
return
{
data
:
model
};
return
{
data
:
allStreams
};
});
}
...
...
public/app/plugins/datasource/logging/result_transformer.test.ts
View file @
20307828
import
{
LogLevel
}
from
'app/core/logs_model'
;
import
{
LogLevel
,
LogsStream
}
from
'app/core/logs_model'
;
import
{
getLogLevel
}
from
'./result_transformer'
;
import
{
findCommonLabels
,
findUniqueLabels
,
formatLabels
,
getLogLevel
,
mergeStreamsToLogs
,
parseLabels
,
}
from
'./result_transformer'
;
describe
(
'getLoglevel()'
,
()
=>
{
it
(
'returns no log level on empty line'
,
()
=>
{
expect
(
getLogLevel
(
''
)).
toBe
(
undefined
);
expect
(
getLogLevel
(
''
)).
toBe
(
LogLevel
.
none
);
});
it
(
'returns no log level on when level is part of a word'
,
()
=>
{
expect
(
getLogLevel
(
'this is a warning'
)).
toBe
(
undefined
);
expect
(
getLogLevel
(
'this is a warning'
)).
toBe
(
LogLevel
.
none
);
});
it
(
'returns log level on line contains a log level'
,
()
=>
{
...
...
@@ -20,3 +27,129 @@ describe('getLoglevel()', () => {
expect
(
getLogLevel
(
'WARN this could be a debug message'
)).
toBe
(
LogLevel
.
warn
);
});
});
describe
(
'parseLabels()'
,
()
=>
{
it
(
'returns no labels on emtpy labels string'
,
()
=>
{
expect
(
parseLabels
(
''
)).
toEqual
({});
expect
(
parseLabels
(
'{}'
)).
toEqual
({});
});
it
(
'returns labels on labels string'
,
()
=>
{
expect
(
parseLabels
(
'{foo="bar", baz="42"}'
)).
toEqual
({
foo
:
'"bar"'
,
baz
:
'"42"'
});
});
});
describe
(
'formatLabels()'
,
()
=>
{
it
(
'returns no labels on emtpy label set'
,
()
=>
{
expect
(
formatLabels
({})).
toEqual
(
''
);
expect
(
formatLabels
({},
'foo'
)).
toEqual
(
'foo'
);
});
it
(
'returns label string on label set'
,
()
=>
{
expect
(
formatLabels
({
foo
:
'"bar"'
,
baz
:
'"42"'
})).
toEqual
(
'{baz="42", foo="bar"}'
);
});
});
describe
(
'findCommonLabels()'
,
()
=>
{
it
(
'returns no common labels on empty sets'
,
()
=>
{
expect
(
findCommonLabels
([{}])).
toEqual
({});
expect
(
findCommonLabels
([{},
{}])).
toEqual
({});
});
it
(
'returns no common labels on differing sets'
,
()
=>
{
expect
(
findCommonLabels
([{
foo
:
'"bar"'
},
{}])).
toEqual
({});
expect
(
findCommonLabels
([{},
{
foo
:
'"bar"'
}])).
toEqual
({});
expect
(
findCommonLabels
([{
baz
:
'42'
},
{
foo
:
'"bar"'
}])).
toEqual
({});
expect
(
findCommonLabels
([{
foo
:
'42'
,
baz
:
'"bar"'
},
{
foo
:
'"bar"'
}])).
toEqual
({});
});
it
(
'returns the single labels set as common labels'
,
()
=>
{
expect
(
findCommonLabels
([{
foo
:
'"bar"'
}])).
toEqual
({
foo
:
'"bar"'
});
});
});
describe
(
'findUniqueLabels()'
,
()
=>
{
it
(
'returns no uncommon labels on empty sets'
,
()
=>
{
expect
(
findUniqueLabels
({},
{})).
toEqual
({});
});
it
(
'returns all labels given no common labels'
,
()
=>
{
expect
(
findUniqueLabels
({
foo
:
'"bar"'
},
{})).
toEqual
({
foo
:
'"bar"'
});
});
it
(
'returns all labels except the common labels'
,
()
=>
{
expect
(
findUniqueLabels
({
foo
:
'"bar"'
,
baz
:
'"42"'
},
{
foo
:
'"bar"'
})).
toEqual
({
baz
:
'"42"'
});
});
});
describe
(
'mergeStreamsToLogs()'
,
()
=>
{
it
(
'returns empty logs given no streams'
,
()
=>
{
expect
(
mergeStreamsToLogs
([]).
rows
).
toEqual
([]);
});
it
(
'returns processed logs from single stream'
,
()
=>
{
const
stream1
:
LogsStream
=
{
labels
:
'{foo="bar"}'
,
entries
:
[
{
line
:
'WARN boooo'
,
timestamp
:
'1970-01-01T00:00:00Z'
,
},
],
};
expect
(
mergeStreamsToLogs
([
stream1
]).
rows
).
toMatchObject
([
{
entry
:
'WARN boooo'
,
labels
:
'{foo="bar"}'
,
key
:
'EK1970-01-01T00:00:00Z{foo="bar"}'
,
logLevel
:
'warn'
,
uniqueLabels
:
''
,
},
]);
});
it
(
'returns merged logs from multiple streams sorted by time and with unique labels'
,
()
=>
{
const
stream1
:
LogsStream
=
{
labels
:
'{foo="bar", baz="1"}'
,
entries
:
[
{
line
:
'WARN boooo'
,
timestamp
:
'1970-01-01T00:00:01Z'
,
},
],
};
const
stream2
:
LogsStream
=
{
labels
:
'{foo="bar", baz="2"}'
,
entries
:
[
{
line
:
'INFO 1'
,
timestamp
:
'1970-01-01T00:00:00Z'
,
},
{
line
:
'INFO 2'
,
timestamp
:
'1970-01-01T00:00:02Z'
,
},
],
};
expect
(
mergeStreamsToLogs
([
stream1
,
stream2
]).
rows
).
toMatchObject
([
{
entry
:
'INFO 2'
,
labels
:
'{foo="bar", baz="2"}'
,
logLevel
:
'info'
,
uniqueLabels
:
'{baz="2"}'
,
},
{
entry
:
'WARN boooo'
,
labels
:
'{foo="bar", baz="1"}'
,
logLevel
:
'warn'
,
uniqueLabels
:
'{baz="1"}'
,
},
{
entry
:
'INFO 1'
,
labels
:
'{foo="bar", baz="2"}'
,
logLevel
:
'info'
,
uniqueLabels
:
'{baz="2"}'
,
},
]);
});
});
public/app/plugins/datasource/logging/result_transformer.ts
View file @
20307828
import
_
from
'lodash'
;
import
moment
from
'moment'
;
import
{
LogLevel
,
LogsModel
,
LogRow
}
from
'app/core/logs_model'
;
import
{
LogLevel
,
LogsMetaItem
,
LogsModel
,
LogRow
,
LogsStream
,
LogsStreamEntry
,
LogsStreamLabels
,
}
from
'app/core/logs_model'
;
import
{
DEFAULT_LIMIT
}
from
'./datasource'
;
/**
* Returns the log level of a log line.
* Parse the line for level words. If no level is found, it returns `LogLevel.none`.
*
* Example: `getLogLevel('WARN 1999-12-31 this is great') // LogLevel.warn`
*/
export
function
getLogLevel
(
line
:
string
):
LogLevel
{
if
(
!
line
)
{
return
undefined
;
return
LogLevel
.
none
;
}
let
level
:
LogLevel
;
Object
.
keys
(
LogLevel
).
forEach
(
key
=>
{
...
...
@@ -16,37 +31,149 @@ export function getLogLevel(line: string): LogLevel {
}
}
});
if
(
!
level
)
{
level
=
LogLevel
.
none
;
}
return
level
;
}
export
function
processEntry
(
entry
:
{
line
:
string
;
timestamp
:
string
},
stream
):
LogRow
{
/**
* Regexp to extract Prometheus-style labels
*/
const
labelRegexp
=
/
\b(\w
+
)(
!
?
=~
?)(
"
[^
"
\n]
*
?
"
)
/g
;
/**
* Returns a map of label keys to value from an input selector string.
*
* Example: `parseLabels('{job="foo", instance="bar"}) // {job: "foo", instance: "bar"}`
*/
export
function
parseLabels
(
labels
:
string
):
LogsStreamLabels
{
const
labelsByKey
:
LogsStreamLabels
=
{};
labels
.
replace
(
labelRegexp
,
(
_
,
key
,
operator
,
value
)
=>
{
labelsByKey
[
key
]
=
value
;
return
''
;
});
return
labelsByKey
;
}
/**
* Returns a map labels that are common to the given label sets.
*/
export
function
findCommonLabels
(
labelsSets
:
LogsStreamLabels
[]):
LogsStreamLabels
{
return
labelsSets
.
reduce
((
acc
,
labels
)
=>
{
if
(
!
labels
)
{
throw
new
Error
(
'Need parsed labels to find common labels.'
);
}
if
(
!
acc
)
{
// Initial set
acc
=
{
...
labels
};
}
else
{
// Remove incoming labels that are missing or not matching in value
Object
.
keys
(
labels
).
forEach
(
key
=>
{
if
(
acc
[
key
]
===
undefined
||
acc
[
key
]
!==
labels
[
key
])
{
delete
acc
[
key
];
}
});
// Remove common labels that are missing from incoming label set
Object
.
keys
(
acc
).
forEach
(
key
=>
{
if
(
labels
[
key
]
===
undefined
)
{
delete
acc
[
key
];
}
});
}
return
acc
;
},
undefined
);
}
/**
* Returns a map of labels that are in `labels`, but not in `commonLabels`.
*/
export
function
findUniqueLabels
(
labels
:
LogsStreamLabels
,
commonLabels
:
LogsStreamLabels
):
LogsStreamLabels
{
const
uncommonLabels
:
LogsStreamLabels
=
{
...
labels
};
Object
.
keys
(
commonLabels
).
forEach
(
key
=>
{
delete
uncommonLabels
[
key
];
});
return
uncommonLabels
;
}
/**
* Serializes the given labels to a string.
*/
export
function
formatLabels
(
labels
:
LogsStreamLabels
,
defaultValue
=
''
):
string
{
if
(
!
labels
||
Object
.
keys
(
labels
).
length
===
0
)
{
return
defaultValue
;
}
const
labelKeys
=
Object
.
keys
(
labels
).
sort
();
const
cleanSelector
=
labelKeys
.
map
(
key
=>
`
${
key
}
=
${
labels
[
key
]}
`
).
join
(
', '
);
return
[
'{'
,
cleanSelector
,
'}'
].
join
(
''
);
}
export
function
processEntry
(
entry
:
LogsStreamEntry
,
labels
:
string
,
uniqueLabels
:
string
,
search
:
string
):
LogRow
{
const
{
line
,
timestamp
}
=
entry
;
const
{
labels
}
=
stream
;
// Assumes unique-ness, needs nanosec precision for timestamp
const
key
=
`EK
${
timestamp
}${
labels
}
`
;
const
time
=
moment
(
timestamp
);
const
timeEpochMs
=
time
.
valueOf
();
const
timeFromNow
=
time
.
fromNow
();
const
timeLocal
=
time
.
format
(
'YYYY-MM-DD HH:mm:ss'
);
const
logLevel
=
getLogLevel
(
line
);
return
{
key
,
labels
,
logLevel
,
timeFromNow
,
timeEpochMs
,
timeLocal
,
uniqueLabels
,
entry
:
line
,
searchWords
:
[
stream
.
search
],
searchWords
:
search
?
[
search
]
:
[
],
timestamp
:
timestamp
,
};
}
export
function
processStreams
(
streams
,
limit
?:
number
):
LogsModel
{
const
combinedEntries
=
streams
.
reduce
((
acc
,
stream
)
=>
{
return
[...
acc
,
...
stream
.
entries
.
map
(
entry
=>
processEntry
(
entry
,
stream
))];
},
[]);
const
sortedEntries
=
_
.
chain
(
combinedEntries
)
export
function
mergeStreamsToLogs
(
streams
:
LogsStream
[],
limit
=
DEFAULT_LIMIT
):
LogsModel
{
// Find unique labels for each stream
streams
=
streams
.
map
(
stream
=>
({
...
stream
,
parsedLabels
:
parseLabels
(
stream
.
labels
),
}));
const
commonLabels
=
findCommonLabels
(
streams
.
map
(
model
=>
model
.
parsedLabels
));
streams
=
streams
.
map
(
stream
=>
({
...
stream
,
uniqueLabels
:
formatLabels
(
findUniqueLabels
(
stream
.
parsedLabels
,
commonLabels
)),
}));
// Merge stream entries into single list of log rows
const
sortedRows
:
LogRow
[]
=
_
.
chain
(
streams
)
.
reduce
(
(
acc
:
LogRow
[],
stream
:
LogsStream
)
=>
[
...
acc
,
...
stream
.
entries
.
map
(
entry
=>
processEntry
(
entry
,
stream
.
labels
,
stream
.
uniqueLabels
,
stream
.
search
)),
],
[]
)
.
sortBy
(
'timestamp'
)
.
reverse
()
.
slice
(
0
,
limit
||
combinedEntries
.
length
)
.
value
();
return
{
rows
:
sortedEntries
};
// Meta data to display in status
const
meta
:
LogsMetaItem
[]
=
[];
if
(
_
.
size
(
commonLabels
)
>
0
)
{
meta
.
push
({
label
:
'Common labels'
,
value
:
formatLabels
(
commonLabels
),
});
}
if
(
limit
)
{
meta
.
push
({
label
:
'Limit'
,
value
:
`
${
limit
}
(
${
sortedRows
.
length
}
returned)`
,
});
}
return
{
meta
,
rows
:
sortedRows
,
};
}
public/sass/components/_gf-form.scss
View file @
20307828
...
...
@@ -116,6 +116,11 @@ $input-border: 1px solid $input-border-color;
color
:
$critical
;
}
&
--small
{
padding
:
(
$input-padding-y
/
2
)
(
$input-padding-x
/
2
);
font-size
:
$font-size-xs
;
}
&
:disabled
{
color
:
$text-color-weak
;
}
...
...
public/sass/components/_switch.scss
View file @
20307828
...
...
@@ -41,7 +41,6 @@
bottom
:
0
;
right
:
0
;
color
:
#fff
;
font-size
:
$font-size-sm
;
text-align
:
center
;
font-size
:
150%
;
display
:
flex
;
...
...
@@ -91,6 +90,20 @@
transform
:
rotateY
(
0
);
}
&
--small
{
max-width
:
2rem
;
min-width
:
1
.5rem
;
input
+
label
{
height
:
25px
;
}
input
+
label
:
:
before
,
input
+
label
::
after
{
font-size
:
$font-size-sm
;
}
}
&
--table-cell
{
margin-bottom
:
0
;
margin-right
:
0
;
...
...
public/sass/pages/_explore.scss
View file @
20307828
...
...
@@ -214,7 +214,42 @@
display
:
grid
;
grid-column-gap
:
1rem
;
grid-row-gap
:
0
.1rem
;
grid-template-columns
:
4px
minmax
(
100px
,
max-content
)
1fr
;
grid-template-columns
:
4px
minmax
(
100px
,
max-content
)
minmax
(
100px
,
25%
)
1fr
;
font-family
:
$font-family-monospace
;
font-size
:
12px
;
}
.logs-controls
{
display
:
flex
;
>
*
{
margin-right
:
1em
;
}
}
.logs-options
,
.logs-graph
{
margin-bottom
:
$panel-margin
;
}
.logs-meta
{
flex
:
1
;
color
:
$text-color-weak
;
padding
:
2px
0
;
}
.logs-meta-item
{
display
:
inline-block
;
margin-right
:
1em
;
}
.logs-meta-item__label
{
margin-right
:
0
.5em
;
font-size
:
0
.9em
;
font-weight
:
500
;
}
.logs-meta-item__value
{
font-family
:
$font-family-monospace
;
}
...
...
@@ -235,18 +270,26 @@
opacity
:
0
.8
;
}
.logs-row-level-crit
,
.logs-row-level-crit
{
background-color
:
#705da0
;
}
.logs-row-level-error
,
.logs-row-level-err
{
background-color
:
$red
;
background-color
:
#e24d42
;
}
.logs-row-level-warn
{
background-color
:
$orange
;
background-color
:
#eab839
;
}
.logs-row-level-info
{
background-color
:
$green
;
background-color
:
#7eb26d
;
}
.logs-row-level-trace
,
.logs-row-level-debug
{
background-color
:
#1f78c1
;
}
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment