forked from Silverfish/proton-bridge
feat(BRIDGE-150): Observability service modification; user distinction utility & heartbeat; various observbility metrics & relevant integration tests
This commit is contained in:
47
internal/services/observability/distinction_error_types.go
Normal file
47
internal/services/observability/distinction_error_types.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import "time"
|
||||
|
||||
// DistinctionErrorTypeEnum - maps to the specific error schema for which we
|
||||
// want to send a user update.
|
||||
type DistinctionErrorTypeEnum int
|
||||
|
||||
const (
|
||||
SyncError DistinctionErrorTypeEnum = iota
|
||||
EventLoopError
|
||||
)
|
||||
|
||||
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name.
|
||||
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals
|
||||
SyncError: "bridge_sync_errors_users_total",
|
||||
EventLoopError: "bridge_event_loop_events_errors_users_total",
|
||||
}
|
||||
|
||||
// createLastSentMap - needs to be updated whenever we make changes to the enum.
|
||||
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time {
|
||||
registerTime := time.Now().Add(-updateInterval)
|
||||
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time)
|
||||
|
||||
for errType := SyncError; errType <= EventLoopError; errType++ {
|
||||
lastSentMap[errType] = registerTime
|
||||
}
|
||||
|
||||
return lastSentMap
|
||||
}
|
||||
170
internal/services/observability/distinction_utility.go
Normal file
170
internal/services/observability/distinction_utility.go
Normal file
@ -0,0 +1,170 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
)
|
||||
|
||||
var updateInterval = time.Minute * 5 //nolint:gochecknoglobals
|
||||
|
||||
type observabilitySender interface {
|
||||
addMetricsIfClients(metric ...proton.ObservabilityMetric)
|
||||
}
|
||||
|
||||
// distinctionUtility - used to discern whether X number of events stem from Y number of users.
|
||||
type distinctionUtility struct {
|
||||
ctx context.Context
|
||||
|
||||
panicHandler async.PanicHandler
|
||||
|
||||
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
|
||||
|
||||
observabilitySender observabilitySender
|
||||
settingsGetter settingsGetter
|
||||
|
||||
userPlanUnsafe string
|
||||
userPlanLock sync.Mutex
|
||||
|
||||
heartbeatData heartbeatData
|
||||
heartbeatDataLock sync.Mutex
|
||||
heartbeatTicker *time.Ticker
|
||||
}
|
||||
|
||||
func newDistinctionUtility(ctx context.Context, panicHandler async.PanicHandler, observabilitySender observabilitySender) *distinctionUtility {
|
||||
distinctionUtility := &distinctionUtility{
|
||||
ctx: ctx,
|
||||
|
||||
panicHandler: panicHandler,
|
||||
|
||||
lastSentMap: createLastSentMap(),
|
||||
|
||||
observabilitySender: observabilitySender,
|
||||
|
||||
userPlanUnsafe: planUnknown,
|
||||
|
||||
heartbeatData: heartbeatData{},
|
||||
heartbeatTicker: time.NewTicker(updateInterval),
|
||||
}
|
||||
|
||||
return distinctionUtility
|
||||
}
|
||||
|
||||
// sendMetricsWithGuard - schedules the metrics to be sent only if there are authenticated clients.
|
||||
func (d *distinctionUtility) sendMetricsWithGuard(metrics ...proton.ObservabilityMetric) {
|
||||
if d.observabilitySender == nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.observabilitySender.addMetricsIfClients(metrics...)
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
|
||||
d.settingsGetter = getter
|
||||
}
|
||||
|
||||
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
|
||||
// within the last 5 minutes.
|
||||
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool {
|
||||
curTime := time.Now()
|
||||
val, ok := d.lastSentMap[key]
|
||||
if !ok {
|
||||
d.lastSentMap[key] = curTime
|
||||
return true
|
||||
}
|
||||
|
||||
if val.Add(updateInterval).Before(curTime) {
|
||||
d.lastSentMap[key] = curTime
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// generateUserMetric creates the relevant user update metric based on its type
|
||||
// and the relevant settings. In the future this will need to be expanded to support multiple
|
||||
// versions of the metric if we ever decide to change them.
|
||||
func (d *distinctionUtility) generateUserMetric(
|
||||
metricType DistinctionErrorTypeEnum,
|
||||
) proton.ObservabilityMetric {
|
||||
schemaName, ok := errorSchemaMap[metricType]
|
||||
if !ok {
|
||||
return proton.ObservabilityMetric{}
|
||||
}
|
||||
|
||||
return generateUserMetric(schemaName, d.getUserPlanSafe(),
|
||||
d.getEmailClientUserAgent(),
|
||||
getEnabled(d.getProxyAllowed()),
|
||||
getEnabled(d.getBetaAccessEnabled()),
|
||||
)
|
||||
}
|
||||
|
||||
func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: schemaName,
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"plan": plan,
|
||||
"mailClient": mailClient,
|
||||
"dohEnabled": dohEnabled,
|
||||
"betaAccessEnabled": betaAccess,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
|
||||
d.updateHeartbeatData(errType)
|
||||
|
||||
if d.checkAndUpdateLastSentMap(errType) {
|
||||
metrics = append(metrics, d.generateUserMetric(errType))
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getEmailClientUserAgent() string {
|
||||
ua := ""
|
||||
if d.settingsGetter != nil {
|
||||
ua = d.settingsGetter.GetCurrentUserAgent()
|
||||
}
|
||||
|
||||
return matchUserAgent(ua)
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getBetaAccessEnabled() bool {
|
||||
if d.settingsGetter == nil {
|
||||
return false
|
||||
}
|
||||
return d.settingsGetter.GetUpdateChannel() == updater.EarlyChannel
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getProxyAllowed() bool {
|
||||
if d.settingsGetter == nil {
|
||||
return false
|
||||
}
|
||||
return d.settingsGetter.GetProxyAllowed()
|
||||
}
|
||||
119
internal/services/observability/heartbeat.go
Normal file
119
internal/services/observability/heartbeat.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const genericHeartbeatSchemaName = "bridge_generic_user_heartbeat_total"
|
||||
|
||||
type heartbeatData struct {
|
||||
receivedSyncError bool
|
||||
receivedEventLoopError bool
|
||||
receivedOtherError bool
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) resetHeartbeatData() {
|
||||
d.heartbeatData.receivedSyncError = false
|
||||
d.heartbeatData.receivedOtherError = false
|
||||
d.heartbeatData.receivedEventLoopError = false
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) {
|
||||
d.withUpdateHeartbeatDataLock(func() {
|
||||
switch errType {
|
||||
case SyncError:
|
||||
d.heartbeatData.receivedSyncError = true
|
||||
case EventLoopError:
|
||||
d.heartbeatData.receivedEventLoopError = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) runHeartbeat() {
|
||||
go func() {
|
||||
defer async.HandlePanic(d.panicHandler)
|
||||
defer d.heartbeatTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-d.ctx.Done():
|
||||
return
|
||||
case <-d.heartbeatTicker.C:
|
||||
d.sendHeartbeat()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) withUpdateHeartbeatDataLock(fn func()) {
|
||||
d.heartbeatDataLock.Lock()
|
||||
defer d.heartbeatDataLock.Unlock()
|
||||
fn()
|
||||
}
|
||||
|
||||
// sendHeartbeat - will only send a heartbeat if there is an authenticated client
|
||||
// otherwise we might end up polluting the cache and therefore our metrics.
|
||||
func (d *distinctionUtility) sendHeartbeat() {
|
||||
d.withUpdateHeartbeatDataLock(func() {
|
||||
d.sendMetricsWithGuard(d.generateHeartbeatUserMetric())
|
||||
d.resetHeartbeatData()
|
||||
})
|
||||
}
|
||||
|
||||
func formatBool(value bool) string {
|
||||
return fmt.Sprintf("%t", value)
|
||||
}
|
||||
|
||||
// generateHeartbeatUserMetric creates the heartbeat user metric and includes the relevant data.
|
||||
func (d *distinctionUtility) generateHeartbeatUserMetric() proton.ObservabilityMetric {
|
||||
return generateHeartbeatMetric(
|
||||
d.getUserPlanSafe(),
|
||||
d.getEmailClientUserAgent(),
|
||||
getEnabled(d.settingsGetter.GetProxyAllowed()),
|
||||
getEnabled(d.getBetaAccessEnabled()),
|
||||
formatBool(d.heartbeatData.receivedOtherError),
|
||||
formatBool(d.heartbeatData.receivedSyncError),
|
||||
formatBool(d.heartbeatData.receivedEventLoopError),
|
||||
)
|
||||
}
|
||||
|
||||
func generateHeartbeatMetric(plan, mailClient, dohEnabled, betaAccess, otherError, syncError, eventLoopError string) proton.ObservabilityMetric {
|
||||
return proton.ObservabilityMetric{
|
||||
Name: genericHeartbeatSchemaName,
|
||||
Version: 1,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Data: map[string]interface{}{
|
||||
"Value": 1,
|
||||
"Labels": map[string]string{
|
||||
"plan": plan,
|
||||
"mailClient": mailClient,
|
||||
"dohEnabled": dohEnabled,
|
||||
"betaAccessEnabled": betaAccess,
|
||||
"receivedOtherError": otherError,
|
||||
"receivedSyncError": syncError,
|
||||
"receivedEventLoopError": eventLoopError,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
121
internal/services/observability/plan_utils.go
Normal file
121
internal/services/observability/plan_utils.go
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/gluon/async"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
const (
|
||||
planUnknown = "unknown"
|
||||
planOther = "other"
|
||||
planBusiness = "business"
|
||||
planIndividual = "individual"
|
||||
planGroup = "group"
|
||||
)
|
||||
|
||||
var planHierarchy = map[string]int{ //nolint:gochecknoglobals
|
||||
planBusiness: 4,
|
||||
planGroup: 3,
|
||||
planIndividual: 2,
|
||||
planOther: 1,
|
||||
planUnknown: 0,
|
||||
}
|
||||
|
||||
type planGetter interface {
|
||||
GetOrganizationData(ctx context.Context) (proton.OrganizationResponse, error)
|
||||
}
|
||||
|
||||
func isHigherPriority(currentPlan, newPlan string) bool {
|
||||
newRank, ok := planHierarchy[newPlan]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
currentRank, ok2 := planHierarchy[currentPlan]
|
||||
if !ok2 {
|
||||
return true // we don't have a valid plan, might as well replace it
|
||||
}
|
||||
|
||||
return newRank > currentRank
|
||||
}
|
||||
|
||||
func mapUserPlan(planName string) string {
|
||||
if planName == "" {
|
||||
return planUnknown
|
||||
}
|
||||
switch strings.TrimSpace(strings.ToLower(planName)) {
|
||||
case "mail2022":
|
||||
return planIndividual
|
||||
case "bundle2022":
|
||||
return planIndividual
|
||||
case "family2022":
|
||||
return planGroup
|
||||
case "visionary2022":
|
||||
return planGroup
|
||||
case "mailpro2022":
|
||||
return planBusiness
|
||||
case "planbiz2024":
|
||||
return planBusiness
|
||||
case "bundlepro2022":
|
||||
return planBusiness
|
||||
case "bundlepro2024":
|
||||
return planBusiness
|
||||
case "duo2024":
|
||||
return planGroup
|
||||
|
||||
default:
|
||||
return planOther
|
||||
}
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) setUserPlan(planName string) {
|
||||
if planName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
d.userPlanLock.Lock()
|
||||
defer d.userPlanLock.Unlock()
|
||||
|
||||
userPlanMapped := mapUserPlan(planName)
|
||||
if isHigherPriority(d.userPlanUnsafe, userPlanMapped) {
|
||||
d.userPlanUnsafe = userPlanMapped
|
||||
}
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) registerUserPlan(ctx context.Context, getter planGetter, panicHandler async.PanicHandler) {
|
||||
go func() {
|
||||
defer async.HandlePanic(panicHandler)
|
||||
|
||||
orgRes, err := getter.GetOrganizationData(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
d.setUserPlan(orgRes.Organization.PlanName)
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *distinctionUtility) getUserPlanSafe() string {
|
||||
d.userPlanLock.Lock()
|
||||
defer d.userPlanLock.Unlock()
|
||||
return d.userPlanUnsafe
|
||||
}
|
||||
@ -36,13 +36,18 @@ const (
|
||||
maxBatchSize = 1000
|
||||
)
|
||||
|
||||
type PushObsMetricFn func(metric proton.ObservabilityMetric)
|
||||
|
||||
type client struct {
|
||||
isTelemetryEnabled func(context.Context) bool
|
||||
sendMetrics func(context.Context, proton.ObservabilityBatch) error
|
||||
}
|
||||
|
||||
// Sender - interface maps to the observability service methods,
|
||||
// so we can easily pass them down to relevant components.
|
||||
type Sender interface {
|
||||
AddMetrics(metrics ...proton.ObservabilityMetric)
|
||||
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
@ -62,6 +67,8 @@ type Service struct {
|
||||
|
||||
userClientStore map[string]*client
|
||||
userClientStoreLock sync.Mutex
|
||||
|
||||
distinctionUtility *distinctionUtility
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||
@ -85,11 +92,19 @@ func NewService(ctx context.Context, panicHandler async.PanicHandler) *Service {
|
||||
userClientStore: make(map[string]*client),
|
||||
}
|
||||
|
||||
service.distinctionUtility = newDistinctionUtility(ctx, panicHandler, service)
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) Run() {
|
||||
// Run starts the observability service goroutine.
|
||||
// The function also sets some utility functions to a helper struct aimed at differentiating the amount of users sending metric updates.
|
||||
func (s *Service) Run(settingsGetter settingsGetter) {
|
||||
s.log.Info("Starting service")
|
||||
|
||||
s.distinctionUtility.setSettingsGetter(settingsGetter)
|
||||
s.distinctionUtility.runHeartbeat()
|
||||
|
||||
go func() {
|
||||
s.start()
|
||||
}()
|
||||
@ -200,7 +215,7 @@ func (s *Service) scheduleDispatch() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
||||
func (s *Service) addMetrics(metric ...proton.ObservabilityMetric) {
|
||||
s.withMetricStoreLock(func() {
|
||||
metricStoreLength := len(s.metricStore)
|
||||
if metricStoreLength >= maxStorageSize {
|
||||
@ -209,12 +224,32 @@ func (s *Service) AddMetric(metric proton.ObservabilityMetric) {
|
||||
dropCount := metricStoreLength - maxStorageSize + 1
|
||||
s.metricStore = s.metricStore[dropCount:]
|
||||
}
|
||||
s.metricStore = append(s.metricStore, metric)
|
||||
s.metricStore = append(s.metricStore, metric...)
|
||||
})
|
||||
|
||||
// If the context has been cancelled i.e. the service has been stopped then we should be free to exit.
|
||||
if s.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.sendSignal(s.signalDataArrived)
|
||||
}
|
||||
|
||||
// addMetricsIfClients - will append a metric only if there are authenticated clients
|
||||
// via which we can reach the endpoint.
|
||||
func (s *Service) addMetricsIfClients(metric ...proton.ObservabilityMetric) {
|
||||
hasClients := false
|
||||
s.withUserClientStoreLock(func() {
|
||||
hasClients = len(s.userClientStore) > 0
|
||||
})
|
||||
|
||||
if !hasClients {
|
||||
return
|
||||
}
|
||||
|
||||
s.addMetrics(metric...)
|
||||
}
|
||||
|
||||
func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client, telemetryService *telemetry.Service) {
|
||||
s.log.Info("Registering user client, ID:", userID)
|
||||
|
||||
@ -225,6 +260,8 @@ func (s *Service) RegisterUserClient(userID string, protonClient *proton.Client,
|
||||
}
|
||||
})
|
||||
|
||||
s.distinctionUtility.registerUserPlan(s.ctx, protonClient, s.panicHandler)
|
||||
|
||||
// There may be a case where we already have metric updates stored, so try to flush;
|
||||
s.sendSignal(s.signalDataArrived)
|
||||
}
|
||||
@ -279,3 +316,25 @@ func (s *Service) sendSignal(channel chan struct{}) {
|
||||
func ModifyThrottlePeriod(duration time.Duration) {
|
||||
throttleDuration = duration
|
||||
}
|
||||
|
||||
func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
|
||||
s.addMetrics(metrics...)
|
||||
}
|
||||
|
||||
// AddDistinctMetrics - sends an additional metric related to the user, so we can determine
|
||||
// what number of events come from what number of users.
|
||||
// As the binning interval is what allows us to do this we
|
||||
// should not send these if there are no logged-in users at that moment.
|
||||
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) {
|
||||
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
|
||||
s.addMetricsIfClients(metrics...)
|
||||
}
|
||||
|
||||
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
|
||||
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
|
||||
s.distinctionUtility.heartbeatTicker.Reset(duration)
|
||||
}
|
||||
|
||||
func ModifyUserMetricInterval(duration time.Duration) {
|
||||
updateInterval = duration
|
||||
}
|
||||
|
||||
106
internal/services/observability/test_utils.go
Normal file
106
internal/services/observability/test_utils.go
Normal file
@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
)
|
||||
|
||||
func GenerateAllUsedDistinctionMetricPermutations() []proton.ObservabilityMetric {
|
||||
planValues := []string{
|
||||
planUnknown,
|
||||
planOther,
|
||||
planBusiness,
|
||||
planIndividual,
|
||||
planGroup}
|
||||
mailClientValues := []string{
|
||||
emailAgentAppleMail,
|
||||
emailAgentOutlook,
|
||||
emailAgentThunderbird,
|
||||
emailAgentOther,
|
||||
emailAgentUnknown,
|
||||
}
|
||||
enabledValues := []string{
|
||||
getEnabled(true), getEnabled(false),
|
||||
}
|
||||
|
||||
var metrics []proton.ObservabilityMetric
|
||||
|
||||
for _, schemaName := range errorSchemaMap {
|
||||
for _, plan := range planValues {
|
||||
for _, mailClient := range mailClientValues {
|
||||
for _, dohEnabled := range enabledValues {
|
||||
for _, betaAccess := range enabledValues {
|
||||
metrics = append(metrics, generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
|
||||
func GenerateAllHeartbeatMetricPermutations() []proton.ObservabilityMetric {
|
||||
planValues := []string{
|
||||
planUnknown,
|
||||
planOther,
|
||||
planBusiness,
|
||||
planIndividual,
|
||||
planGroup}
|
||||
mailClientValues := []string{
|
||||
emailAgentAppleMail,
|
||||
emailAgentOutlook,
|
||||
emailAgentThunderbird,
|
||||
emailAgentOther,
|
||||
emailAgentUnknown,
|
||||
}
|
||||
enabledValues := []string{
|
||||
getEnabled(true), getEnabled(false),
|
||||
}
|
||||
|
||||
trueFalseValues := []string{
|
||||
"true", "false",
|
||||
}
|
||||
|
||||
var metrics []proton.ObservabilityMetric
|
||||
for _, plan := range planValues {
|
||||
for _, mailClient := range mailClientValues {
|
||||
for _, dohEnabled := range enabledValues {
|
||||
for _, betaAccess := range enabledValues {
|
||||
for _, receivedOtherError := range trueFalseValues {
|
||||
for _, receivedSyncError := range trueFalseValues {
|
||||
for _, receivedEventLoopError := range trueFalseValues {
|
||||
metrics = append(metrics,
|
||||
generateHeartbeatMetric(plan,
|
||||
mailClient,
|
||||
dohEnabled,
|
||||
betaAccess,
|
||||
receivedOtherError,
|
||||
receivedSyncError,
|
||||
receivedEventLoopError,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return metrics
|
||||
}
|
||||
68
internal/services/observability/utils.go
Normal file
68
internal/services/observability/utils.go
Normal file
@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
)
|
||||
|
||||
// settingsGetter - interface that maps to bridge object methods such that we
|
||||
// can pass the whole object instead of individual function callbacks.
|
||||
type settingsGetter interface {
|
||||
GetCurrentUserAgent() string
|
||||
GetProxyAllowed() bool
|
||||
GetUpdateChannel() updater.Channel
|
||||
}
|
||||
|
||||
// User agent mapping.
|
||||
const (
|
||||
emailAgentAppleMail = "apple_mail"
|
||||
emailAgentOutlook = "outlook"
|
||||
emailAgentThunderbird = "thunderbird"
|
||||
emailAgentOther = "other"
|
||||
emailAgentUnknown = "unknown"
|
||||
)
|
||||
|
||||
func matchUserAgent(userAgent string) string {
|
||||
if userAgent == "" {
|
||||
return emailAgentUnknown
|
||||
}
|
||||
|
||||
userAgent = strings.ToLower(userAgent)
|
||||
switch {
|
||||
case strings.Contains(userAgent, "outlook"):
|
||||
return emailAgentOutlook
|
||||
case strings.Contains(userAgent, "thunderbird"):
|
||||
return emailAgentThunderbird
|
||||
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "mail"):
|
||||
return emailAgentAppleMail
|
||||
case strings.Contains(userAgent, "mac") && strings.Contains(userAgent, "notes"):
|
||||
return emailAgentUnknown
|
||||
default:
|
||||
return emailAgentOther
|
||||
}
|
||||
}
|
||||
|
||||
func getEnabled(value bool) string {
|
||||
if !value {
|
||||
return "disabled"
|
||||
}
|
||||
return "enabled"
|
||||
}
|
||||
111
internal/services/observability/utils_test.go
Normal file
111
internal/services/observability/utils_test.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2024 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package observability
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMatchUserAgent(t *testing.T) {
|
||||
type agentParseResult struct {
|
||||
agent string
|
||||
result string
|
||||
}
|
||||
|
||||
testCases := []agentParseResult{
|
||||
{
|
||||
agent: "Microsoft Outlook/16.0.17928.20114 (Windows 11 Version 23H2)",
|
||||
result: emailAgentOutlook,
|
||||
},
|
||||
{
|
||||
agent: "Mailbird/3.0.18.0 (Windows 11 Version 23H2)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Microsoft Outlook/16.0.17830.20166 (Windows 11 Version 23H2)",
|
||||
result: emailAgentOutlook,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Mail/16.0-3776.700.51 (macOS 14.6)",
|
||||
result: emailAgentAppleMail,
|
||||
},
|
||||
{
|
||||
agent: "/ (Windows 11 Version 23H2)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Microsoft Outlook for Mac/16.88.0-BUILDDAY (macOS 14.6)",
|
||||
result: emailAgentOutlook,
|
||||
},
|
||||
{
|
||||
agent: "/ (macOS 14.5)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "/ (Freedesktop SDK 23.08 (Flatpak runtime))",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Mail/16.0-3774.600.62 (macOS 14.5)",
|
||||
result: emailAgentAppleMail,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Notes/4.11-2817 (macOS 14.6)",
|
||||
result: emailAgentUnknown,
|
||||
},
|
||||
{
|
||||
agent: "NoClient/0.0.1 (macOS 14.6)",
|
||||
result: emailAgentOther,
|
||||
},
|
||||
{
|
||||
agent: "Thunderbird/115.15.0 (Ubuntu 20.04.6 LTS)",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "Thunderbird/115.14.0 (macOS 14.6)",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "Thunderbird/115.10.2 (Windows 11 Version 23H2)",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "Mac OS X Notes/4.9-1965 (macOS Monterey (12.0))",
|
||||
result: emailAgentUnknown,
|
||||
},
|
||||
{
|
||||
agent: " Thunderbird/115.14.0 (macOS 14.6) ",
|
||||
result: emailAgentThunderbird,
|
||||
},
|
||||
{
|
||||
agent: "",
|
||||
result: emailAgentUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
require.Equal(t, testCase.result, matchUserAgent(testCase.agent))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBool(t *testing.T) {
|
||||
require.Equal(t, "false", formatBool(false))
|
||||
require.Equal(t, "true", formatBool(true))
|
||||
}
|
||||
Reference in New Issue
Block a user