github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/agent/upstream/remote/remote.go (about) 1 package remote 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "github.com/pyroscope-io/pyroscope/pkg/agent/log" 8 "io" 9 "net/http" 10 "net/url" 11 "path" 12 "runtime/debug" 13 "strconv" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/pyroscope-io/pyroscope/pkg/agent/upstream" 19 ) 20 21 var ( 22 ErrCloudTokenRequired = errors.New("Please provide an authentication token. You can find it here: https://pyroscope.io/cloud") 23 cloudHostnameSuffix = "pyroscope.cloud" 24 ) 25 26 type Remote struct { 27 cfg RemoteConfig 28 jobs chan *upstream.UploadJob 29 client *http.Client 30 Logger log.Logger 31 32 done chan struct{} 33 wg sync.WaitGroup 34 } 35 36 type RemoteConfig struct { 37 AuthToken string 38 BasicAuthUser string 39 BasicAuthPassword string 40 TenantID string 41 HTTPHeaders map[string]string 42 UpstreamThreads int 43 UpstreamAddress string 44 UpstreamRequestTimeout time.Duration 45 } 46 47 func New(cfg RemoteConfig, logger log.Logger) (*Remote, error) { 48 remote := &Remote{ 49 cfg: cfg, 50 jobs: make(chan *upstream.UploadJob, 100), 51 client: &http.Client{ 52 Transport: &http.Transport{ 53 MaxConnsPerHost: cfg.UpstreamThreads, 54 }, 55 Timeout: cfg.UpstreamRequestTimeout, 56 }, 57 Logger: logger, 58 done: make(chan struct{}), 59 } 60 61 // parse the upstream address 62 u, err := url.Parse(cfg.UpstreamAddress) 63 if err != nil { 64 return nil, err 65 } 66 67 // authorize the token first 68 if cfg.AuthToken == "" && requiresAuthToken(u) { 69 return nil, ErrCloudTokenRequired 70 } 71 72 return remote, nil 73 } 74 75 func (r *Remote) Start() { 76 for i := 0; i < r.cfg.UpstreamThreads; i++ { 77 go r.handleJobs() 78 } 79 } 80 81 func (r *Remote) Stop() { 82 if r.done != nil { 83 close(r.done) 84 } 85 86 // wait for uploading goroutines exit 87 r.wg.Wait() 88 } 89 90 func (r *Remote) Upload(job *upstream.UploadJob) { 91 select { 92 case r.jobs <- job: 93 default: 94 r.Logger.Errorf("remote upload queue is full, dropping a profile job") 95 } 96 } 97 98 // UploadSync is only used in benchmarks right now 99 func (r *Remote) UploadSync(job *upstream.UploadJob) error { 100 return r.uploadProfile(job) 101 } 102 103 func (r *Remote) uploadProfile(j *upstream.UploadJob) error { 104 u, err := url.Parse(r.cfg.UpstreamAddress) 105 if err != nil { 106 return fmt.Errorf("url parse: %v", err) 107 } 108 109 q := u.Query() 110 q.Set("name", j.Name) 111 // TODO: I think these should be renamed to startTime / endTime 112 q.Set("from", strconv.Itoa(int(j.StartTime.Unix()))) 113 q.Set("until", strconv.Itoa(int(j.EndTime.Unix()))) 114 q.Set("spyName", j.SpyName) 115 q.Set("sampleRate", strconv.Itoa(int(j.SampleRate))) 116 q.Set("units", string(j.Units)) 117 q.Set("aggregationType", string(j.AggregationType)) 118 119 u.Path = path.Join(u.Path, "ingest") 120 u.RawQuery = q.Encode() 121 122 r.Logger.Debugf("uploading at %s", u.String()) 123 // new a request for the job 124 request, err := http.NewRequest("POST", u.String(), bytes.NewReader(j.Trie.Bytes())) 125 if err != nil { 126 return fmt.Errorf("new http request: %v", err) 127 } 128 request.Header.Set("Content-Type", "binary/octet-stream+trie") 129 130 if r.cfg.AuthToken != "" { 131 request.Header.Set("Authorization", "Bearer "+r.cfg.AuthToken) 132 } else if r.cfg.BasicAuthUser != "" && r.cfg.BasicAuthPassword != "" { 133 request.SetBasicAuth(r.cfg.BasicAuthUser, r.cfg.BasicAuthPassword) 134 } 135 if r.cfg.TenantID != "" { 136 request.Header.Set("X-Scope-OrgID", r.cfg.TenantID) 137 } 138 for k, v := range r.cfg.HTTPHeaders { 139 request.Header.Set(k, v) 140 } 141 142 // do the request and get the response 143 response, err := r.client.Do(request) 144 if err != nil { 145 return fmt.Errorf("do http request: %v", err) 146 } 147 defer response.Body.Close() 148 149 // read all the response body 150 respBody, err := io.ReadAll(response.Body) 151 if err != nil { 152 return fmt.Errorf("read response body: %v", err) 153 } 154 155 if response.StatusCode != 200 { 156 return fmt.Errorf("failed to upload. server responded with statusCode: '%d' and body: '%s'", response.StatusCode, string(respBody)) 157 } 158 159 return nil 160 } 161 162 // handle the jobs 163 func (r *Remote) handleJobs() { 164 for { 165 select { 166 case <-r.done: 167 return 168 case job := <-r.jobs: 169 r.safeUpload(job) 170 } 171 } 172 } 173 174 func requiresAuthToken(u *url.URL) bool { 175 return strings.HasSuffix(u.Host, cloudHostnameSuffix) 176 } 177 178 // do safe upload 179 func (r *Remote) safeUpload(job *upstream.UploadJob) { 180 defer func() { 181 if catch := recover(); catch != nil { 182 r.Logger.Errorf("recover stack: %v", debug.Stack()) 183 } 184 }() 185 186 // update the profile data to server 187 if err := r.uploadProfile(job); err != nil { 188 r.Logger.Errorf("upload profile: %v", err) 189 } 190 }