2022-03-25 09:06:47 +03:00
package metrics
import (
"fmt"
"sort"
2022-03-28 02:27:38 +03:00
"github.com/owncast/owncast/core"
2022-03-25 09:06:47 +03:00
"github.com/owncast/owncast/models"
2023-07-22 08:25:59 +03:00
"github.com/owncast/owncast/services/status"
2023-06-25 21:58:25 +03:00
"github.com/owncast/owncast/storage/configrepository"
2022-03-26 23:01:23 +03:00
"github.com/owncast/owncast/utils"
2022-03-25 09:06:47 +03:00
)
2022-03-27 02:13:06 +03:00
const (
2022-04-10 09:02:50 +03:00
healthyPercentageMinValue = 75
maxCPUUsage = 90
minClientCountForDetails = 3
2022-03-27 02:13:06 +03:00
)
2022-03-25 09:06:47 +03:00
// GetStreamHealthOverview will return the stream health overview.
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) GetStreamHealthOverview ( ) * models . StreamHealthOverview {
return m . metrics . streamHealthOverview
2022-03-25 09:06:47 +03:00
}
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) generateStreamHealthOverview ( ) {
2022-04-10 09:02:50 +03:00
// Determine what percentage of total players are represented in our overview.
totalPlayerCount := len ( core . GetActiveViewers ( ) )
if totalPlayerCount == 0 {
2023-06-25 21:58:25 +03:00
m . metrics . streamHealthOverview = nil
2022-04-10 09:02:50 +03:00
return
2022-03-25 09:06:47 +03:00
}
2023-06-25 21:58:25 +03:00
pct := m . getClientErrorHeathyPercentage ( )
2022-05-09 01:58:39 +03:00
if pct < 1 {
2023-06-25 21:58:25 +03:00
m . metrics . streamHealthOverview = nil
2022-04-10 09:02:50 +03:00
return
}
overview := & models . StreamHealthOverview {
Healthy : pct > healthyPercentageMinValue ,
HealthyPercentage : pct ,
2023-06-25 21:58:25 +03:00
Message : m . getStreamHealthOverviewMessage ( ) ,
2022-03-27 02:13:06 +03:00
}
2023-06-25 21:58:25 +03:00
if totalPlayerCount > 0 && len ( m . windowedBandwidths ) > 0 {
representation := utils . IntPercentage ( len ( m . windowedBandwidths ) , totalPlayerCount )
2022-04-07 10:14:23 +03:00
overview . Representation = representation
}
2022-04-10 09:02:50 +03:00
2023-06-25 21:58:25 +03:00
m . metrics . streamHealthOverview = overview
2022-03-27 02:13:06 +03:00
}
2022-03-25 09:06:47 +03:00
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) getStreamHealthOverviewMessage ( ) string {
if message := m . wastefulBitrateOverviewMessage ( ) ; message != "" {
2022-04-10 09:02:50 +03:00
return message
2023-06-25 21:58:25 +03:00
} else if message := m . cpuUsageHealthOverviewMessage ( ) ; message != "" {
2022-04-10 09:02:50 +03:00
return message
2023-06-25 21:58:25 +03:00
} else if message := m . networkSpeedHealthOverviewMessage ( ) ; message != "" {
2022-04-10 09:02:50 +03:00
return message
2023-06-25 21:58:25 +03:00
} else if message := m . errorCountHealthOverviewMessage ( ) ; message != "" {
2022-04-10 09:02:50 +03:00
return message
}
return ""
}
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) networkSpeedHealthOverviewMessage ( ) string {
2022-03-25 09:06:47 +03:00
type singleVariant struct {
isVideoPassthrough bool
bitrate int
}
2023-07-22 08:25:59 +03:00
configRepository := configrepository . Get ( )
2023-06-25 21:58:25 +03:00
outputVariants := configRepository . GetStreamOutputVariants ( )
2022-03-25 09:06:47 +03:00
streamSortVariants := make ( [ ] singleVariant , len ( outputVariants ) )
for i , variant := range outputVariants {
variantSort := singleVariant {
bitrate : variant . VideoBitrate ,
isVideoPassthrough : variant . IsVideoPassthrough ,
}
streamSortVariants [ i ] = variantSort
}
sort . Slice ( streamSortVariants , func ( i , j int ) bool {
if streamSortVariants [ i ] . isVideoPassthrough && ! streamSortVariants [ j ] . isVideoPassthrough {
return true
}
if ! streamSortVariants [ i ] . isVideoPassthrough && streamSortVariants [ j ] . isVideoPassthrough {
return false
}
return streamSortVariants [ i ] . bitrate > streamSortVariants [ j ] . bitrate
} )
2022-04-10 09:02:50 +03:00
lowestSupportedBitrate := float64 ( streamSortVariants [ len ( streamSortVariants ) - 1 ] . bitrate )
2023-06-25 21:58:25 +03:00
totalNumberOfClients := len ( m . windowedBandwidths )
2022-03-25 09:06:47 +03:00
if totalNumberOfClients == 0 {
2022-04-10 09:02:50 +03:00
return ""
2022-03-25 09:06:47 +03:00
}
// Determine healthy status based on bandwidth speeds of clients.
unhealthyClientCount := 0
2022-03-27 02:13:06 +03:00
2023-06-25 21:58:25 +03:00
for _ , speed := range m . windowedBandwidths {
2022-03-25 09:06:47 +03:00
if int ( speed ) < int ( lowestSupportedBitrate * 1.1 ) {
unhealthyClientCount ++
}
}
if unhealthyClientCount == 0 {
2022-04-10 09:02:50 +03:00
return ""
2022-03-25 09:06:47 +03:00
}
2022-04-10 09:02:50 +03:00
return fmt . Sprintf ( "%d of %d viewers (%d%%) are consuming video slower than, or too close to your bitrate of %d kbps." , unhealthyClientCount , totalNumberOfClients , int ( ( float64 ( unhealthyClientCount ) / float64 ( totalNumberOfClients ) ) * 100 ) , int ( lowestSupportedBitrate ) )
2022-03-27 02:13:06 +03:00
}
2022-04-10 09:02:50 +03:00
// wastefulBitrateOverviewMessage attempts to determine if a streamer is sending to
// Owncast at a bitrate higher than they're streaming to their viewers leading
// to wasted CPU by having to compress it.
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) wastefulBitrateOverviewMessage ( ) string {
if len ( m . metrics . CPUUtilizations ) < 2 {
2022-04-10 09:02:50 +03:00
return ""
2022-03-27 02:13:06 +03:00
}
2022-04-10 09:02:50 +03:00
// Only return an alert if the CPU usage is around the max cpu threshold.
2023-06-25 21:58:25 +03:00
recentCPUUses := m . metrics . CPUUtilizations [ len ( m . metrics . CPUUtilizations ) - 2 : ]
2022-03-27 02:13:06 +03:00
values := make ( [ ] float64 , len ( recentCPUUses ) )
for i , val := range recentCPUUses {
values [ i ] = val . Value
}
recentCPUUse := utils . Avg ( values )
2022-04-10 09:02:50 +03:00
if recentCPUUse < maxCPUUsage - 10 {
return ""
2022-03-27 02:13:06 +03:00
}
2023-07-22 08:25:59 +03:00
stat := status . Get ( )
currentBroadcast := stat . GetCurrentBroadcast ( )
2022-04-10 09:02:50 +03:00
if currentBroadcast == nil {
return ""
}
2023-07-22 08:25:59 +03:00
currentBroadcaster := stat . GetBroadcaster ( )
2022-04-10 09:02:50 +03:00
if currentBroadcast == nil {
return ""
}
2022-05-12 03:00:13 +03:00
if currentBroadcaster . StreamDetails . VideoBitrate == 0 {
2022-04-10 09:02:50 +03:00
return ""
}
2022-05-12 03:00:13 +03:00
// Not all streams report their inbound bitrate.
2022-04-10 09:02:50 +03:00
inboundBitrate := currentBroadcaster . StreamDetails . VideoBitrate
2022-05-12 03:00:13 +03:00
if inboundBitrate == 0 {
return ""
}
2023-07-22 08:25:59 +03:00
configRepository := configrepository . Get ( )
2022-05-12 03:00:13 +03:00
2023-06-25 21:58:25 +03:00
outputVariants := configRepository . GetStreamOutputVariants ( )
2022-05-12 03:00:13 +03:00
type singleVariant struct {
isVideoPassthrough bool
bitrate int
}
streamSortVariants := make ( [ ] singleVariant , len ( outputVariants ) )
for i , variant := range outputVariants {
variantSort := singleVariant {
bitrate : variant . VideoBitrate ,
isVideoPassthrough : variant . IsVideoPassthrough ,
}
streamSortVariants [ i ] = variantSort
}
sort . Slice ( streamSortVariants , func ( i , j int ) bool {
if streamSortVariants [ i ] . isVideoPassthrough && ! streamSortVariants [ j ] . isVideoPassthrough {
return true
}
if ! streamSortVariants [ i ] . isVideoPassthrough && streamSortVariants [ j ] . isVideoPassthrough {
return false
}
return streamSortVariants [ i ] . bitrate > streamSortVariants [ j ] . bitrate
} )
maxBitrate := streamSortVariants [ 0 ] . bitrate
2022-04-10 09:02:50 +03:00
if inboundBitrate > maxBitrate {
2022-05-12 03:00:13 +03:00
return fmt . Sprintf ( "You're streaming to Owncast at %dkbps but only broadcasting to your viewers at %dkbps, requiring unnecessary work to be performed and possible excessive CPU use. You may want to decrease what you're sending to Owncast or increase what you send to your viewers so the highest bitrate matches." , inboundBitrate , maxBitrate )
2022-03-27 02:13:06 +03:00
}
2022-04-10 09:02:50 +03:00
return ""
}
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) cpuUsageHealthOverviewMessage ( ) string {
if len ( m . metrics . CPUUtilizations ) < 2 {
2022-04-10 09:02:50 +03:00
return ""
}
2022-03-27 02:13:06 +03:00
2023-06-25 21:58:25 +03:00
recentCPUUses := m . metrics . CPUUtilizations [ len ( m . metrics . CPUUtilizations ) - 2 : ]
2022-04-10 09:02:50 +03:00
values := make ( [ ] float64 , len ( recentCPUUses ) )
for i , val := range recentCPUUses {
values [ i ] = val . Value
2022-03-27 02:13:06 +03:00
}
2022-04-10 09:02:50 +03:00
recentCPUUse := utils . Avg ( values )
if recentCPUUse < maxCPUUsage {
return ""
}
return fmt . Sprintf ( "The CPU usage on your server is over %d%%. This may cause video to be provided slower than necessary, causing buffering for your viewers. Consider increasing the resources available or reducing the number of output variants you made available." , maxCPUUsage )
2022-03-27 02:13:06 +03:00
}
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) errorCountHealthOverviewMessage ( ) string {
totalNumberOfClients := len ( m . windowedBandwidths )
2022-03-27 02:13:06 +03:00
if totalNumberOfClients == 0 {
2022-04-10 09:02:50 +03:00
return ""
2022-03-27 02:13:06 +03:00
}
2023-06-25 21:58:25 +03:00
clientsWithErrors := m . getClientsWithErrorsCount ( )
2022-03-26 23:01:23 +03:00
2022-03-27 02:13:06 +03:00
if clientsWithErrors == 0 {
2022-04-10 09:02:50 +03:00
return ""
2022-03-25 09:06:47 +03:00
}
2022-04-07 10:14:23 +03:00
// Only return these detailed values and messages if we feel we have enough
// clients to be able to make a reasonable assessment. This is an arbitrary
// number but 1 out of 1 isn't helpful.
2022-03-27 02:13:06 +03:00
2022-04-07 10:14:23 +03:00
if totalNumberOfClients >= minClientCountForDetails {
healthyPercentage := utils . IntPercentage ( clientsWithErrors , totalNumberOfClients )
2023-07-22 08:25:59 +03:00
configRepository := configrepository . Get ( )
2022-04-07 10:14:23 +03:00
isUsingPassthrough := false
2023-06-25 21:58:25 +03:00
outputVariants := configRepository . GetStreamOutputVariants ( )
2022-04-07 10:14:23 +03:00
for _ , variant := range outputVariants {
if variant . IsVideoPassthrough {
isUsingPassthrough = true
}
2022-03-31 00:02:49 +03:00
}
2022-04-07 10:14:23 +03:00
if isUsingPassthrough {
2022-04-10 09:02:50 +03:00
return fmt . Sprintf ( "%d of %d viewers (%d%%) are experiencing errors. You're currently using a video passthrough output, often known for causing playback issues for people. It is suggested you turn it off." , clientsWithErrors , totalNumberOfClients , healthyPercentage )
2022-04-07 10:14:23 +03:00
}
2022-04-10 09:02:50 +03:00
2023-07-22 08:25:59 +03:00
stat := status . Get ( )
currentBroadcast := stat . GetCurrentBroadcast ( )
2022-04-26 00:09:06 +03:00
if currentBroadcast != nil && currentBroadcast . LatencyLevel . SecondsPerSegment < 3 {
return fmt . Sprintf ( "%d of %d viewers (%d%%) may be experiencing some issues. You may want to increase your latency buffer level in your video configuration to see if it helps." , clientsWithErrors , totalNumberOfClients , healthyPercentage )
}
2022-04-10 09:02:50 +03:00
return fmt . Sprintf ( "%d of %d viewers (%d%%) may be experiencing some issues." , clientsWithErrors , totalNumberOfClients , healthyPercentage )
2022-03-31 00:02:49 +03:00
}
2022-04-07 10:14:23 +03:00
2022-04-10 09:02:50 +03:00
return ""
}
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) getClientsWithErrorsCount ( ) int {
2022-04-10 09:02:50 +03:00
clientsWithErrors := 0
2023-06-25 21:58:25 +03:00
for _ , errors := range m . windowedErrorCounts {
2022-04-10 09:02:50 +03:00
if errors > 0 {
clientsWithErrors ++
}
}
return clientsWithErrors
}
2023-06-25 21:58:25 +03:00
func ( m * Metrics ) getClientErrorHeathyPercentage ( ) int {
totalNumberOfClients := len ( m . windowedErrorCounts )
2022-04-10 09:02:50 +03:00
if totalNumberOfClients == 0 {
return - 1
}
2023-06-25 21:58:25 +03:00
clientsWithErrors := m . getClientsWithErrorsCount ( )
2022-04-10 09:02:50 +03:00
if clientsWithErrors == 0 {
return 100
2022-03-27 02:13:06 +03:00
}
2022-04-10 09:02:50 +03:00
pct := 100 - utils . IntPercentage ( clientsWithErrors , totalNumberOfClients )
return pct
2022-03-25 09:06:47 +03:00
}