Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
heptapod
heptapod
Commits
1c42d96eb6ad
Commit
21b70167
authored
Apr 06, 2019
by
Chris Baumbauer
Committed by
Mike Greiling
Apr 06, 2019
Browse files
Add Knative metrics to Prometheus
parent
d8f3b1692725
Changes
52
Hide whitespace changes
Inline
Side-by-side
app/assets/javascripts/serverless/components/area.vue
0 → 100644
View file @
1c42d96e
<
script
>
import
{
GlAreaChart
}
from
'
@gitlab/ui/dist/charts
'
;
import
{
debounceByAnimationFrame
}
from
'
~/lib/utils/common_utils
'
;
import
dateFormat
from
'
dateformat
'
;
import
{
X_INTERVAL
}
from
'
../constants
'
;
import
{
validateGraphData
}
from
'
../utils
'
;
let
debouncedResize
;
export
default
{
components
:
{
GlAreaChart
,
},
inheritAttrs
:
false
,
props
:
{
graphData
:
{
type
:
Object
,
required
:
true
,
validator
:
validateGraphData
,
},
containerWidth
:
{
type
:
Number
,
required
:
true
,
},
},
data
()
{
return
{
tooltipPopoverTitle
:
''
,
tooltipPopoverContent
:
''
,
width
:
this
.
containerWidth
,
};
},
computed
:
{
chartData
()
{
return
this
.
graphData
.
queries
.
reduce
((
accumulator
,
query
)
=>
{
accumulator
[
query
.
unit
]
=
query
.
result
.
reduce
((
acc
,
res
)
=>
acc
.
concat
(
res
.
values
),
[]);
return
accumulator
;
},
{});
},
extractTimeData
()
{
return
this
.
chartData
.
requests
.
map
(
data
=>
data
.
time
);
},
generateSeries
()
{
return
{
name
:
'
Invocations
'
,
type
:
'
line
'
,
data
:
this
.
chartData
.
requests
.
map
(
data
=>
[
data
.
time
,
data
.
value
]),
symbolSize
:
0
,
};
},
getInterval
()
{
const
{
result
}
=
this
.
graphData
.
queries
[
0
];
if
(
result
.
length
===
0
)
{
return
1
;
}
const
split
=
result
[
0
].
values
.
reduce
(
(
acc
,
pair
)
=>
(
pair
.
value
>
acc
?
pair
.
value
:
acc
),
1
,
);
return
split
<
X_INTERVAL
?
split
:
X_INTERVAL
;
},
chartOptions
()
{
return
{
xAxis
:
{
name
:
'
time
'
,
type
:
'
time
'
,
axisLabel
:
{
formatter
:
date
=>
dateFormat
(
date
,
'
h:MM TT
'
),
},
data
:
this
.
extractTimeData
,
nameTextStyle
:
{
padding
:
[
18
,
0
,
0
,
0
],
},
},
yAxis
:
{
name
:
this
.
yAxisLabel
,
nameTextStyle
:
{
padding
:
[
0
,
0
,
36
,
0
],
},
splitNumber
:
this
.
getInterval
,
},
legend
:
{
formatter
:
this
.
xAxisLabel
,
},
series
:
this
.
generateSeries
,
};
},
xAxisLabel
()
{
return
this
.
graphData
.
queries
.
map
(
query
=>
query
.
label
).
join
(
'
,
'
);
},
yAxisLabel
()
{
const
[
query
]
=
this
.
graphData
.
queries
;
return
`
${
this
.
graphData
.
y_label
}
(
${
query
.
unit
}
)`
;
},
},
watch
:
{
containerWidth
:
'
onResize
'
,
},
beforeDestroy
()
{
window
.
removeEventListener
(
'
resize
'
,
debouncedResize
);
},
created
()
{
debouncedResize
=
debounceByAnimationFrame
(
this
.
onResize
);
window
.
addEventListener
(
'
resize
'
,
debouncedResize
);
},
methods
:
{
formatTooltipText
(
params
)
{
const
[
seriesData
]
=
params
.
seriesData
;
this
.
tooltipPopoverTitle
=
dateFormat
(
params
.
value
,
'
dd mmm yyyy, h:MMTT
'
);
this
.
tooltipPopoverContent
=
`
${
this
.
yAxisLabel
}
:
${
seriesData
.
value
[
1
]}
`
;
},
onResize
()
{
const
{
width
}
=
this
.
$refs
.
areaChart
.
$el
.
getBoundingClientRect
();
this
.
width
=
width
;
},
},
};
</
script
>
<
template
>
<div
class=
"prometheus-graph"
>
<div
class=
"prometheus-graph-header"
>
<h5
ref=
"graphTitle"
class=
"prometheus-graph-title"
>
{{
graphData
.
title
}}
</h5>
<div
ref=
"graphWidgets"
class=
"prometheus-graph-widgets"
><slot></slot></div>
</div>
<gl-area-chart
ref=
"areaChart"
v-bind=
"$attrs"
:data=
"[]"
:option=
"chartOptions"
:format-tooltip-text=
"formatTooltipText"
:width=
"width"
:include-legend-avg-max=
"false"
>
<template
slot=
"tooltipTitle"
>
{{
tooltipPopoverTitle
}}
</
template
>
<
template
slot=
"tooltipContent"
>
{{
tooltipPopoverContent
}}
</
template
>
</gl-area-chart>
</div>
</template>
app/assets/javascripts/serverless/components/function_details.vue
View file @
1c42d96e
<
script
>
import
_
from
'
underscore
'
;
import
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
PodBox
from
'
./pod_box.vue
'
;
import
Url
from
'
./url.vue
'
;
import
AreaChart
from
'
./area.vue
'
;
import
MissingPrometheus
from
'
./missing_prometheus.vue
'
;
export
default
{
components
:
{
PodBox
,
Url
,
AreaChart
,
MissingPrometheus
,
},
props
:
{
func
:
{
type
:
Object
,
required
:
true
,
},
hasPrometheus
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
clustersPath
:
{
type
:
String
,
required
:
true
,
},
helpPath
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
elWidth
:
0
,
};
},
computed
:
{
name
()
{
return
this
.
func
.
name
;
},
description
()
{
return
this
.
func
.
description
;
return
_
.
isString
(
this
.
func
.
description
)
?
this
.
func
.
description
:
''
;
},
funcUrl
()
{
return
this
.
func
.
url
;
},
podCount
()
{
return
this
.
func
.
podcount
||
0
;
return
Number
(
this
.
func
.
podcount
)
||
0
;
},
...
mapState
([
'
graphData
'
,
'
hasPrometheusData
'
]),
...
mapGetters
([
'
hasPrometheusMissingData
'
]),
},
created
()
{
this
.
fetchMetrics
({
metricsPath
:
this
.
func
.
metricsUrl
,
hasPrometheus
:
this
.
hasPrometheus
,
});
},
mounted
()
{
this
.
elWidth
=
this
.
$el
.
clientWidth
;
},
methods
:
{
...
mapActions
([
'
fetchMetrics
'
]),
},
};
</
script
>
<
template
>
<section
id=
"serverless-function-details"
>
<h3>
{{
name
}}
</h3>
<div
class=
"append-bottom-default"
>
<h3
class=
"serverless-function-name"
>
{{
name
}}
</h3>
<div
class=
"append-bottom-default
serverless-function-description
"
>
<div
v-for=
"(line, index) in description.split('\n')"
:key=
"index"
>
{{
line
}}
</div>
</div>
<url
:uri=
"funcUrl"
/>
...
...
@@ -52,5 +90,13 @@ export default {
</p>
</div>
<div
v-else
><p>
No pods loaded at this time.
</p></div>
<area-chart
v-if=
"hasPrometheusData"
:graph-data=
"graphData"
:container-width=
"elWidth"
/>
<missing-prometheus
v-if=
"!hasPrometheus || hasPrometheusMissingData"
:help-path=
"helpPath"
:clusters-path=
"clustersPath"
:missing-data=
"hasPrometheusMissingData"
/>
</section>
</
template
>
app/assets/javascripts/serverless/components/function_row.vue
View file @
1c42d96e
<
script
>
import
_
from
'
underscore
'
;
import
Timeago
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
Url
from
'
./url.vue
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
...
...
@@ -19,6 +20,10 @@ export default {
return
this
.
func
.
name
;
},
description
()
{
if
(
!
_
.
isString
(
this
.
func
.
description
))
{
return
''
;
}
const
desc
=
this
.
func
.
description
.
split
(
'
\n
'
);
if
(
desc
.
length
>
1
)
{
return
desc
[
1
];
...
...
app/assets/javascripts/serverless/components/functions.vue
View file @
1c42d96e
<
script
>
import
{
GlSkeletonLoading
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
FunctionRow
from
'
./function_row.vue
'
;
import
EnvironmentRow
from
'
./environment_row.vue
'
;
import
EmptyState
from
'
./empty_state.vue
'
;
...
...
@@ -9,14 +10,9 @@ export default {
EnvironmentRow
,
FunctionRow
,
EmptyState
,
Gl
Skeleton
Loading
,
GlLoading
Icon
,
},
props
:
{
functions
:
{
type
:
Object
,
required
:
true
,
default
:
()
=>
({}),
},
installed
:
{
type
:
Boolean
,
required
:
true
,
...
...
@@ -29,17 +25,23 @@ export default {
type
:
String
,
required
:
true
,
},
loadingData
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
hasFunctionData
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
statusPath
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
...
mapState
([
'
isLoading
'
,
'
hasFunctionData
'
]),
...
mapGetters
([
'
getFunctions
'
]),
},
created
()
{
this
.
fetchFunctions
({
functionsPath
:
this
.
statusPath
,
});
},
methods
:
{
...
mapActions
([
'
fetchFunctions
'
]),
},
};
</
script
>
...
...
@@ -47,14 +49,16 @@ export default {
<section
id=
"serverless-functions"
>
<div
v-if=
"installed"
>
<div
v-if=
"hasFunctionData"
>
<template
v-if=
"loadingData"
>
<div
v-for=
"j in 3"
:key=
"j"
class=
"gl-responsive-table-row"
><gl-skeleton-loading
/></div>
</
template
>
<gl-loading-icon
v-if=
"isLoading"
:size=
"2"
class=
"prepend-top-default append-bottom-default"
/>
<template
v-else
>
<div
class=
"groups-list-tree-container"
>
<ul
class=
"content-list group-list-tree"
>
<environment-row
v-for=
"(env, index) in
f
unctions"
v-for=
"(env, index) in
getF
unctions"
:key=
"index"
:env=
"env"
:env-name=
"index"
...
...
app/assets/javascripts/serverless/components/missing_prometheus.vue
0 → 100644
View file @
1c42d96e
<
script
>
import
{
GlButton
,
GlLink
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
../../locale
'
;
export
default
{
components
:
{
GlButton
,
GlLink
,
},
props
:
{
clustersPath
:
{
type
:
String
,
required
:
true
,
},
helpPath
:
{
type
:
String
,
required
:
true
,
},
missingData
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
missingStateClass
()
{
return
this
.
missingData
?
'
missing-prometheus-state
'
:
'
empty-prometheus-state
'
;
},
prometheusHelpPath
()
{
return
`
${
this
.
helpPath
}
#prometheus-support`
;
},
description
()
{
return
this
.
missingData
?
s__
(
`ServerlessDetails|Invocation metrics loading or not available at this time.`
)
:
s__
(
`ServerlessDetails|Function invocation metrics require Prometheus to be installed first.`
,
);
},
},
};
</
script
>
<
template
>
<div
class=
"row"
:class=
"missingStateClass"
>
<div
class=
"col-12"
>
<div
class=
"text-content"
>
<h4
class=
"state-title text-left"
>
{{
s__
(
`ServerlessDetails|Invocations`
)
}}
</h4>
<p
class=
"state-description"
>
{{
description
}}
<gl-link
:href=
"prometheusHelpPath"
>
{{
s__
(
`ServerlessDetails|More information`
)
}}
</gl-link
>
.
</p>
<div
v-if=
"!missingData"
class=
"text-left"
>
<gl-button
:href=
"clustersPath"
variant=
"success"
>
{{
s__
(
'
ServerlessDetails|Install Prometheus
'
)
}}
</gl-button>
</div>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/serverless/constants.js
0 → 100644
View file @
1c42d96e
export
const
MAX_REQUESTS
=
3
;
// max number of times to retry
export
const
X_INTERVAL
=
5
;
// Reflects the number of verticle bars on the x-axis
app/assets/javascripts/serverless/serverless_bundle.js
View file @
1c42d96e
import
Visibility
from
'
visibilityjs
'
;
import
Vue
from
'
vue
'
;
import
{
s__
}
from
'
../locale
'
;
import
Flash
from
'
../flash
'
;
import
Poll
from
'
../lib/utils/poll
'
;
import
ServerlessStore
from
'
./stores/serverless_store
'
;
import
ServerlessDetailsStore
from
'
./stores/serverless_details_store
'
;
import
GetFunctionsService
from
'
./services/get_functions_service
'
;
import
Functions
from
'
./components/functions.vue
'
;
import
FunctionDetails
from
'
./components/function_details.vue
'
;
import
{
createStore
}
from
'
./store
'
;
export
default
class
Serverless
{
constructor
()
{
...
...
@@ -19,10 +13,12 @@ export default class Serverless {
serviceUrl
,
serviceNamespace
,
servicePodcount
,
serviceMetricsUrl
,
prometheus
,
clustersPath
,
helpPath
,
}
=
document
.
querySelector
(
'
.js-serverless-function-details-page
'
).
dataset
;
const
el
=
document
.
querySelector
(
'
#js-serverless-function-details
'
);
this
.
store
=
new
ServerlessDetailsStore
();
const
{
store
}
=
this
;
const
service
=
{
name
:
serviceName
,
...
...
@@ -31,20 +27,19 @@ export default class Serverless {
url
:
serviceUrl
,
namespace
:
serviceNamespace
,
podcount
:
servicePodcount
,
metricsUrl
:
serviceMetricsUrl
,
};
this
.
store
.
updateDetailedFunction
(
service
);
this
.
functionDetails
=
new
Vue
({
el
,
data
()
{
return
{
state
:
store
.
state
,
};
},
store
:
createStore
(),
render
(
createElement
)
{
return
createElement
(
FunctionDetails
,
{
props
:
{
func
:
this
.
state
.
functionDetail
,
func
:
service
,
hasPrometheus
:
prometheus
!==
undefined
,
clustersPath
,
helpPath
,
},
});
},
...
...
@@ -54,95 +49,27 @@ export default class Serverless {
'
.js-serverless-functions-page
'
,
).
dataset
;
this
.
service
=
new
GetFunctionsService
(
statusPath
);
this
.
knativeInstalled
=
installed
!==
undefined
;
this
.
store
=
new
ServerlessStore
(
this
.
knativeInstalled
,
clustersPath
,
helpPath
);
this
.
initServerless
();
this
.
functionLoadCount
=
0
;
if
(
statusPath
&&
this
.
knativeInstalled
)
{
this
.
initPolling
();
}
}
}
initServerless
()
{
const
{
store
}
=
this
;
const
el
=
document
.
querySelector
(
'
#js-serverless-functions
'
);
this
.
functions
=
new
Vue
({
el
,
data
()
{
return
{
state
:
store
.
state
,
};
},
render
(
createElement
)
{
return
createElement
(
Functions
,
{
props
:
{
functions
:
this
.
state
.
functions
,
installed
:
this
.
state
.
installed
,
clustersPath
:
this
.
state
.
clustersPath
,
helpPath
:
this
.
state
.
helpPath
,
loadingData
:
this
.
state
.
loadingData
,
hasFunctionData
:
this
.
state
.
hasFunctionData
,
},
});
},
});
}
initPolling
()
{
this
.
poll
=
new
Poll
({
resource
:
this
.
service
,
method
:
'
fetchData
'
,
successCallback
:
data
=>
this
.
handleSuccess
(
data
),
errorCallback
:
()
=>
Serverless
.
handleError
(),
});
if
(
!
Visibility
.
hidden
())
{
this
.
poll
.
makeRequest
();
}
else
{
this
.
service
.
fetchData
()
.
then
(
data
=>
this
.
handleSuccess
(
data
))
.
catch
(()
=>
Serverless
.
handleError
());
}
Visibility
.
change
(()
=>
{
if
(
!
Visibility
.
hidden
()
&&
!
this
.
destroyed
)
{
this
.
poll
.
restart
();
}
else
{
this
.
poll
.
stop
();
}
});
}
handleSuccess
(
data
)
{
if
(
data
.
status
===
200
)
{
this
.
store
.
updateFunctionsFromServer
(
data
.
data
);
this
.
store
.
updateLoadingState
(
false
);
}
else
if
(
data
.
status
===
204
)
{
/* Time out after 3 attempts to retrieve data */
this
.
functionLoadCount
+=
1
;
if
(
this
.
functionLoadCount
===
3
)
{
this
.
poll
.
stop
();
this
.
store
.
toggleNoFunctionData
();
}
const
el
=
document
.
querySelector
(
'
#js-serverless-functions
'
);
this
.
functions
=
new
Vue
({
el
,
store
:
createStore
(),
render
(
createElement
)
{
return
createElement
(
Functions
,
{
props
:
{
installed
:
installed
!==
undefined
,
clustersPath
,
helpPath
,
statusPath
,
},
});
},
});
}
}
static
handleError
()
{
Flash
(
s__
(
'
Serverless|An error occurred while retrieving serverless components
'
));
}
destroy
()
{
this
.
destroyed
=
true
;
if
(
this
.
poll
)
{
this
.
poll
.
stop
();
}
this
.
functions
.
$destroy
();
this
.
functionDetails
.
$destroy
();
}
...
...
app/assets/javascripts/serverless/services/get_functions_service.js
deleted
100644 → 0
View file @
d8f3b169
import
axios
from
'
~/lib/utils/axios_utils
'
;
export
default
class
GetFunctionsService
{
constructor
(
endpoint
)
{
this
.
endpoint
=
endpoint
;
}
fetchData
()
{
return
axios
.
get
(
this
.
endpoint
);
}
}
app/assets/javascripts/serverless/store/actions.js
0 → 100644
View file @
1c42d96e
import
*
as
types
from
'
./mutation_types
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
statusCodes
from
'
~/lib/utils/http_status
'
;
import
{
backOff
}
from
'
~/lib/utils/common_utils
'
;