github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/core/quality/mysterium_morqa.go (about) 1 /* 2 * Copyright (C) 2018 The "MysteriumNetwork/node" Authors. 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18 package quality 19 20 import ( 21 "bytes" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "sync" 27 "time" 28 29 gocache "github.com/patrickmn/go-cache" 30 "github.com/rs/zerolog/log" 31 "google.golang.org/protobuf/proto" 32 33 "github.com/mysteriumnetwork/metrics" 34 "github.com/mysteriumnetwork/node/core/node" 35 "github.com/mysteriumnetwork/node/identity" 36 "github.com/mysteriumnetwork/node/requests" 37 ) 38 39 const ( 40 mysteriumMorqaAgentName = "goclient-v0.1" 41 42 maxBatchMetricsToKeep = 100 43 maxBatchMetricsToWait = 30 * time.Second 44 45 maxBatchSentFails = 3 46 ) 47 48 type metric struct { 49 owner string 50 event *metrics.Event 51 } 52 53 // MysteriumMORQA HTTP client for Mysterium Quality Oracle - MORQA. 54 type MysteriumMORQA struct { 55 baseURL string 56 client *requests.HTTPClient 57 signer identity.SignerFactory 58 59 batch map[string]*batchWithTimeout 60 eventsMu sync.RWMutex 61 metrics chan metric 62 63 once sync.Once 64 stop chan struct{} 65 66 cache *gocache.Cache 67 } 68 69 type batchWithTimeout struct { 70 batch *metrics.Batch 71 failed int 72 lastFail time.Time 73 } 74 75 // NewMorqaClient creates Mysterium Morqa client with a real communication. 76 func NewMorqaClient(httpClient *requests.HTTPClient, baseURL string, signer identity.SignerFactory) *MysteriumMORQA { 77 morqa := &MysteriumMORQA{ 78 baseURL: baseURL, 79 client: httpClient, 80 signer: signer, 81 82 batch: make(map[string]*batchWithTimeout), 83 metrics: make(chan metric, 1000*maxBatchMetricsToKeep), 84 stop: make(chan struct{}), 85 86 cache: gocache.New(1*time.Minute, 10*time.Minute), 87 } 88 89 return morqa 90 } 91 92 // Start starts sending batch metrics to the Morqa server. 93 func (m *MysteriumMORQA) Start() { 94 trigger := time.After(maxBatchMetricsToWait) 95 96 for { 97 select { 98 case metric := <-m.metrics: 99 m.addMetric(metric) 100 101 m.eventsMu.RLock() 102 size := len(m.batch[metric.owner].batch.Events) 103 failed := m.batch[metric.owner].failed 104 lastFailed := m.batch[metric.owner].lastFail 105 m.eventsMu.RUnlock() 106 107 if size < maxBatchMetricsToKeep { 108 continue 109 } 110 111 if failed > maxBatchSentFails { 112 m.eventsMu.Lock() 113 delete(m.batch, metric.owner) 114 m.eventsMu.Unlock() 115 } 116 117 if time.Now().Before(lastFailed.Add(maxBatchMetricsToWait)) { 118 continue 119 } 120 121 case <-trigger: 122 case <-m.stop: 123 return 124 } 125 126 m.sendAll() 127 128 trigger = time.After(maxBatchMetricsToWait) 129 } 130 } 131 132 // signBatch creates signature for the metrics batch. 133 func (m *MysteriumMORQA) signBatch(owner string, batch *metrics.Batch) (string, error) { 134 bin, err := proto.Marshal(batch) 135 if err != nil { 136 return "", fmt.Errorf("failed to marshal metrics event: %w", err) 137 } 138 139 signature, err := m.signer(identity.FromAddress(owner)).Sign(bin) 140 if err != nil { 141 return "", fmt.Errorf("failed to sign metrics event: %w", err) 142 } 143 144 return signature.Base64(), nil 145 } 146 147 // Stop sends the final metrics to the MORQA and stops the sending process. 148 func (m *MysteriumMORQA) Stop() { 149 m.once.Do(func() { 150 close(m.stop) 151 }) 152 153 m.sendAll() 154 } 155 156 func (m *MysteriumMORQA) addMetric(metric metric) { 157 m.eventsMu.Lock() 158 defer m.eventsMu.Unlock() 159 160 batch, ok := m.batch[metric.owner] 161 if !ok || batch == nil { 162 batch = &batchWithTimeout{ 163 batch: &metrics.Batch{}, 164 } 165 m.batch[metric.owner] = batch 166 } 167 168 switch metric.event.Metric.(type) { 169 case *metrics.Event_SessionStatisticsPayload: // Allow sending only the last session statistics payload in a single batch. 170 for i, e := range m.batch[metric.owner].batch.Events { 171 if _, ok := e.Metric.(*metrics.Event_SessionStatisticsPayload); ok { 172 m.batch[metric.owner].batch.Events[i] = metric.event 173 return 174 } 175 } 176 case *metrics.Event_PingEvent: // Allow sending only the last ping event in a single batch. 177 for i, e := range m.batch[metric.owner].batch.Events { 178 if _, ok := e.Metric.(*metrics.Event_PingEvent); ok { 179 m.batch[metric.owner].batch.Events[i] = metric.event 180 return 181 } 182 } 183 } 184 185 batch.batch.Events = append(batch.batch.Events, metric.event) 186 m.batch[metric.owner] = batch 187 } 188 189 func (m *MysteriumMORQA) sendAll() { 190 m.eventsMu.Lock() 191 defer m.eventsMu.Unlock() 192 193 for owner := range m.batch { 194 err := m.sendMetrics(owner) 195 if err != nil { 196 log.Error().Err(err).Msgf("Failed to sent batch metrics request, %v", len(m.batch[owner].batch.GetEvents())) 197 m.batch[owner].failed++ 198 m.batch[owner].lastFail = time.Now() 199 } 200 } 201 } 202 203 func (m *MysteriumMORQA) sendMetrics(owner string) error { 204 if len(m.batch[owner].batch.Events) == 0 { 205 return nil 206 } 207 208 batch := m.batch[owner].batch 209 210 signature, err := m.signBatch(owner, batch) 211 if err != nil { 212 log.Error().Err(err).Msg("Failed to sign metrics event") 213 } 214 215 sb := &metrics.SignedBatch{ 216 Signature: signature, 217 Batch: batch, 218 } 219 220 request, err := m.newRequestBinary(http.MethodPost, "batch", sb) 221 if err != nil { 222 return err 223 } 224 225 request.Close = true 226 227 response, err := m.client.Do(request) 228 if err != nil { 229 return err 230 } 231 defer response.Body.Close() 232 233 if err := parseResponseError(response); err != nil { 234 return err 235 } 236 237 delete(m.batch, owner) 238 239 return nil 240 } 241 242 // MonitoringStatus retrieve monitoring statuses. 243 func (m *MysteriumMORQA) MonitoringStatus(providerIds []string) MonitoringStatusResponse { 244 payload := struct { 245 Providers []string `json:"providers"` 246 }{Providers: providerIds} 247 248 request, err := m.newRequestJSON(http.MethodPost, "providers/monitoring-status", &payload) 249 if err != nil { 250 log.Warn().Err(err).Msg("Failed creating request POST: /providers/monitoring-status") 251 return map[string]MonitoringStatus{} 252 } 253 254 var r MonitoringStatusResponse 255 if err = m.doRequestAndCacheResponse(request, time.Minute, &r); err != nil { 256 log.Warn().Err(err).Msg("Failed parsing response from POST: /providers/monitoring-status") 257 return nil 258 } 259 260 return r 261 } 262 263 // ProposalsQuality returns a list of proposals with a quality parameters. 264 func (m *MysteriumMORQA) ProposalsQuality() []ProposalQuality { 265 request, err := m.newRequestJSON(http.MethodGet, "providers/quality", nil) 266 if err != nil { 267 log.Warn().Err(err).Msg("Failed to create proposals quality request") 268 269 return nil 270 } 271 272 var qualityResponse []ProposalQuality 273 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &qualityResponse); err != nil { 274 log.Warn().Err(err).Msg("Failed to parse proposals quality") 275 276 return nil 277 } 278 279 return qualityResponse 280 } 281 282 // ProviderSessions fetch provider sessions from prometheus 283 func (m *MysteriumMORQA) ProviderSessions(providerID string) []ProviderSession { 284 request, err := m.newRequestJSON(http.MethodGet, fmt.Sprintf("providers/sessions?provider_id=%s", providerID), nil) 285 if err != nil { 286 log.Warn().Err(err).Msg("Failed to create proposals quality request") 287 288 return nil 289 } 290 291 var responseBody struct { 292 Connects []ProviderSession `json:"connects"` 293 } 294 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &responseBody); err != nil { 295 log.Warn().Err(err).Msg("Failed to parse proposals quality") 296 297 return nil 298 } 299 return responseBody.Connects 300 } 301 302 // ProviderStatuses fetch provider connectivity statuses from quality oracle. 303 func (m *MysteriumMORQA) ProviderStatuses(providerID string) (node.MonitoringAgentStatuses, error) { 304 id := identity.FromAddress(providerID) 305 306 request, err := requests.NewSignedGetRequest(m.baseURL, "provider/statuses", m.signer(id)) 307 if err != nil { 308 return nil, err 309 } 310 311 var statuses node.MonitoringAgentStatuses 312 313 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &statuses); err != nil { 314 log.Err(err).Msg("Failed to parse provider monitoring agent statuses") 315 return nil, err 316 } 317 318 return statuses, nil 319 } 320 321 // ProviderSessionsList fetch provider sessions list from quality oracle. 322 func (m *MysteriumMORQA) ProviderSessionsList(id identity.Identity, rangeTime string) ([]node.SessionItem, error) { 323 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/sessions?range=%s", rangeTime), m.signer(id)) 324 if err != nil { 325 return nil, err 326 } 327 328 var sessions []node.SessionItem 329 330 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &sessions); err != nil { 331 log.Err(err).Msg("Failed to parse provider monitoring sessions list") 332 return nil, err 333 } 334 335 return sessions, nil 336 } 337 338 // ProviderTransferredData fetch total traffic served by the provider during a period of time from quality oracle. 339 func (m *MysteriumMORQA) ProviderTransferredData(id identity.Identity, rangeTime string) (node.TransferredData, error) { 340 var data node.TransferredData 341 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/transferred-data?range=%s", rangeTime), m.signer(id)) 342 if err != nil { 343 return data, err 344 } 345 346 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &data); err != nil { 347 log.Err(err).Msg("Failed to parse provider transferred data") 348 return data, err 349 } 350 351 return data, nil 352 } 353 354 // ProviderSessionsCount fetch provider sessions number from quality oracle. 355 func (m *MysteriumMORQA) ProviderSessionsCount(id identity.Identity, rangeTime string) (node.SessionsCount, error) { 356 var count node.SessionsCount 357 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/sessions-count?range=%s", rangeTime), m.signer(id)) 358 if err != nil { 359 return count, err 360 } 361 362 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &count); err != nil { 363 log.Err(err).Msg("Failed to parse provider monitoring sessions count") 364 return count, err 365 } 366 367 return count, nil 368 } 369 370 // ProviderConsumersCount fetch consumers number served by provider from quality oracle. 371 func (m *MysteriumMORQA) ProviderConsumersCount(id identity.Identity, rangeTime string) (node.ConsumersCount, error) { 372 var count node.ConsumersCount 373 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/consumers-count?range=%s", rangeTime), m.signer(id)) 374 if err != nil { 375 return count, err 376 } 377 378 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &count); err != nil { 379 log.Err(err).Msg("Failed to parse provider monitoring consumers count") 380 return count, err 381 } 382 383 return count, nil 384 } 385 386 // ProviderEarningsSeries fetch earnings data series metrics from quality oracle. 387 func (m *MysteriumMORQA) ProviderEarningsSeries(id identity.Identity, rangeTime string) (node.EarningsSeries, error) { 388 var data node.EarningsSeries 389 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/series-earnings?range=%s", rangeTime), m.signer(id)) 390 if err != nil { 391 return data, err 392 } 393 394 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &data); err != nil { 395 log.Err(err).Msg("Failed to parse provider series earnings") 396 return data, err 397 } 398 399 return data, nil 400 } 401 402 // ProviderSessionsSeries fetch earnings data series metrics from quality oracle. 403 func (m *MysteriumMORQA) ProviderSessionsSeries(id identity.Identity, rangeTime string) (node.SessionsSeries, error) { 404 var data node.SessionsSeries 405 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/series-sessions?range=%s", rangeTime), m.signer(id)) 406 if err != nil { 407 return data, err 408 } 409 410 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &data); err != nil { 411 log.Err(err).Msg("Failed to parse provider series sessions") 412 return data, err 413 } 414 415 return data, nil 416 } 417 418 // ProviderTransferredDataSeries fetch transferred bytes data series metrics from quality oracle. 419 func (m *MysteriumMORQA) ProviderTransferredDataSeries(id identity.Identity, rangeTime string) (node.TransferredDataSeries, error) { 420 var data node.TransferredDataSeries 421 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/series-data?range=%s", rangeTime), m.signer(id)) 422 if err != nil { 423 return data, err 424 } 425 426 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &data); err != nil { 427 log.Err(err).Msg("Failed to parse provider series data") 428 return data, err 429 } 430 431 return data, nil 432 } 433 434 // ProviderServiceEarnings fetch earnings per service type. 435 func (m *MysteriumMORQA) ProviderServiceEarnings(id identity.Identity) (node.EarningsPerService, error) { 436 var data node.EarningsPerService 437 request, err := requests.NewSignedGetRequest(m.baseURL, "provider/service-earnings", m.signer(id)) 438 if err != nil { 439 return data, err 440 } 441 442 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &data); err != nil { 443 log.Err(err).Msg("Failed to parse service earnings") 444 return data, err 445 } 446 447 return data, nil 448 } 449 450 // ProviderActivityStats fetch provider activity stats from quality oracle. 451 func (m *MysteriumMORQA) ProviderActivityStats(id identity.Identity) (node.ActivityStats, error) { 452 var stats node.ActivityStats 453 request, err := requests.NewSignedGetRequest(m.baseURL, fmt.Sprintf("provider/activity-stats"), m.signer(id)) 454 if err != nil { 455 return stats, err 456 } 457 458 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &stats); err != nil { 459 log.Err(err).Msg("Failed to parse provider activity stats") 460 return stats, err 461 } 462 463 return stats, nil 464 } 465 466 // ProviderQuality fetch provider quality from quality oracle. 467 func (m *MysteriumMORQA) ProviderQuality(id identity.Identity) (node.QualityInfo, error) { 468 var res node.QualityInfo 469 request, err := requests.NewSignedGetRequest(m.baseURL, "provider/quality", m.signer(id)) 470 if err != nil { 471 return res, err 472 } 473 474 if err = m.doRequestAndCacheResponse(request, 10*time.Minute, &res); err != nil { 475 log.Err(err).Msg("Failed to parse provider quality") 476 return res, err 477 } 478 479 return res, nil 480 } 481 482 // SendMetric submits new metric. 483 func (m *MysteriumMORQA) SendMetric(id string, event *metrics.Event) error { 484 select { 485 case m.metrics <- metric{owner: id, event: event}: 486 case <-time.After(10 * time.Second): 487 log.Warn().Msgf("Timeout waiting for metric store, skipping it %v:%v", id, event) 488 } 489 490 return nil 491 } 492 493 func (m *MysteriumMORQA) newRequest(method, path string, body []byte) (*http.Request, error) { 494 url := m.baseURL 495 if len(path) > 0 { 496 url = fmt.Sprintf("%v/%v", url, path) 497 } 498 499 request, err := http.NewRequest(method, url, bytes.NewBuffer(body)) 500 request.Header.Set("User-Agent", mysteriumMorqaAgentName) 501 request.Header.Set("Accept", "application/json") 502 503 return request, err 504 } 505 506 func (m *MysteriumMORQA) newRequestJSON(method, path string, payload interface{}) (*http.Request, error) { 507 payloadBody, err := json.Marshal(payload) 508 if err != nil { 509 return nil, err 510 } 511 512 req, err := m.newRequest(method, path, payloadBody) 513 req.Header.Set("Accept", "application/json") 514 515 return req, err 516 } 517 518 func (m *MysteriumMORQA) newRequestBinary(method, path string, payload proto.Message) (*http.Request, error) { 519 payloadBody, err := proto.Marshal(payload) 520 if err != nil { 521 return nil, err 522 } 523 524 request, err := m.newRequest(method, path, payloadBody) 525 request.Header.Set("Content-Type", "application/octet-stream") 526 527 return request, err 528 } 529 530 func (m *MysteriumMORQA) doRequestAndCacheResponse(request *http.Request, ttl time.Duration, dto interface{}) error { 531 if err, ok := m.cache.Get("err" + request.URL.RequestURI()); ok { 532 return err.(error) 533 } 534 535 if resp, ok := m.cache.Get(request.URL.RequestURI()); ok { 536 serializedDTO, err := json.Marshal(resp) 537 if err != nil { 538 return err 539 } 540 541 return json.Unmarshal(serializedDTO, dto) 542 } 543 544 response, err := m.client.Do(request) 545 if err != nil { 546 m.cache.Set("err"+request.URL.RequestURI(), err, ttl) 547 log.Warn().Err(err).Msg("Failed to request proposals quality") 548 549 return nil 550 } 551 defer response.Body.Close() 552 553 if err := parseResponseJSON(response, dto); err != nil { 554 m.cache.Set("err"+request.URL.RequestURI(), err, ttl) 555 return err 556 } 557 558 m.cache.Set(request.URL.RequestURI(), dto, ttl) 559 560 return nil 561 } 562 563 func parseResponseJSON(response *http.Response, dto interface{}) error { 564 responseJSON, err := io.ReadAll(response.Body) 565 if err != nil { 566 return err 567 } 568 569 return json.Unmarshal(responseJSON, dto) 570 } 571 572 type errorDTO struct { 573 Message string `json:"message"` 574 } 575 576 // Sometimes we can get json message with single "message" field which represents error - try to get that. 577 func parseResponseError(response *http.Response) error { 578 if response.StatusCode >= 200 && response.StatusCode < 300 { 579 return nil 580 } 581 582 var parsedBody errorDTO 583 584 var message string 585 586 err := parseResponseJSON(response, &parsedBody) 587 if err != nil { 588 message = err.Error() 589 } else { 590 message = parsedBody.Message 591 } 592 593 return fmt.Errorf("server response invalid: %s (%s). Possible error: %s", response.Status, response.Request.URL, message) 594 }