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
e99c1b48
Unverified
Commit
e99c1b48
authored
Oct 31, 2018
by
David
Committed by
GitHub
Oct 31, 2018
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13904 from grafana/davkal/explore-logging
Explore: repair logging after recent Explore restructuring
parents
3d452e5a
037167ff
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
598 additions
and
126 deletions
+598
-126
public/app/core/logs_model.ts
+14
-0
public/app/features/explore/Explore.tsx
+22
-19
public/app/features/explore/Graph.tsx
+1
-1
public/app/features/explore/Logs.tsx
+21
-25
public/app/features/explore/QueryRows.tsx
+1
-4
public/app/features/explore/TimePicker.test.tsx
+4
-4
public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx
+29
-0
public/app/plugins/datasource/logging/components/LoggingQueryField.tsx
+205
-0
public/app/plugins/datasource/logging/components/LoggingStartPage.tsx
+60
-0
public/app/plugins/datasource/logging/datasource.ts
+6
-1
public/app/plugins/datasource/logging/language_provider.ts
+211
-0
public/app/plugins/datasource/logging/module.ts
+9
-1
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
+5
-31
public/app/plugins/datasource/prometheus/language_provider.ts
+1
-37
public/sass/pages/_explore.scss
+9
-3
No files found.
public/app/core/logs_model.ts
View file @
e99c1b48
import
_
from
'lodash'
;
export
enum
LogLevel
{
crit
=
'crit'
,
warn
=
'warn'
,
...
...
@@ -27,3 +29,15 @@ export interface LogRow {
export
interface
LogsModel
{
rows
:
LogRow
[];
}
export
function
mergeStreams
(
streams
:
LogsModel
[],
limit
?:
number
):
LogsModel
{
const
combinedEntries
=
streams
.
reduce
((
acc
,
stream
)
=>
{
return
[...
acc
,
...
stream
.
rows
];
},
[]);
const
sortedEntries
=
_
.
chain
(
combinedEntries
)
.
sortBy
(
'timestamp'
)
.
reverse
()
.
slice
(
0
,
limit
||
combinedEntries
.
length
)
.
value
();
return
{
rows
:
sortedEntries
};
}
public/app/features/explore/Explore.tsx
View file @
e99c1b48
...
...
@@ -25,6 +25,7 @@ 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
;
...
...
@@ -769,11 +770,12 @@ 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
=
_
.
flatten
(
const
logsResult
=
mergeStreams
(
queryTransactions
.
filter
(
qt
=>
qt
.
resultType
===
'Logs'
&&
qt
.
done
&&
qt
.
result
).
map
(
qt
=>
qt
.
result
)
);
const
loading
=
queryTransactions
.
some
(
qt
=>
!
qt
.
done
);
const
showStartPages
=
StartPage
&&
queryTransactions
.
length
===
0
;
const
viewModeCount
=
[
supportsGraph
,
supportsLogs
,
supportsTable
].
filter
(
m
=>
m
).
length
;
return
(
<
div
className=
{
exploreClass
}
ref=
{
this
.
getRef
}
>
...
...
@@ -858,7 +860,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onClickHintFix=
{
this
.
onModifyQueries
}
onExecuteQuery=
{
this
.
onSubmit
}
onRemoveQueryRow=
{
this
.
onRemoveQueryRow
}
supportsLogs=
{
supportsLogs
}
transactions=
{
queryTransactions
}
/>
<
main
className=
"m-t-2"
>
...
...
@@ -866,23 +867,25 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
{
showStartPages
&&
<
StartPage
onClickQuery=
{
this
.
onClickQuery
}
/>
}
{
!
showStartPages
&&
(
<>
<
div
className=
"result-options"
>
{
supportsGraph
?
(
<
button
className=
{
`btn toggle-btn ${graphButtonActive}`
}
onClick=
{
this
.
onClickGraphButton
}
>
Graph
</
button
>
)
:
null
}
{
supportsTable
?
(
<
button
className=
{
`btn toggle-btn ${tableButtonActive}`
}
onClick=
{
this
.
onClickTableButton
}
>
Table
</
button
>
)
:
null
}
{
supportsLogs
?
(
<
button
className=
{
`btn toggle-btn ${logsButtonActive}`
}
onClick=
{
this
.
onClickLogsButton
}
>
Logs
</
button
>
)
:
null
}
</
div
>
{
viewModeCount
>
1
&&
(
<
div
className=
"result-options"
>
{
supportsGraph
?
(
<
button
className=
{
`btn toggle-btn ${graphButtonActive}`
}
onClick=
{
this
.
onClickGraphButton
}
>
Graph
</
button
>
)
:
null
}
{
supportsTable
?
(
<
button
className=
{
`btn toggle-btn ${tableButtonActive}`
}
onClick=
{
this
.
onClickTableButton
}
>
Table
</
button
>
)
:
null
}
{
supportsLogs
?
(
<
button
className=
{
`btn toggle-btn ${logsButtonActive}`
}
onClick=
{
this
.
onClickLogsButton
}
>
Logs
</
button
>
)
:
null
}
</
div
>
)
}
{
supportsGraph
&&
showingGraph
&&
(
...
...
public/app/features/explore/Graph.tsx
View file @
e99c1b48
...
...
@@ -169,7 +169,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
return
(
<
div
className=
"panel-container"
>
{
loading
&&
<
div
className=
"explore-
graph
__loader"
/>
}
{
loading
&&
<
div
className=
"explore-
panel
__loader"
/>
}
{
this
.
props
.
data
&&
this
.
props
.
data
.
length
>
MAX_NUMBER_OF_TIME_SERIES
&&
!
this
.
state
.
showAllTimeSeries
&&
(
...
...
public/app/features/explore/Logs.tsx
View file @
e99c1b48
...
...
@@ -10,37 +10,33 @@ interface LogsProps {
loading
:
boolean
;
}
const
EXAMPLE_QUERY
=
'{job="default/prometheus"}'
;
export
default
class
Logs
extends
PureComponent
<
LogsProps
,
{}
>
{
render
()
{
const
{
className
=
''
,
data
}
=
this
.
props
;
const
{
className
=
''
,
data
,
loading
=
false
}
=
this
.
props
;
const
hasData
=
data
&&
data
.
rows
&&
data
.
rows
.
length
>
0
;
return
(
<
div
className=
{
`${className} logs`
}
>
{
hasData
?
(
<
div
className=
"logs-entries panel-container"
>
{
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
>
<
div
>
<
Highlighter
textToHighlight=
{
row
.
entry
}
searchWords=
{
row
.
searchWords
}
findChunks=
{
findHighlightChunksInText
}
highlightClassName=
"logs-row-match-highlight"
/>
</
div
>
</
Fragment
>
))
}
</
div
>
)
:
null
}
{
!
hasData
?
(
<
div
className=
"panel-container"
>
Enter a query like
<
code
>
{
EXAMPLE_QUERY
}
</
code
>
<
div
className=
"panel-container"
>
{
loading
&&
<
div
className=
"explore-panel__loader"
/>
}
<
div
className=
"logs-entries"
>
{
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
>
<
div
>
<
Highlighter
textToHighlight=
{
row
.
entry
}
searchWords=
{
row
.
searchWords
}
findChunks=
{
findHighlightChunksInText
}
highlightClassName=
"logs-row-match-highlight"
/>
</
div
>
</
Fragment
>
))
}
</
div
>
)
:
null
}
{
!
loading
&&
!
hasData
&&
'No data was returned.'
}
</
div
>
</
div
>
);
}
...
...
public/app/features/explore/QueryRows.tsx
View file @
e99c1b48
...
...
@@ -26,8 +26,6 @@ interface QueryRowCommonProps {
className
?:
string
;
datasource
:
DataSource
;
history
:
HistoryItem
[];
// Temporarily
supportsLogs
?:
boolean
;
transactions
:
QueryTransaction
[];
}
...
...
@@ -78,7 +76,7 @@ class QueryRow extends PureComponent<QueryRowProps> {
};
render
()
{
const
{
datasource
,
history
,
query
,
supportsLogs
,
transactions
}
=
this
.
props
;
const
{
datasource
,
history
,
query
,
transactions
}
=
this
.
props
;
const
transactionWithError
=
transactions
.
find
(
t
=>
t
.
error
!==
undefined
);
const
hint
=
getFirstHintFromTransactions
(
transactions
);
const
queryError
=
transactionWithError
?
transactionWithError
.
error
:
null
;
...
...
@@ -98,7 +96,6 @@ class QueryRow extends PureComponent<QueryRowProps> {
onClickHintFix=
{
this
.
onClickHintFix
}
onPressEnter=
{
this
.
onPressEnter
}
onQueryChange=
{
this
.
onChangeQuery
}
supportsLogs=
{
supportsLogs
}
/>
</
div
>
<
div
className=
"query-row-tools"
>
...
...
public/app/features/explore/TimePicker.test.tsx
View file @
e99c1b48
...
...
@@ -33,8 +33,8 @@ describe('<TimePicker />', () => {
to
:
'1000'
,
};
const
rangeString
=
rangeUtil
.
describeTimeRange
({
from
:
parseTime
(
range
.
from
),
to
:
parseTime
(
range
.
to
),
from
:
parseTime
(
range
.
from
,
true
),
to
:
parseTime
(
range
.
to
,
true
),
});
const
wrapper
=
shallow
(<
TimePicker
range=
{
range
}
isUtc
isOpen
/>);
expect
(
wrapper
.
state
(
'fromRaw'
)).
toBe
(
'1970-01-01 00:00:00'
);
...
...
@@ -50,8 +50,8 @@ describe('<TimePicker />', () => {
to
:
'4000'
,
};
const
rangeString
=
rangeUtil
.
describeTimeRange
({
from
:
parseTime
(
range
.
from
),
to
:
parseTime
(
range
.
to
),
from
:
parseTime
(
range
.
from
,
true
),
to
:
parseTime
(
range
.
to
,
true
),
});
const
onChangeTime
=
sinon
.
spy
();
...
...
public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx
0 → 100644
View file @
e99c1b48
import
React
from
'react'
;
const
CHEAT_SHEET_ITEMS
=
[
{
title
:
'Logs From a Job'
,
expression
:
'{job="default/prometheus"}'
,
label
:
'Returns all log lines emitted by instances of this job.'
,
},
{
title
:
'Search For Text'
,
expression
:
'{app="cassandra"} Maximum memory usage'
,
label
:
'Returns all log lines for the selector and highlights the given text in the results.'
,
},
];
export
default
(
props
:
any
)
=>
(
<
div
>
<
h1
>
Logging Cheat Sheet
</
h1
>
{
CHEAT_SHEET_ITEMS
.
map
(
item
=>
(
<
div
className=
"cheat-sheet-item"
key=
{
item
.
expression
}
>
<
div
className=
"cheat-sheet-item__title"
>
{
item
.
title
}
</
div
>
<
div
className=
"cheat-sheet-item__expression"
onClick=
{
e
=>
props
.
onClickQuery
(
item
.
expression
)
}
>
<
code
>
{
item
.
expression
}
</
code
>
</
div
>
<
div
className=
"cheat-sheet-item__label"
>
{
item
.
label
}
</
div
>
</
div
>
))
}
</
div
>
);
public/app/plugins/datasource/logging/components/LoggingQueryField.tsx
0 → 100644
View file @
e99c1b48
import
_
from
'lodash'
;
import
React
from
'react'
;
import
Cascader
from
'rc-cascader'
;
import
PluginPrism
from
'slate-prism'
;
import
Prism
from
'prismjs'
;
import
{
TypeaheadOutput
}
from
'app/types/explore'
;
// dom also includes Element polyfills
import
{
getNextCharacter
,
getPreviousCousin
}
from
'app/features/explore/utils/dom'
;
import
BracesPlugin
from
'app/features/explore/slate-plugins/braces'
;
import
RunnerPlugin
from
'app/features/explore/slate-plugins/runner'
;
import
TypeaheadField
,
{
TypeaheadInput
,
QueryFieldState
}
from
'app/features/explore/QueryField'
;
const
PRISM_SYNTAX
=
'promql'
;
export
function
willApplySuggestion
(
suggestion
:
string
,
{
typeaheadContext
,
typeaheadText
}:
QueryFieldState
):
string
{
// Modify suggestion based on context
switch
(
typeaheadContext
)
{
case
'context-labels'
:
{
const
nextChar
=
getNextCharacter
();
if
(
!
nextChar
||
nextChar
===
'}'
||
nextChar
===
','
)
{
suggestion
+=
'='
;
}
break
;
}
case
'context-label-values'
:
{
// Always add quotes and remove existing ones instead
if
(
!
typeaheadText
.
match
(
/^
(
!
?
=~
?
"|"
)
/
))
{
suggestion
=
`"
${
suggestion
}
`
;
}
if
(
getNextCharacter
()
!==
'"'
)
{
suggestion
=
`
${
suggestion
}
"`
;
}
break
;
}
default
:
}
return
suggestion
;
}
interface
CascaderOption
{
label
:
string
;
value
:
string
;
children
?:
CascaderOption
[];
disabled
?:
boolean
;
}
interface
LoggingQueryFieldProps
{
datasource
:
any
;
error
?:
string
|
JSX
.
Element
;
hint
?:
any
;
history
?:
any
[];
initialQuery
?:
string
|
null
;
onClickHintFix
?:
(
action
:
any
)
=>
void
;
onPressEnter
?:
()
=>
void
;
onQueryChange
?:
(
value
:
string
,
override
?:
boolean
)
=>
void
;
}
interface
LoggingQueryFieldState
{
logLabelOptions
:
any
[];
syntaxLoaded
:
boolean
;
}
class
LoggingQueryField
extends
React
.
PureComponent
<
LoggingQueryFieldProps
,
LoggingQueryFieldState
>
{
plugins
:
any
[];
languageProvider
:
any
;
constructor
(
props
:
LoggingQueryFieldProps
,
context
)
{
super
(
props
,
context
);
if
(
props
.
datasource
.
languageProvider
)
{
this
.
languageProvider
=
props
.
datasource
.
languageProvider
;
}
this
.
plugins
=
[
BracesPlugin
(),
RunnerPlugin
({
handler
:
props
.
onPressEnter
}),
PluginPrism
({
onlyIn
:
node
=>
node
.
type
===
'code_block'
,
getSyntax
:
node
=>
'promql'
,
}),
];
this
.
state
=
{
logLabelOptions
:
[],
syntaxLoaded
:
false
,
};
}
componentDidMount
()
{
if
(
this
.
languageProvider
)
{
this
.
languageProvider
.
start
().
then
(()
=>
this
.
onReceiveMetrics
());
}
}
onChangeLogLabels
=
(
values
:
string
[],
selectedOptions
:
CascaderOption
[])
=>
{
let
query
;
if
(
selectedOptions
.
length
===
1
)
{
if
(
selectedOptions
[
0
].
children
.
length
===
0
)
{
query
=
selectedOptions
[
0
].
value
;
}
else
{
// Ignore click on group
return
;
}
}
else
{
const
key
=
selectedOptions
[
0
].
value
;
const
value
=
selectedOptions
[
1
].
value
;
query
=
`{
${
key
}
="
${
value
}
"}`
;
}
this
.
onChangeQuery
(
query
,
true
);
};
onChangeQuery
=
(
value
:
string
,
override
?:
boolean
)
=>
{
// Send text change to parent
const
{
onQueryChange
}
=
this
.
props
;
if
(
onQueryChange
)
{
onQueryChange
(
value
,
override
);
}
};
onClickHintFix
=
()
=>
{
const
{
hint
,
onClickHintFix
}
=
this
.
props
;
if
(
onClickHintFix
&&
hint
&&
hint
.
fix
)
{
onClickHintFix
(
hint
.
fix
.
action
);
}
};
onReceiveMetrics
=
()
=>
{
Prism
.
languages
[
PRISM_SYNTAX
]
=
this
.
languageProvider
.
getSyntax
();
const
{
logLabelOptions
}
=
this
.
languageProvider
;
this
.
setState
({
logLabelOptions
,
syntaxLoaded
:
true
,
});
};
onTypeahead
=
(
typeahead
:
TypeaheadInput
):
TypeaheadOutput
=>
{
if
(
!
this
.
languageProvider
)
{
return
{
suggestions
:
[]
};
}
const
{
history
}
=
this
.
props
;
const
{
prefix
,
text
,
value
,
wrapperNode
}
=
typeahead
;
// Get DOM-dependent context
const
wrapperClasses
=
Array
.
from
(
wrapperNode
.
classList
);
const
labelKeyNode
=
getPreviousCousin
(
wrapperNode
,
'.attr-name'
);
const
labelKey
=
labelKeyNode
&&
labelKeyNode
.
textContent
;
const
nextChar
=
getNextCharacter
();
const
result
=
this
.
languageProvider
.
provideCompletionItems
(
{
text
,
value
,
prefix
,
wrapperClasses
,
labelKey
},
{
history
}
);
console
.
log
(
'handleTypeahead'
,
wrapperClasses
,
text
,
prefix
,
nextChar
,
labelKey
,
result
.
context
);
return
result
;
};
render
()
{
const
{
error
,
hint
,
initialQuery
}
=
this
.
props
;
const
{
logLabelOptions
,
syntaxLoaded
}
=
this
.
state
;
const
cleanText
=
this
.
languageProvider
?
this
.
languageProvider
.
cleanText
:
undefined
;
return
(
<
div
className=
"prom-query-field"
>
<
div
className=
"prom-query-field-tools"
>
<
Cascader
options=
{
logLabelOptions
}
onChange=
{
this
.
onChangeLogLabels
}
>
<
button
className=
"btn navbar-button navbar-button--tight"
>
Log labels
</
button
>
</
Cascader
>
</
div
>
<
div
className=
"prom-query-field-wrapper"
>
<
TypeaheadField
additionalPlugins=
{
this
.
plugins
}
cleanText=
{
cleanText
}
initialValue=
{
initialQuery
}
onTypeahead=
{
this
.
onTypeahead
}
onWillApplySuggestion=
{
willApplySuggestion
}
onValueChanged=
{
this
.
onChangeQuery
}
placeholder=
"Enter a PromQL query"
portalOrigin=
"prometheus"
syntaxLoaded=
{
syntaxLoaded
}
/>
{
error
?
<
div
className=
"prom-query-field-info text-error"
>
{
error
}
</
div
>
:
null
}
{
hint
?
(
<
div
className=
"prom-query-field-info text-warning"
>
{
hint
.
label
}{
' '
}
{
hint
.
fix
?
(
<
a
className=
"text-link muted"
onClick=
{
this
.
onClickHintFix
}
>
{
hint
.
fix
.
label
}
</
a
>
)
:
null
}
</
div
>
)
:
null
}
</
div
>
</
div
>
);
}
}
export
default
LoggingQueryField
;
public/app/plugins/datasource/logging/components/LoggingStartPage.tsx
0 → 100644
View file @
e99c1b48
import
React
,
{
PureComponent
}
from
'react'
;
import
classNames
from
'classnames'
;
import
LoggingCheatSheet
from
'./LoggingCheatSheet'
;
const
TAB_MENU_ITEMS
=
[
{
text
:
'Start'
,
id
:
'start'
,
icon
:
'fa fa-rocket'
,
},
];
export
default
class
LoggingStartPage
extends
PureComponent
<
any
,
{
active
:
string
}
>
{
state
=
{
active
:
'start'
,
};
onClickTab
=
active
=>
{
this
.
setState
({
active
});
};
render
()
{
const
{
active
}
=
this
.
state
;
const
customCss
=
''
;
return
(
<
div
style=
{
{
margin
:
'45px 0'
,
border
:
'1px solid #ddd'
,
borderRadius
:
5
}
}
>
<
div
className=
"page-header-canvas"
>
<
div
className=
"page-container"
>
<
div
className=
"page-header"
>
<
nav
>
<
ul
className=
{
`gf-tabs ${customCss}`
}
>
{
TAB_MENU_ITEMS
.
map
((
tab
,
idx
)
=>
{
const
tabClasses
=
classNames
({
'gf-tabs-link'
:
true
,
active
:
tab
.
id
===
active
,
});
return
(
<
li
className=
"gf-tabs-item"
key=
{
tab
.
id
}
>
<
a
className=
{
tabClasses
}
onClick=
{
()
=>
this
.
onClickTab
(
tab
.
id
)
}
>
<
i
className=
{
tab
.
icon
}
/>
{
tab
.
text
}
</
a
>
</
li
>
);
})
}
</
ul
>
</
nav
>
</
div
>
</
div
>
</
div
>
<
div
className=
"page-container page-body"
>
{
active
===
'start'
&&
<
LoggingCheatSheet
onClickQuery=
{
this
.
props
.
onClickQuery
}
/>
}
</
div
>
</
div
>
);
}
}
public/app/plugins/datasource/logging/datasource.ts
View file @
e99c1b48
...
...
@@ -2,6 +2,7 @@ import _ from 'lodash';
import
*
as
dateMath
from
'app/core/utils/datemath'
;
import
LanguageProvider
from
'./language_provider'
;
import
{
processStreams
}
from
'./result_transformer'
;
const
DEFAULT_LIMIT
=
100
;
...
...
@@ -48,8 +49,12 @@ function serializeParams(data: any) {
}
export
default
class
LoggingDatasource
{
languageProvider
:
LanguageProvider
;
/** @ngInject */
constructor
(
private
instanceSettings
,
private
backendSrv
,
private
templateSrv
)
{}
constructor
(
private
instanceSettings
,
private
backendSrv
,
private
templateSrv
)
{
this
.
languageProvider
=
new
LanguageProvider
(
this
);
}
_request
(
apiUrl
:
string
,
data
?,
options
?:
any
)
{
const
baseUrl
=
this
.
instanceSettings
.
url
;
...
...
public/app/plugins/datasource/logging/language_provider.ts
0 → 100644
View file @
e99c1b48
import
_
from
'lodash'
;
import
moment
from
'moment'
;
import
{
CompletionItem
,
CompletionItemGroup
,
LanguageProvider
,
TypeaheadInput
,
TypeaheadOutput
,
}
from
'app/types/explore'
;
import
{
parseSelector
}
from
'app/plugins/datasource/prometheus/language_utils'
;
import
PromqlSyntax
from
'app/plugins/datasource/prometheus/promql'
;
const
DEFAULT_KEYS
=
[
'job'
,
'instance'
];
const
EMPTY_SELECTOR
=
'{}'
;
const
HISTORY_ITEM_COUNT
=
5
;
const
HISTORY_COUNT_CUTOFF
=
1000
*
60
*
60
*
24
;
// 24h
const
wrapLabel
=
(
label
:
string
)
=>
({
label
});
export
function
addHistoryMetadata
(
item
:
CompletionItem
,
history
:
any
[]):
CompletionItem
{
const
cutoffTs
=
Date
.
now
()
-
HISTORY_COUNT_CUTOFF
;
const
historyForItem
=
history
.
filter
(
h
=>
h
.
ts
>
cutoffTs
&&
h
.
query
===
item
.
label
);
const
count
=
historyForItem
.
length
;
const
recent
=
historyForItem
[
0
];
let
hint
=
`Queried
${
count
}
times in the last 24h.`
;
if
(
recent
)
{
const
lastQueried
=
moment
(
recent
.
ts
).
fromNow
();
hint
=
`
${
hint
}
Last queried
${
lastQueried
}
.`
;
}
return
{
...
item
,
documentation
:
hint
,
};
}
export
default
class
LoggingLanguageProvider
extends
LanguageProvider
{
labelKeys
?:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelValues
?:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
logLabelOptions
:
any
[];
started
:
boolean
;
constructor
(
datasource
:
any
,
initialValues
?:
any
)
{
super
();
this
.
datasource
=
datasource
;
this
.
labelKeys
=
{};
this
.
labelValues
=
{};
this
.
started
=
false
;
Object
.
assign
(
this
,
initialValues
);
}
// Strip syntax chars
cleanText
=
s
=>
s
.
replace
(
/
[
{}[
\]
="(),!~+
\-
*
/
^%
]
/g
,
''
).
trim
();
getSyntax
()
{
return
PromqlSyntax
;
}
request
=
url
=>
{
return
this
.
datasource
.
metadataRequest
(
url
);
};
start
=
()
=>
{
if
(
!
this
.
started
)
{
this
.
started
=
true
;
return
Promise
.
all
([
this
.
fetchLogLabels
()]);
}
return
Promise
.
resolve
([]);
};
// Keep this DOM-free for testing
provideCompletionItems
({
prefix
,
wrapperClasses
,
text
}:
TypeaheadInput
,
context
?:
any
):
TypeaheadOutput
{
// Syntax spans have 3 classes by default. More indicate a recognized token
const
tokenRecognized
=
wrapperClasses
.
length
>
3
;
// Determine candidates by CSS context
if
(
_
.
includes
(
wrapperClasses
,
'context-labels'
))
{
// Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|}
return
this
.
getLabelCompletionItems
.
apply
(
this
,
arguments
);
}
else
if
(
// Show default suggestions in a couple of scenarios
(
prefix
&&
!
tokenRecognized
)
||
// Non-empty prefix, but not inside known token
(
prefix
===
''
&&
!
text
.
match
(
/^
[\]
})
\s]
+$/
))
||
// Empty prefix, but not following a closing brace
text
.
match
(
/
[
+
\-
*
/
^%
]
/
)
// Anything after binary operator
)
{
return
this
.
getEmptyCompletionItems
(
context
||
{});
}
return
{
suggestions
:
[],
};
}
getEmptyCompletionItems
(
context
:
any
):
TypeaheadOutput
{
const
{
history
}
=
context
;
const
suggestions
:
CompletionItemGroup
[]
=
[];
if
(
history
&&
history
.
length
>
0
)
{
const
historyItems
=
_
.
chain
(
history
)
.
uniqBy
(
'query'
)
.
take
(
HISTORY_ITEM_COUNT
)
.
map
(
h
=>
h
.
query
)
.
map
(
wrapLabel
)
.
map
(
item
=>
addHistoryMetadata
(
item
,
history
))
.
value
();
suggestions
.
push
({
prefixMatch
:
true
,
skipSort
:
true
,
label
:
'History'
,
items
:
historyItems
,
});
}
return
{
suggestions
};
}
getLabelCompletionItems
({
text
,
wrapperClasses
,
labelKey
,
value
}:
TypeaheadInput
):
TypeaheadOutput
{
let
context
:
string
;
const
suggestions
:
CompletionItemGroup
[]
=
[];
const
line
=
value
.
anchorBlock
.
getText
();
const
cursorOffset
:
number
=
value
.
anchorOffset
;
// Get normalized selector
let
selector
;
let
parsedSelector
;
try
{
parsedSelector
=
parseSelector
(
line
,
cursorOffset
);
selector
=
parsedSelector
.
selector
;
}
catch
{
selector
=
EMPTY_SELECTOR
;
}
const
containsMetric
=
selector
.
indexOf
(
'__name__='
)
>
-
1
;
const
existingKeys
=
parsedSelector
?
parsedSelector
.
labelKeys
:
[];
if
((
text
&&
text
.
match
(
/^!
?
=~
?
/
))
||
_
.
includes
(
wrapperClasses
,
'attr-value'
))
{
// Label values
if
(
labelKey
&&
this
.
labelValues
[
selector
]
&&
this
.
labelValues
[
selector
][
labelKey
])
{
const
labelValues
=
this
.
labelValues
[
selector
][
labelKey
];
context
=
'context-label-values'
;
suggestions
.
push
({
label
:
`Label values for "
${
labelKey
}
"`
,
items
:
labelValues
.
map
(
wrapLabel
),
});
}
}
else
{
// Label keys
const
labelKeys
=
this
.
labelKeys
[
selector
]
||
(
containsMetric
?
null
:
DEFAULT_KEYS
);
if
(
labelKeys
)
{
const
possibleKeys
=
_
.
difference
(
labelKeys
,
existingKeys
);
if
(
possibleKeys
.
length
>
0
)
{
context
=
'context-labels'
;
suggestions
.
push
({
label
:
`Labels`
,
items
:
possibleKeys
.
map
(
wrapLabel
)
});
}
}
}
return
{
context
,
suggestions
};
}
async
fetchLogLabels
()
{
const
url
=
'/api/prom/label'
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
labelKeys
=
body
.
data
.
slice
().
sort
();
const
labelKeysBySelector
=
{
...
this
.
labelKeys
,
[
EMPTY_SELECTOR
]:
labelKeys
,
};
const
labelValuesByKey
=
{};
this
.
logLabelOptions
=
[];
for
(
const
key
of
labelKeys
)
{
const
valuesUrl
=
`/api/prom/label/
${
key
}
/values`
;
const
res
=
await
this
.
request
(
valuesUrl
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
values
=
body
.
data
.
slice
().
sort
();
labelValuesByKey
[
key
]
=
values
;
this
.
logLabelOptions
.
push
({
label
:
key
,
value
:
key
,
children
:
values
.
map
(
value
=>
({
label
:
value
,
value
})),
});
}
this
.
labelValues
=
{
[
EMPTY_SELECTOR
]:
labelValuesByKey
};
this
.
labelKeys
=
labelKeysBySelector
;
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchLabelValues
(
key
:
string
)
{
const
url
=
`/api/prom/label/
${
key
}
/values`
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
exisingValues
=
this
.
labelValues
[
EMPTY_SELECTOR
];
const
values
=
{
...
exisingValues
,
[
key
]:
body
.
data
,
};
this
.
labelValues
=
{
...
this
.
labelValues
,
[
EMPTY_SELECTOR
]:
values
,
};
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
}
public/app/plugins/datasource/logging/module.ts
View file @
e99c1b48
import
Datasource
from
'./datasource'
;
import
LoggingStartPage
from
'./components/LoggingStartPage'
;
import
LoggingQueryField
from
'./components/LoggingQueryField'
;
export
class
LoggingConfigCtrl
{
static
templateUrl
=
'partials/config.html'
;
}
export
{
Datasource
,
LoggingConfigCtrl
as
ConfigCtrl
};
export
{
Datasource
,
LoggingConfigCtrl
as
ConfigCtrl
,
LoggingQueryField
as
ExploreQueryField
,
LoggingStartPage
as
ExploreStartPage
,
};
public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
View file @
e99c1b48
...
...
@@ -94,11 +94,9 @@ interface PromQueryFieldProps {
onClickHintFix
?:
(
action
:
any
)
=>
void
;
onPressEnter
?:
()
=>
void
;
onQueryChange
?:
(
value
:
string
,
override
?:
boolean
)
=>
void
;
supportsLogs
?:
boolean
;
// To be removed after Logging gets its own query field
}
interface
PromQueryFieldState
{
logLabelOptions
:
any
[];
metricsOptions
:
any
[];
metricsByPrefix
:
CascaderOption
[];
syntaxLoaded
:
boolean
;
...
...
@@ -125,7 +123,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
];
this
.
state
=
{
logLabelOptions
:
[],
metricsByPrefix
:
[],
metricsOptions
:
[],
syntaxLoaded
:
false
,
...
...
@@ -138,23 +135,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
}
}
onChangeLogLabels
=
(
values
:
string
[],
selectedOptions
:
CascaderOption
[])
=>
{
let
query
;
if
(
selectedOptions
.
length
===
1
)
{
if
(
selectedOptions
[
0
].
children
.
length
===
0
)
{
query
=
selectedOptions
[
0
].
value
;
}
else
{
// Ignore click on group
return
;
}
}
else
{
const
key
=
selectedOptions
[
0
].
value
;
const
value
=
selectedOptions
[
1
].
value
;
query
=
`{
${
key
}
="
${
value
}
"}`
;
}
this
.
onChangeQuery
(
query
,
true
);
};
onChangeMetrics
=
(
values
:
string
[],
selectedOptions
:
CascaderOption
[])
=>
{
let
query
;
if
(
selectedOptions
.
length
===
1
)
{
...
...
@@ -239,22 +219,16 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
};
render
()
{
const
{
error
,
hint
,
initialQuery
,
supportsLogs
}
=
this
.
props
;
const
{
logLabelOptions
,
metricsOptions
,
syntaxLoaded
}
=
this
.
state
;
const
{
error
,
hint
,
initialQuery
}
=
this
.
props
;
const
{
metricsOptions
,
syntaxLoaded
}
=
this
.
state
;
const
cleanText
=
this
.
languageProvider
?
this
.
languageProvider
.
cleanText
:
undefined
;
return
(
<
div
className=
"prom-query-field"
>
<
div
className=
"prom-query-field-tools"
>
{
supportsLogs
?
(
<
Cascader
options=
{
logLabelOptions
}
onChange=
{
this
.
onChangeLogLabels
}
>
<
button
className=
"btn navbar-button navbar-button--tight"
>
Log labels
</
button
>
</
Cascader
>
)
:
(
<
Cascader
options=
{
metricsOptions
}
onChange=
{
this
.
onChangeMetrics
}
>
<
button
className=
"btn navbar-button navbar-button--tight"
>
Metrics
</
button
>
</
Cascader
>
)
}
<
Cascader
options=
{
metricsOptions
}
onChange=
{
this
.
onChangeMetrics
}
>
<
button
className=
"btn navbar-button navbar-button--tight"
>
Metrics
</
button
>
</
Cascader
>
</
div
>
<
div
className=
"prom-query-field-wrapper"
>
<
TypeaheadField
...
...
public/app/plugins/datasource/prometheus/language_provider.ts
View file @
e99c1b48
...
...
@@ -46,8 +46,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
labelKeys
?:
{
[
index
:
string
]:
string
[]
};
// metric -> [labelKey,...]
labelValues
?:
{
[
index
:
string
]:
{
[
index
:
string
]:
string
[]
}
};
// metric -> labelKey -> [labelValue,...]
metrics
?:
string
[];
logLabelOptions
:
any
[];
supportsLogs
?:
boolean
;
started
:
boolean
;
constructor
(
datasource
:
any
,
initialValues
?:
any
)
{
...
...
@@ -58,7 +56,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
this
.
labelKeys
=
{};
this
.
labelValues
=
{};
this
.
metrics
=
[];
this
.
supportsLogs
=
false
;
this
.
started
=
false
;
Object
.
assign
(
this
,
initialValues
);
...
...
@@ -243,8 +240,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}
// Query labels for selector
// Temporarily add skip for logging
if
(
selector
&&
!
this
.
labelValues
[
selector
]
&&
!
this
.
supportsLogs
)
{
if
(
selector
&&
!
this
.
labelValues
[
selector
])
{
if
(
selector
===
EMPTY_SELECTOR
)
{
// Query label values for default labels
refresher
=
Promise
.
all
(
DEFAULT_KEYS
.
map
(
key
=>
this
.
fetchLabelValues
(
key
)));
...
...
@@ -275,38 +271,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}
}
// Temporarily here while reusing this field for logging
async
fetchLogLabels
()
{
const
url
=
'/api/prom/label'
;
try
{
const
res
=
await
this
.
request
(
url
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
labelKeys
=
body
.
data
.
slice
().
sort
();
const
labelKeysBySelector
=
{
...
this
.
labelKeys
,
[
EMPTY_SELECTOR
]:
labelKeys
,
};
const
labelValuesByKey
=
{};
this
.
logLabelOptions
=
[];
for
(
const
key
of
labelKeys
)
{
const
valuesUrl
=
`/api/prom/label/
${
key
}
/values`
;
const
res
=
await
this
.
request
(
valuesUrl
);
const
body
=
await
(
res
.
data
||
res
.
json
());
const
values
=
body
.
data
.
slice
().
sort
();
labelValuesByKey
[
key
]
=
values
;
this
.
logLabelOptions
.
push
({
label
:
key
,
value
:
key
,
children
:
values
.
map
(
value
=>
({
label
:
value
,
value
})),
});
}
this
.
labelValues
=
{
[
EMPTY_SELECTOR
]:
labelValuesByKey
};
this
.
labelKeys
=
labelKeysBySelector
;
}
catch
(
e
)
{
console
.
error
(
e
);
}
}
async
fetchLabelValues
(
key
:
string
)
{
const
url
=
`/api/v1/label/
${
key
}
/values`
;
try
{
...
...
public/sass/pages/_explore.scss
View file @
e99c1b48
...
...
@@ -87,7 +87,7 @@
flex-wrap
:
wrap
;
}
.explore-
graph
__loader
{
.explore-
panel
__loader
{
height
:
2px
;
position
:
relative
;
overflow
:
hidden
;
...
...
@@ -95,7 +95,7 @@
margin
:
$panel-margin
/
2
;
}
.explore-
graph
__loader
:after
{
.explore-
panel
__loader
:after
{
content
:
' '
;
display
:
block
;
width
:
25%
;
...
...
@@ -219,7 +219,13 @@
}
.logs-row-match-highlight
{
background-color
:
lighten
(
$blue
,
20%
);
// Undoing mark styling
background
:
inherit
;
padding
:
inherit
;
color
:
$typeahead-selected-color
;
border-bottom
:
1px
solid
$typeahead-selected-color
;
background-color
:
lighten
(
$typeahead-selected-color
,
60%
);
}
.logs-row-level
{
...
...
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