[Integrations] small refactoring (#1752)
* feat(backend): small refactoring in integrations * feat(backend): ignore context timeout error
This commit is contained in:
parent
ef95bdeca5
commit
f8a8dfa459
32 changed files with 467 additions and 396 deletions
|
|
@ -1,17 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"openreplay/backend/pkg/db/postgres/pool"
|
|
||||||
"openreplay/backend/pkg/integrations"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
|
||||||
config "openreplay/backend/internal/config/integrations"
|
config "openreplay/backend/internal/config/integrations"
|
||||||
"openreplay/backend/internal/integrations/clientManager"
|
"openreplay/backend/pkg/integrations"
|
||||||
"openreplay/backend/pkg/intervals"
|
|
||||||
"openreplay/backend/pkg/metrics"
|
"openreplay/backend/pkg/metrics"
|
||||||
databaseMetrics "openreplay/backend/pkg/metrics/database"
|
databaseMetrics "openreplay/backend/pkg/metrics/database"
|
||||||
"openreplay/backend/pkg/queue"
|
"openreplay/backend/pkg/queue"
|
||||||
|
|
@ -26,89 +25,38 @@ func main() {
|
||||||
|
|
||||||
cfg := config.New()
|
cfg := config.New()
|
||||||
|
|
||||||
// Init postgres connection
|
pgConn, err := pgx.Connect(context.Background(), cfg.Postgres.String())
|
||||||
pgConn, err := pool.New(cfg.Postgres.String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("can't init postgres connection: %s", err)
|
log.Fatalf("can't init postgres connection: %s", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer pgConn.Close()
|
defer pgConn.Close(context.Background())
|
||||||
|
|
||||||
tokenizer := token.NewTokenizer(cfg.TokenSecret)
|
|
||||||
|
|
||||||
manager := clientManager.NewManager()
|
|
||||||
|
|
||||||
producer := queue.NewProducer(cfg.MessageSizeLimit, true)
|
producer := queue.NewProducer(cfg.MessageSizeLimit, true)
|
||||||
defer producer.Close(15000)
|
defer producer.Close(15000)
|
||||||
|
|
||||||
// TODO: rework with integration manager
|
storage := integrations.NewStorage(pgConn)
|
||||||
listener, err := integrations.New(pgConn, cfg.Postgres.String())
|
if err := storage.Listen(); err != nil {
|
||||||
|
log.Fatalf("Listener error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := integrations.New(cfg, storage, producer, integrations.NewManager(), token.NewTokenizer(cfg.TokenSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Postgres listener error: %v\n", err)
|
log.Fatalf("Listener error: %v", err)
|
||||||
log.Fatalf("Postgres listener error")
|
|
||||||
}
|
}
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
listener.IterateIntegrationsOrdered(func(i *integrations.Integration, err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Postgres error: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Integration initialization: %v\n", *i)
|
|
||||||
err = manager.Update(i)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Integration parse error: %v | Integration: %v\n", err, *i)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sigchan := make(chan os.Signal, 1)
|
sigchan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
tick := time.Tick(intervals.INTEGRATIONS_REQUEST_INTERVAL * time.Millisecond)
|
|
||||||
|
|
||||||
log.Printf("Integration service started\n")
|
log.Printf("Integration service started\n")
|
||||||
manager.RequestAll()
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigchan:
|
case sig := <-sigchan:
|
||||||
log.Printf("Caught signal %v: terminating\n", sig)
|
log.Printf("Caught signal %v: terminating\n", sig)
|
||||||
listener.Close()
|
|
||||||
pgConn.Close()
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case <-tick:
|
|
||||||
log.Printf("Requesting all...\n")
|
|
||||||
manager.RequestAll()
|
|
||||||
case event := <-manager.Events:
|
|
||||||
log.Printf("New integration event: %+v\n", *event.IntegrationEvent)
|
|
||||||
sessionID := event.SessionID
|
|
||||||
if sessionID == 0 {
|
|
||||||
sessData, err := tokenizer.Parse(event.Token)
|
|
||||||
if err != nil && err != token.EXPIRED {
|
|
||||||
log.Printf("Error on token parsing: %v; Token: %v", err, event.Token)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sessionID = sessData.ID
|
|
||||||
}
|
|
||||||
producer.Produce(cfg.TopicAnalytics, sessionID, event.IntegrationEvent.Encode())
|
|
||||||
case err := <-manager.Errors:
|
|
||||||
log.Printf("Integration error: %v\n", err)
|
|
||||||
case i := <-manager.RequestDataUpdates:
|
|
||||||
// log.Printf("Last request integration update: %v || %v\n", i, string(i.RequestData))
|
|
||||||
if err := listener.UpdateIntegrationRequestData(&i); err != nil {
|
|
||||||
log.Printf("Postgres Update request_data error: %v\n", err)
|
|
||||||
}
|
|
||||||
case err := <-listener.Errors:
|
case err := <-listener.Errors:
|
||||||
log.Printf("Postgres listen error: %v\n", err)
|
log.Printf("Listener error: %v", err)
|
||||||
listener.Close()
|
|
||||||
pgConn.Close()
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case iPointer := <-listener.Integrations:
|
|
||||||
log.Printf("Integration update: %v\n", *iPointer)
|
|
||||||
err := manager.Update(iPointer)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Integration parse error: %v | Integration: %v\n", err, *iPointer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package clientManager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"openreplay/backend/internal/integrations/integration"
|
|
||||||
"openreplay/backend/pkg/integrations"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type manager struct {
|
|
||||||
clientMap integration.ClientMap
|
|
||||||
Events chan *integration.SessionErrorEvent
|
|
||||||
Errors chan error
|
|
||||||
RequestDataUpdates chan integrations.Integration // not pointer because it could change in other thread
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager() *manager {
|
|
||||||
return &manager{
|
|
||||||
clientMap: make(integration.ClientMap),
|
|
||||||
RequestDataUpdates: make(chan integrations.Integration, 100),
|
|
||||||
Events: make(chan *integration.SessionErrorEvent, 100),
|
|
||||||
Errors: make(chan error, 100),
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *manager) Update(i *integrations.Integration) error {
|
|
||||||
key := strconv.Itoa(int(i.ProjectID)) + i.Provider
|
|
||||||
if i.Options == nil {
|
|
||||||
delete(m.clientMap, key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c, exists := m.clientMap[key]
|
|
||||||
if !exists {
|
|
||||||
c, err := integration.NewClient(i, m.RequestDataUpdates, m.Events, m.Errors)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.clientMap[key] = c
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return c.Update(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *manager) RequestAll() {
|
|
||||||
for _, c := range m.clientMap {
|
|
||||||
go c.Request()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
package integration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"openreplay/backend/pkg/integrations"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"openreplay/backend/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MAX_ATTEMPTS_IN_A_ROW = 4
|
|
||||||
const MAX_ATTEMPTS = 40
|
|
||||||
const ATTEMPTS_INTERVAL = 3 * 60 * 60 * 1000
|
|
||||||
|
|
||||||
type requester interface {
|
|
||||||
Request(*client) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestData struct {
|
|
||||||
LastMessageTimestamp uint64 // `json:"lastMessageTimestamp, string"`
|
|
||||||
LastMessageId string
|
|
||||||
UnsuccessfullAttemptsCount int
|
|
||||||
LastAttemptTimestamp int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type client struct {
|
|
||||||
requestData
|
|
||||||
requester
|
|
||||||
integration *integrations.Integration
|
|
||||||
// TODO: timeout ?
|
|
||||||
mux sync.Mutex
|
|
||||||
updateChan chan<- integrations.Integration
|
|
||||||
evChan chan<- *SessionErrorEvent
|
|
||||||
errChan chan<- error
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionErrorEvent struct {
|
|
||||||
SessionID uint64
|
|
||||||
Token string
|
|
||||||
*messages.IntegrationEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientMap map[string]*client
|
|
||||||
|
|
||||||
func NewClient(i *integrations.Integration, updateChan chan<- integrations.Integration, evChan chan<- *SessionErrorEvent, errChan chan<- error) (*client, error) {
|
|
||||||
c := new(client)
|
|
||||||
if err := c.Update(i); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(i.RequestData, &c.requestData); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.evChan = evChan
|
|
||||||
c.errChan = errChan
|
|
||||||
c.updateChan = updateChan
|
|
||||||
// TODO: RequestData manager
|
|
||||||
if c.requestData.LastMessageTimestamp == 0 {
|
|
||||||
// ?
|
|
||||||
c.requestData.LastMessageTimestamp = uint64(time.Now().Add(-time.Hour * 24).UnixMilli())
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// from outside
|
|
||||||
func (c *client) Update(i *integrations.Integration) error {
|
|
||||||
c.mux.Lock()
|
|
||||||
defer c.mux.Unlock()
|
|
||||||
var r requester
|
|
||||||
switch i.Provider {
|
|
||||||
case "bugsnag":
|
|
||||||
r = new(bugsnag)
|
|
||||||
case "cloudwatch":
|
|
||||||
r = new(cloudwatch)
|
|
||||||
case "datadog":
|
|
||||||
r = new(datadog)
|
|
||||||
case "elasticsearch":
|
|
||||||
r = new(elasticsearch)
|
|
||||||
case "newrelic":
|
|
||||||
r = new(newrelic)
|
|
||||||
case "rollbar":
|
|
||||||
r = new(rollbar)
|
|
||||||
case "sentry":
|
|
||||||
r = new(sentry)
|
|
||||||
case "stackdriver":
|
|
||||||
r = new(stackdriver)
|
|
||||||
case "sumologic":
|
|
||||||
r = new(sumologic)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(i.Options, r); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.integration = i
|
|
||||||
c.requester = r
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// package scope
|
|
||||||
func (c *client) setLastMessageTimestamp(timestamp uint64) {
|
|
||||||
if timestamp > c.requestData.LastMessageTimestamp {
|
|
||||||
c.requestData.LastMessageTimestamp = timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (c *client) getLastMessageTimestamp() uint64 {
|
|
||||||
return c.requestData.LastMessageTimestamp
|
|
||||||
}
|
|
||||||
func (c *client) setLastMessageId(timestamp uint64, id string) {
|
|
||||||
//if timestamp >= c.requestData.LastMessageTimestamp {
|
|
||||||
c.requestData.LastMessageId = id
|
|
||||||
c.requestData.LastMessageTimestamp = timestamp
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
func (c *client) getLastMessageId() string {
|
|
||||||
return c.requestData.LastMessageId
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *client) handleError(err error) {
|
|
||||||
c.errChan <- fmt.Errorf("%v | Integration: %v", err, *c.integration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thread-safe
|
|
||||||
func (c *client) Request() {
|
|
||||||
c.mux.Lock()
|
|
||||||
defer c.mux.Unlock()
|
|
||||||
if c.requestData.UnsuccessfullAttemptsCount >= MAX_ATTEMPTS ||
|
|
||||||
(c.requestData.UnsuccessfullAttemptsCount >= MAX_ATTEMPTS_IN_A_ROW &&
|
|
||||||
time.Now().UnixMilli()-c.requestData.LastAttemptTimestamp < ATTEMPTS_INTERVAL) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.requestData.LastAttemptTimestamp = time.Now().UnixMilli()
|
|
||||||
err := c.requester.Request(c)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("ERRROR L139")
|
|
||||||
log.Println(err)
|
|
||||||
c.handleError(err)
|
|
||||||
c.requestData.UnsuccessfullAttemptsCount++
|
|
||||||
} else {
|
|
||||||
c.requestData.UnsuccessfullAttemptsCount = 0
|
|
||||||
}
|
|
||||||
rd, err := json.Marshal(c.requestData)
|
|
||||||
if err != nil {
|
|
||||||
c.handleError(err)
|
|
||||||
}
|
|
||||||
// RequestData is a byte array (pointer-like type), but it's replacement
|
|
||||||
// won't affect the previous value sent by channel
|
|
||||||
c.integration.RequestData = rd
|
|
||||||
c.updateChan <- *c.integration
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -34,8 +34,9 @@ type bugsnagEvent struct {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// need result chan and lastMessageTs
|
||||||
func (b *bugsnag) Request(c *client) error {
|
func (b *bugsnag) Request(c *client) error {
|
||||||
sinceTs := c.getLastMessageTimestamp() + 1000 // From next second
|
sinceTs := c.requestData.GetLastMessageTimestamp() + 1000 // From next second
|
||||||
sinceFormatted := time.UnixMilli(int64(sinceTs)).Format(time.RFC3339)
|
sinceFormatted := time.UnixMilli(int64(sinceTs)).Format(time.RFC3339)
|
||||||
requestURL := fmt.Sprintf("https://api.bugsnag.com/projects/%v/events", b.BugsnagProjectId)
|
requestURL := fmt.Sprintf("https://api.bugsnag.com/projects/%v/events", b.BugsnagProjectId)
|
||||||
req, err := http.NewRequest("GET", requestURL, nil)
|
req, err := http.NewRequest("GET", requestURL, nil)
|
||||||
|
|
@ -93,7 +94,7 @@ func (b *bugsnag) Request(c *client) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
timestamp := uint64(parsedTime.UnixMilli())
|
timestamp := uint64(parsedTime.UnixMilli())
|
||||||
c.setLastMessageTimestamp(timestamp)
|
c.requestData.SetLastMessageTimestamp(timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
Token: token,
|
Token: token,
|
||||||
110
backend/pkg/integrations/clients/client.go
Normal file
110
backend/pkg/integrations/clients/client.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package clients
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"openreplay/backend/pkg/integrations/model"
|
||||||
|
"openreplay/backend/pkg/messages"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type requester interface {
|
||||||
|
Request(*client) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type client struct {
|
||||||
|
requester
|
||||||
|
requestData *model.RequestInfo
|
||||||
|
integration *model.Integration
|
||||||
|
mux sync.Mutex
|
||||||
|
updateChan chan<- model.Integration
|
||||||
|
evChan chan<- *SessionErrorEvent
|
||||||
|
errChan chan<- error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionErrorEvent struct {
|
||||||
|
SessionID uint64
|
||||||
|
Token string
|
||||||
|
*messages.IntegrationEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientMap map[string]*client
|
||||||
|
|
||||||
|
func NewClient(i *model.Integration, updateChan chan<- model.Integration, evChan chan<- *SessionErrorEvent, errChan chan<- error) (*client, error) {
|
||||||
|
ri, err := i.GetRequestInfo()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c := &client{
|
||||||
|
evChan: evChan,
|
||||||
|
errChan: errChan,
|
||||||
|
updateChan: updateChan,
|
||||||
|
requestData: ri,
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Update(i *model.Integration) error {
|
||||||
|
var r requester
|
||||||
|
switch i.Provider {
|
||||||
|
case "bugsnag":
|
||||||
|
r = new(bugsnag)
|
||||||
|
case "cloudwatch":
|
||||||
|
r = new(cloudwatch)
|
||||||
|
case "datadog":
|
||||||
|
r = new(datadog)
|
||||||
|
case "elasticsearch":
|
||||||
|
r = new(elasticsearch)
|
||||||
|
case "newrelic":
|
||||||
|
r = new(newrelic)
|
||||||
|
case "rollbar":
|
||||||
|
r = new(rollbar)
|
||||||
|
case "sentry":
|
||||||
|
r = new(sentry)
|
||||||
|
case "stackdriver":
|
||||||
|
r = new(stackdriver)
|
||||||
|
case "sumologic":
|
||||||
|
r = new(sumologic)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(i.Options, r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
|
||||||
|
c.integration = i
|
||||||
|
c.requester = r
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) handleError(err error) {
|
||||||
|
c.errChan <- fmt.Errorf("%v | Integration: %v", err, *c.integration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) Request() {
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
|
||||||
|
if !c.requestData.CanAttempt() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.requestData.UpdateLastAttempt()
|
||||||
|
|
||||||
|
err := c.requester.Request(c)
|
||||||
|
if err != nil {
|
||||||
|
c.requestData.Inc()
|
||||||
|
c.handleError(fmt.Errorf("ERRROR L139, err: %s", err))
|
||||||
|
} else {
|
||||||
|
c.requestData.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
rd, err := c.requestData.Encode()
|
||||||
|
if err != nil {
|
||||||
|
c.handleError(err)
|
||||||
|
}
|
||||||
|
// RequestData is a byte array (pointer-like type), but it's replacement
|
||||||
|
// won't affect the previous value sent by channel
|
||||||
|
c.integration.RequestData = rd
|
||||||
|
c.updateChan <- *c.integration
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
|
||||||
|
|
||||||
"openreplay/backend/pkg/messages"
|
"openreplay/backend/pkg/messages"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -21,7 +20,7 @@ type cloudwatch struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cw *cloudwatch) Request(c *client) error {
|
func (cw *cloudwatch) Request(c *client) error {
|
||||||
startTs := int64(c.getLastMessageTimestamp() + 1) // From next millisecond
|
startTs := int64(c.requestData.GetLastMessageTimestamp() + 1) // From next millisecond
|
||||||
//endTs := utils.CurrentTimestamp()
|
//endTs := utils.CurrentTimestamp()
|
||||||
sess, err := session.NewSession(aws.NewConfig().
|
sess, err := session.NewSession(aws.NewConfig().
|
||||||
WithRegion(cw.Region).
|
WithRegion(cw.Region).
|
||||||
|
|
@ -40,7 +39,7 @@ func (cw *cloudwatch) Request(c *client) error {
|
||||||
// SetLimit(10000). // Default 10000
|
// SetLimit(10000). // Default 10000
|
||||||
SetLogGroupName(cw.LogGroupName).
|
SetLogGroupName(cw.LogGroupName).
|
||||||
SetFilterPattern("openReplaySessionToken")
|
SetFilterPattern("openReplaySessionToken")
|
||||||
//SetFilterPattern("asayer_session_id")
|
//SetFilterPattern("asayer_session_id")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
output, err := svc.FilterLogEvents(filterOptions)
|
output, err := svc.FilterLogEvents(filterOptions)
|
||||||
|
|
@ -64,7 +63,7 @@ func (cw *cloudwatch) Request(c *client) error {
|
||||||
name = *e.LogStreamName
|
name = *e.LogStreamName
|
||||||
}
|
}
|
||||||
timestamp := uint64(*e.Timestamp)
|
timestamp := uint64(*e.Timestamp)
|
||||||
c.setLastMessageTimestamp(timestamp)
|
c.requestData.SetLastMessageTimestamp(timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
//SessionID: sessionID,
|
//SessionID: sessionID,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -71,7 +71,7 @@ func (d *datadog) makeRequest(nextLogId *string, fromTs uint64, toTs uint64) (*h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *datadog) Request(c *client) error {
|
func (d *datadog) Request(c *client) error {
|
||||||
fromTs := c.getLastMessageTimestamp() + 1 // From next millisecond
|
fromTs := c.requestData.GetLastMessageTimestamp() + 1 // From next millisecond
|
||||||
toTs := uint64(time.Now().UnixMilli())
|
toTs := uint64(time.Now().UnixMilli())
|
||||||
var nextLogId *string
|
var nextLogId *string
|
||||||
for {
|
for {
|
||||||
|
|
@ -111,7 +111,7 @@ func (d *datadog) Request(c *client) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
timestamp := uint64(parsedTime.UnixMilli())
|
timestamp := uint64(parsedTime.UnixMilli())
|
||||||
c.setLastMessageTimestamp(timestamp)
|
c.requestData.SetLastMessageTimestamp(timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
//SessionID: sessionID,
|
//SessionID: sessionID,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
@ -34,8 +34,6 @@ func (es *elasticsearch) Request(c *client) error {
|
||||||
Addresses: []string{
|
Addresses: []string{
|
||||||
address,
|
address,
|
||||||
},
|
},
|
||||||
//Username: es.ApiKeyId,
|
|
||||||
//Password: es.ApiKey,
|
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
}
|
}
|
||||||
esC, err := elasticlib.NewClient(cfg)
|
esC, err := elasticlib.NewClient(cfg)
|
||||||
|
|
@ -46,7 +44,7 @@ func (es *elasticsearch) Request(c *client) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gteTs := c.getLastMessageTimestamp() + 1000 // Sec or millisec to add ?
|
gteTs := c.requestData.GetLastMessageTimestamp() + 1000
|
||||||
log.Printf("gteTs: %v ", gteTs)
|
log.Printf("gteTs: %v ", gteTs)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
query := map[string]interface{}{
|
query := map[string]interface{}{
|
||||||
|
|
@ -164,7 +162,7 @@ func (es *elasticsearch) Request(c *client) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
timestamp := uint64(esLog.Time.UnixMilli())
|
timestamp := uint64(esLog.Time.UnixMilli())
|
||||||
c.setLastMessageTimestamp(timestamp)
|
c.requestData.SetLastMessageTimestamp(timestamp)
|
||||||
|
|
||||||
var sessionID uint64
|
var sessionID uint64
|
||||||
sessionID, err = strconv.ParseUint(token, 10, 64)
|
sessionID, err = strconv.ParseUint(token, 10, 64)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -27,18 +27,16 @@ type newrelicResponce struct {
|
||||||
Results []struct {
|
Results []struct {
|
||||||
Events []json.RawMessage
|
Events []json.RawMessage
|
||||||
}
|
}
|
||||||
// Metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type newrelicEvent struct {
|
type newrelicEvent struct {
|
||||||
//AsayerSessionID uint64 `json:"asayer_session_id,string"` // string/int decoder?
|
|
||||||
OpenReplaySessionToken string `json:"openReplaySessionToken"`
|
OpenReplaySessionToken string `json:"openReplaySessionToken"`
|
||||||
ErrorClass string `json:"error.class"`
|
ErrorClass string `json:"error.class"`
|
||||||
Timestamp uint64 `json:"timestamp"`
|
Timestamp uint64 `json:"timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nr *newrelic) Request(c *client) error {
|
func (nr *newrelic) Request(c *client) error {
|
||||||
sinceTs := c.getLastMessageTimestamp() + 1000 // From next second
|
sinceTs := c.requestData.GetLastMessageTimestamp() + 1000 // From next second
|
||||||
// In docs - format "yyyy-mm-dd HH:MM:ss", but time.RFC3339 works fine too
|
// In docs - format "yyyy-mm-dd HH:MM:ss", but time.RFC3339 works fine too
|
||||||
sinceFormatted := time.UnixMilli(int64(sinceTs)).Format(time.RFC3339)
|
sinceFormatted := time.UnixMilli(int64(sinceTs)).Format(time.RFC3339)
|
||||||
// US/EU endpoint ??
|
// US/EU endpoint ??
|
||||||
|
|
@ -86,7 +84,7 @@ func (nr *newrelic) Request(c *client) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
c.setLastMessageTimestamp(e.Timestamp)
|
c.requestData.SetLastMessageTimestamp(e.Timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
Token: e.OpenReplaySessionToken,
|
Token: e.OpenReplaySessionToken,
|
||||||
IntegrationEvent: &messages.IntegrationEvent{
|
IntegrationEvent: &messages.IntegrationEvent{
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -60,12 +60,12 @@ type rollbarJobStatusResponce struct {
|
||||||
type rollbarEvent map[string]string
|
type rollbarEvent map[string]string
|
||||||
|
|
||||||
/*
|
/*
|
||||||
It is possible to use /api/1/instances (20 per page)
|
It is possible to use /api/1/instances (20 per page)
|
||||||
Jobs for the identical requests are hashed
|
Jobs for the identical requests are hashed
|
||||||
*/
|
*/
|
||||||
func (rb *rollbar) Request(c *client) error {
|
func (rb *rollbar) Request(c *client) error {
|
||||||
fromTs := c.getLastMessageTimestamp() + 1000 // From next second
|
fromTs := c.requestData.GetLastMessageTimestamp() + 1000 // From next second
|
||||||
c.setLastMessageTimestamp(fromTs) // anti-job-hashing
|
c.requestData.SetLastMessageTimestamp(fromTs) // anti-job-hashing
|
||||||
fromTsSec := fromTs / 1e3
|
fromTsSec := fromTs / 1e3
|
||||||
query := fmt.Sprintf(RB_QUERY, fromTsSec)
|
query := fmt.Sprintf(RB_QUERY, fromTsSec)
|
||||||
jsonBody := fmt.Sprintf(`{
|
jsonBody := fmt.Sprintf(`{
|
||||||
|
|
@ -153,7 +153,7 @@ func (rb *rollbar) Request(c *client) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
timestamp := timestampSec * 1000
|
timestamp := timestampSec * 1000
|
||||||
c.setLastMessageTimestamp(timestamp)
|
c.requestData.SetLastMessageTimestamp(timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
Token: e["body.message.openReplaySessionToken"],
|
Token: e["body.message.openReplaySessionToken"],
|
||||||
IntegrationEvent: &messages.IntegrationEvent{
|
IntegrationEvent: &messages.IntegrationEvent{
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -46,7 +46,7 @@ func (sn *sentry) Request(c *client) error {
|
||||||
req.Header.Add("Authorization", authHeader)
|
req.Header.Add("Authorization", authHeader)
|
||||||
|
|
||||||
// by link ?
|
// by link ?
|
||||||
lastEventId := c.getLastMessageId()
|
lastEventId := c.requestData.GetLastMessageId()
|
||||||
firstEvent := true
|
firstEvent := true
|
||||||
|
|
||||||
PageLoop:
|
PageLoop:
|
||||||
|
|
@ -88,7 +88,7 @@ PageLoop:
|
||||||
timestamp := uint64(parsedTime.UnixMilli())
|
timestamp := uint64(parsedTime.UnixMilli())
|
||||||
// TODO: not to receive all the messages (use default integration timestamp)
|
// TODO: not to receive all the messages (use default integration timestamp)
|
||||||
if firstEvent { // TODO: reverse range?
|
if firstEvent { // TODO: reverse range?
|
||||||
c.setLastMessageId(timestamp, e.EventID)
|
c.requestData.SetLastMessageId(timestamp, e.EventID)
|
||||||
firstEvent = false
|
firstEvent = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cloud.google.com/go/logging/logadmin"
|
"cloud.google.com/go/logging/logadmin"
|
||||||
"google.golang.org/api/iterator"
|
"google.golang.org/api/iterator"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
|
|
||||||
//"strconv"
|
//"strconv"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -33,7 +32,7 @@ type saCreds struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sd *stackdriver) Request(c *client) error {
|
func (sd *stackdriver) Request(c *client) error {
|
||||||
fromTs := c.getLastMessageTimestamp() + 1 // Timestamp is RFC3339Nano, so we take the next millisecond
|
fromTs := c.requestData.GetLastMessageTimestamp() + 1 // Timestamp is RFC3339Nano, so we take the next millisecond
|
||||||
fromFormatted := time.UnixMilli(int64(fromTs)).Format(time.RFC3339Nano)
|
fromFormatted := time.UnixMilli(int64(fromTs)).Format(time.RFC3339Nano)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -85,7 +84,7 @@ func (sd *stackdriver) Request(c *client) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
timestamp := uint64(e.Timestamp.UnixMilli())
|
timestamp := uint64(e.Timestamp.UnixMilli())
|
||||||
c.setLastMessageTimestamp(timestamp)
|
c.requestData.SetLastMessageTimestamp(timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
//SessionID: sessionID,
|
//SessionID: sessionID,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -13,10 +13,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The maximum value for limit is 10,000 messages or 100 MB in total message size,
|
The maximum value for limit is 10,000 messages or 100 MB in total message size,
|
||||||
which means the query may return less than 10,000 messages if you exceed the size limit.
|
which means the query may return less than 10,000 messages if you exceed the size limit.
|
||||||
|
|
||||||
API Documentation: https://help.sumologic.com/APIs/Search-Job-API/About-the-Search-Job-API
|
API Documentation: https://help.sumologic.com/APIs/Search-Job-API/About-the-Search-Job-API
|
||||||
*/
|
*/
|
||||||
const SL_LIMIT = 10000
|
const SL_LIMIT = 10000
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ func (sl *sumologic) deleteJob(jobId string, errChan chan<- error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sl *sumologic) Request(c *client) error {
|
func (sl *sumologic) Request(c *client) error {
|
||||||
fromTs := c.getLastMessageTimestamp() + 1 // From next millisecond
|
fromTs := c.requestData.GetLastMessageTimestamp() + 1 // From next millisecond
|
||||||
toTs := time.Now().UnixMilli()
|
toTs := time.Now().UnixMilli()
|
||||||
requestURL := fmt.Sprintf("https://api.%vsumologic.com/api/v1/search/jobs", "eu.") // deployment server??
|
requestURL := fmt.Sprintf("https://api.%vsumologic.com/api/v1/search/jobs", "eu.") // deployment server??
|
||||||
jsonBody := fmt.Sprintf(`{
|
jsonBody := fmt.Sprintf(`{
|
||||||
|
|
@ -189,7 +189,7 @@ func (sl *sumologic) Request(c *client) error {
|
||||||
if len(name) > 20 {
|
if len(name) > 20 {
|
||||||
name = name[:20] // not sure about that
|
name = name[:20] // not sure about that
|
||||||
}
|
}
|
||||||
c.setLastMessageTimestamp(e.Timestamp)
|
c.requestData.SetLastMessageTimestamp(e.Timestamp)
|
||||||
c.evChan <- &SessionErrorEvent{
|
c.evChan <- &SessionErrorEvent{
|
||||||
//SessionID: sessionID,
|
//SessionID: sessionID,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|
@ -1,22 +1,11 @@
|
||||||
package integration
|
package clients
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var reSessionID = regexp.MustCompile(`(?i)asayer_session_id=([0-9]+)`)
|
|
||||||
|
|
||||||
func GetAsayerSessionId(s string) (uint64, error) {
|
|
||||||
matches := reSessionID.FindStringSubmatch(s)
|
|
||||||
if len(matches) < 2 {
|
|
||||||
return 0, fmt.Errorf("'asayer_session_id' not found in '%v' ", s)
|
|
||||||
}
|
|
||||||
return strconv.ParseUint(matches[1], 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLinkFromAngularBrackets(s string) string {
|
func GetLinkFromAngularBrackets(s string) string {
|
||||||
beg := strings.Index(s, "<") + 1
|
beg := strings.Index(s, "<") + 1
|
||||||
end := strings.Index(s, ">")
|
end := strings.Index(s, ">")
|
||||||
|
|
@ -1,100 +1,94 @@
|
||||||
package integrations
|
package integrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v4"
|
config "openreplay/backend/internal/config/integrations"
|
||||||
|
"openreplay/backend/pkg/intervals"
|
||||||
"openreplay/backend/pkg/db/postgres/pool"
|
"openreplay/backend/pkg/queue/types"
|
||||||
|
"openreplay/backend/pkg/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Listener struct {
|
type Listener struct {
|
||||||
conn *pgx.Conn
|
cfg *config.Config
|
||||||
db pool.Pool
|
storage Storage
|
||||||
Integrations chan *Integration
|
producer types.Producer
|
||||||
Errors chan error
|
manager *Manager
|
||||||
|
tokenizer *token.Tokenizer
|
||||||
|
Errors chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Integration struct {
|
func New(cfg *config.Config, storage Storage, producer types.Producer, manager *Manager, tokenizer *token.Tokenizer) (*Listener, error) {
|
||||||
ProjectID uint32 `json:"project_id"`
|
listener := &Listener{
|
||||||
Provider string `json:"provider"`
|
cfg: cfg,
|
||||||
RequestData json.RawMessage `json:"request_data"`
|
storage: storage,
|
||||||
Options json.RawMessage `json:"options"`
|
Errors: make(chan error),
|
||||||
}
|
producer: producer,
|
||||||
|
manager: manager,
|
||||||
func New(db pool.Pool, url string) (*Listener, error) {
|
tokenizer: tokenizer,
|
||||||
conn, err := pgx.Connect(context.Background(), url)
|
}
|
||||||
|
ints, err := storage.GetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
listener := &Listener{
|
for _, i := range ints {
|
||||||
conn: conn,
|
// Add new integration to manager
|
||||||
db: db,
|
if err = manager.Update(i); err != nil {
|
||||||
Errors: make(chan error),
|
log.Printf("Integration parse error: %v | Integration: %v\n", err, *i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
listener.Integrations = make(chan *Integration, 50)
|
manager.RequestAll()
|
||||||
if _, err := conn.Exec(context.Background(), "LISTEN integration"); err != nil {
|
go listener.worker()
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
go listener.listen()
|
|
||||||
return listener, nil
|
return listener, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (listener *Listener) listen() {
|
func (l *Listener) worker() {
|
||||||
|
clientsCheckTick := time.Tick(intervals.INTEGRATIONS_REQUEST_INTERVAL * time.Millisecond)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
notification, err := listener.conn.WaitForNotification(context.Background())
|
select {
|
||||||
if err != nil {
|
case <-clientsCheckTick:
|
||||||
listener.Errors <- err
|
l.manager.RequestAll()
|
||||||
continue
|
case event := <-l.manager.Events:
|
||||||
}
|
log.Printf("New integration event: %+v\n", *event.IntegrationEvent)
|
||||||
switch notification.Channel {
|
sessionID := event.SessionID
|
||||||
case "integration":
|
if sessionID == 0 {
|
||||||
integrationP := new(Integration)
|
sessData, err := l.tokenizer.Parse(event.Token)
|
||||||
if err := json.Unmarshal([]byte(notification.Payload), integrationP); err != nil {
|
if err != nil && err != token.EXPIRED {
|
||||||
listener.Errors <- fmt.Errorf("%v | Payload: %v", err, notification.Payload)
|
log.Printf("Error on token parsing: %v; Token: %v", err, event.Token)
|
||||||
} else {
|
continue
|
||||||
listener.Integrations <- integrationP
|
}
|
||||||
|
sessionID = sessData.ID
|
||||||
|
}
|
||||||
|
// Why do we produce integration events to analytics topic
|
||||||
|
l.producer.Produce(l.cfg.TopicAnalytics, sessionID, event.IntegrationEvent.Encode())
|
||||||
|
case err := <-l.manager.Errors:
|
||||||
|
log.Printf("Integration error: %v\n", err)
|
||||||
|
case i := <-l.manager.RequestDataUpdates:
|
||||||
|
if err := l.storage.Update(&i); err != nil {
|
||||||
|
log.Printf("Postgres Update request_data error: %v\n", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
newNotification, err := l.storage.CheckNew()
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context deadline exceeded") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Errors <- fmt.Errorf("Integration storage error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("Integration update: %v\n", *newNotification)
|
||||||
|
err = l.manager.Update(newNotification)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Integration parse error: %v | Integration: %v\n", err, *newNotification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (listener *Listener) Close() error {
|
func (l *Listener) Close() error {
|
||||||
return listener.conn.Close(context.Background())
|
return l.storage.UnListen()
|
||||||
}
|
|
||||||
|
|
||||||
func (listener *Listener) IterateIntegrationsOrdered(iter func(integration *Integration, err error)) error {
|
|
||||||
rows, err := listener.db.Query(`
|
|
||||||
SELECT project_id, provider, options, request_data
|
|
||||||
FROM integrations
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
i := new(Integration)
|
|
||||||
if err := rows.Scan(&i.ProjectID, &i.Provider, &i.Options, &i.RequestData); err != nil {
|
|
||||||
iter(nil, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
iter(i, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (listener *Listener) UpdateIntegrationRequestData(i *Integration) error {
|
|
||||||
return listener.db.Exec(`
|
|
||||||
UPDATE integrations
|
|
||||||
SET request_data = $1
|
|
||||||
WHERE project_id=$2 AND provider=$3`,
|
|
||||||
i.RequestData, i.ProjectID, i.Provider,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
backend/pkg/integrations/manager.go
Normal file
48
backend/pkg/integrations/manager.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package integrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"openreplay/backend/pkg/integrations/clients"
|
||||||
|
"openreplay/backend/pkg/integrations/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
clientMap clients.ClientMap
|
||||||
|
Events chan *clients.SessionErrorEvent
|
||||||
|
Errors chan error
|
||||||
|
RequestDataUpdates chan model.Integration // not pointer because it could change in other thread
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
clientMap: make(clients.ClientMap),
|
||||||
|
RequestDataUpdates: make(chan model.Integration, 100),
|
||||||
|
Events: make(chan *clients.SessionErrorEvent, 100),
|
||||||
|
Errors: make(chan error, 100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Update(i *model.Integration) (err error) {
|
||||||
|
log.Printf("Integration initialization: %v\n", *i)
|
||||||
|
key := i.GetKey()
|
||||||
|
if i.Options == nil {
|
||||||
|
delete(m.clientMap, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c, exists := m.clientMap[key]
|
||||||
|
if !exists {
|
||||||
|
c, err = clients.NewClient(i, m.RequestDataUpdates, m.Events, m.Errors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.clientMap[key] = c
|
||||||
|
}
|
||||||
|
return c.Update(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) RequestAll() {
|
||||||
|
log.Printf("Requesting all...\n")
|
||||||
|
for _, c := range m.clientMap {
|
||||||
|
go c.Request()
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/pkg/integrations/model/integration.go
Normal file
38
backend/pkg/integrations/model/integration.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Integration struct {
|
||||||
|
ProjectID uint32 `json:"project_id"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
RequestData json.RawMessage `json:"request_data"`
|
||||||
|
Options json.RawMessage `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Integration) Encode() []byte {
|
||||||
|
b, _ := json.Marshal(i)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Integration) Decode(data []byte) error {
|
||||||
|
return json.Unmarshal(data, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Integration) GetKey() string {
|
||||||
|
return fmt.Sprintf("%d%s", i.ProjectID, i.Provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Integration) GetRequestInfo() (*RequestInfo, error) {
|
||||||
|
ri := new(RequestInfo)
|
||||||
|
if err := json.Unmarshal(i.RequestData, ri); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ri.LastMessageTimestamp == 0 {
|
||||||
|
ri.LastMessageTimestamp = uint64(time.Now().Add(-time.Hour * 24).UnixMilli())
|
||||||
|
}
|
||||||
|
return ri, nil
|
||||||
|
}
|
||||||
58
backend/pkg/integrations/model/request.go
Normal file
58
backend/pkg/integrations/model/request.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_ATTEMPTS_IN_A_ROW = 4
|
||||||
|
const MAX_ATTEMPTS = 40
|
||||||
|
const ATTEMPTS_INTERVAL = 3 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
type RequestInfo struct {
|
||||||
|
LastMessageId string
|
||||||
|
LastMessageTimestamp uint64
|
||||||
|
LastAttemptTimestamp int64
|
||||||
|
UnsuccessfullAttemptsCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestInfo) SetLastMessageTimestamp(timestamp uint64) {
|
||||||
|
if timestamp > c.LastMessageTimestamp {
|
||||||
|
c.LastMessageTimestamp = timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (c *RequestInfo) GetLastMessageTimestamp() uint64 {
|
||||||
|
return c.LastMessageTimestamp
|
||||||
|
}
|
||||||
|
func (c *RequestInfo) SetLastMessageId(timestamp uint64, id string) {
|
||||||
|
c.LastMessageId = id
|
||||||
|
c.LastMessageTimestamp = timestamp
|
||||||
|
}
|
||||||
|
func (c *RequestInfo) GetLastMessageId() string {
|
||||||
|
return c.LastMessageId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestInfo) CanAttempt() bool {
|
||||||
|
if c.UnsuccessfullAttemptsCount >= MAX_ATTEMPTS ||
|
||||||
|
(c.UnsuccessfullAttemptsCount >= MAX_ATTEMPTS_IN_A_ROW &&
|
||||||
|
time.Now().UnixMilli()-c.LastAttemptTimestamp < ATTEMPTS_INTERVAL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestInfo) UpdateLastAttempt() {
|
||||||
|
c.LastAttemptTimestamp = time.Now().UnixMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestInfo) Inc() {
|
||||||
|
c.UnsuccessfullAttemptsCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestInfo) Reset() {
|
||||||
|
c.UnsuccessfullAttemptsCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RequestInfo) Encode() ([]byte, error) {
|
||||||
|
return json.Marshal(c)
|
||||||
|
}
|
||||||
92
backend/pkg/integrations/storage.go
Normal file
92
backend/pkg/integrations/storage.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package integrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"openreplay/backend/pkg/integrations/model"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Storage interface {
|
||||||
|
Listen() error
|
||||||
|
UnListen() error
|
||||||
|
CheckNew() (*model.Integration, error)
|
||||||
|
GetAll() ([]*model.Integration, error)
|
||||||
|
Update(i *model.Integration) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type storageImpl struct {
|
||||||
|
conn *pgx.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorage(conn *pgx.Conn) Storage {
|
||||||
|
return &storageImpl{
|
||||||
|
conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageImpl) Listen() error {
|
||||||
|
_, err := s.conn.Exec(context.Background(), "LISTEN integration")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageImpl) UnListen() error {
|
||||||
|
_, err := s.conn.Exec(context.Background(), "UNLISTEN integration")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageImpl) CheckNew() (*model.Integration, error) {
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
notification, err := s.conn.WaitForNotification(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if notification.Channel == "integration" {
|
||||||
|
integrationP := new(model.Integration)
|
||||||
|
if err := json.Unmarshal([]byte(notification.Payload), integrationP); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return integrationP, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown notification channel: %s", notification.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageImpl) GetAll() ([]*model.Integration, error) {
|
||||||
|
rows, err := s.conn.Query(context.Background(), `
|
||||||
|
SELECT project_id, provider, options, request_data
|
||||||
|
FROM integrations
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
integrations := make([]*model.Integration, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
i := new(model.Integration)
|
||||||
|
if err := rows.Scan(&i.ProjectID, &i.Provider, &i.Options, &i.RequestData); err != nil {
|
||||||
|
log.Printf("Postgres scan error: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
integrations = append(integrations, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return integrations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storageImpl) Update(i *model.Integration) error {
|
||||||
|
_, err := s.conn.Exec(context.Background(), `
|
||||||
|
UPDATE integrations
|
||||||
|
SET request_data = $1
|
||||||
|
WHERE project_id=$2 AND provider=$3`,
|
||||||
|
i.RequestData, i.ProjectID, i.Provider,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue