-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reporter package #116
Reporter package #116
Changes from 8 commits
8f9b09c
7d58f75
91be76f
b8f24eb
1b3eadd
deedd11
6708954
bce8a31
f674228
b9beb9f
9716758
ad23b7a
ebb9a59
af5201f
e934f4e
571c675
6725d28
7a5d1c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,211 @@ | ||||||||
// Copyright 2023 Jigsaw Operations LLC | ||||||||
// | ||||||||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||
// you may not use this file except in compliance with the License. | ||||||||
// You may obtain a copy of the License at | ||||||||
// | ||||||||
// https://www.apache.org/licenses/LICENSE-2.0 | ||||||||
// | ||||||||
// Unless required by applicable law or agreed to in writing, software | ||||||||
// distributed under the License is distributed on an "AS IS" BASIS, | ||||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||
// See the License for the specific language governing permissions and | ||||||||
// limitations under the License. | ||||||||
|
||||||||
package reporter | ||||||||
|
||||||||
import ( | ||||||||
"bytes" | ||||||||
"encoding/json" | ||||||||
"fmt" | ||||||||
"io" | ||||||||
"log" | ||||||||
"math/rand" | ||||||||
"net/http" | ||||||||
"net/url" | ||||||||
"sort" | ||||||||
"time" | ||||||||
) | ||||||||
|
||||||||
var debugLog log.Logger = *log.New(io.Discard, "", 0) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should remove all logging from this library.
Suggested change
|
||||||||
var httpClient = &http.Client{} | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass the client to the RemoteReporter
Suggested change
|
||||||||
|
||||||||
type ConnectivityReport struct { | ||||||||
// Connection setup | ||||||||
Connection interface{} `json:"connection"` | ||||||||
// Observations | ||||||||
Time time.Time `json:"time"` | ||||||||
DurationMs int64 `json:"durationMs"` | ||||||||
Error interface{} `json:"error"` | ||||||||
} | ||||||||
|
||||||||
type Report interface { | ||||||||
IsSuccess() bool | ||||||||
CanMarshal() bool | ||||||||
} | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
// ConnectivityReport implements the Report interface | ||||||||
func (r ConnectivityReport) IsSuccess() bool { | ||||||||
if r.Error == nil { | ||||||||
return true | ||||||||
} else { | ||||||||
return false | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
// Makes sure the Report type can be marshalled into JSON | ||||||||
func (r ConnectivityReport) CanMarshal() bool { | ||||||||
_, err := json.Marshal(r) | ||||||||
if err != nil { | ||||||||
log.Printf("Error encoding JSON: %s\n", err) | ||||||||
return false | ||||||||
} else { | ||||||||
return true | ||||||||
} | ||||||||
} | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: |
||||||||
type Collector interface { | ||||||||
Collect(Report) error | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
} | ||||||||
|
||||||||
type RemoteCollector struct { | ||||||||
collectorEndpoint *url.URL | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
} | ||||||||
|
||||||||
type SamplingCollector struct { | ||||||||
collector Collector | ||||||||
successFraction float64 | ||||||||
failureFraction float64 | ||||||||
} | ||||||||
|
||||||||
type CollectorTarget struct { | ||||||||
collector Collector | ||||||||
priority int | ||||||||
maxRetry int | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
} | ||||||||
|
||||||||
// TODO: implement a rotating collector | ||||||||
type RotatingCollector struct { | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
collectors []CollectorTarget | ||||||||
stopOnSuccess bool | ||||||||
} | ||||||||
|
||||||||
// SortByPriority sorts the collectors based on their priority in ascending order | ||||||||
func (rc *RotatingCollector) SortByPriority() { | ||||||||
sort.Slice(rc.collectors, func(i, j int) bool { | ||||||||
return rc.collectors[i].priority < rc.collectors[j].priority | ||||||||
}) | ||||||||
} | ||||||||
|
||||||||
func (c *RotatingCollector) Collect(report Report) error { | ||||||||
// sort collectors in RotatingCollector by priority | ||||||||
// into a new slice | ||||||||
c.SortByPriority() | ||||||||
for _, target := range c.collectors { | ||||||||
for i := 0; i < target.maxRetry; i++ { | ||||||||
err := target.collector.Collect(report) | ||||||||
if err != nil { | ||||||||
if err, ok := err.(StatusErr); ok { | ||||||||
switch { | ||||||||
case err.StatusCode == 500: | ||||||||
// skip retrying | ||||||||
break | ||||||||
case err.StatusCode == 408: | ||||||||
// wait for 1 second before retry | ||||||||
time.Sleep(time.Duration(1000 * time.Millisecond)) | ||||||||
default: | ||||||||
break | ||||||||
} | ||||||||
} else { | ||||||||
return err | ||||||||
} | ||||||||
} else { | ||||||||
fmt.Println("Report sent") | ||||||||
if c.stopOnSuccess { | ||||||||
return nil | ||||||||
} | ||||||||
break | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
return nil | ||||||||
} | ||||||||
|
||||||||
type StatusErr struct { | ||||||||
StatusCode int | ||||||||
Message string | ||||||||
} | ||||||||
|
||||||||
func (e StatusErr) Error() string { | ||||||||
return e.Message | ||||||||
} | ||||||||
|
||||||||
func (c *RemoteCollector) Collect(report Report) error { | ||||||||
jsonData, err := json.Marshal(report) | ||||||||
var statusCode int | ||||||||
if err != nil { | ||||||||
log.Printf("Error encoding JSON: %s\n", err) | ||||||||
return err | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
} | ||||||||
statusCode, err = sendReport(jsonData, c.collectorEndpoint) | ||||||||
if err != nil { | ||||||||
log.Printf("Send report failed: %v", err) | ||||||||
return err | ||||||||
} else if statusCode > 400 { | ||||||||
return StatusErr{ | ||||||||
StatusCode: statusCode, | ||||||||
Message: fmt.Sprintf("HTTP request failed with status code %d", statusCode), | ||||||||
} | ||||||||
} else { | ||||||||
fmt.Println("Report sent") | ||||||||
return nil | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
func (c *SamplingCollector) Collect(report Report) error { | ||||||||
var samplingRate float64 | ||||||||
if report.IsSuccess() { | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
samplingRate = c.successFraction | ||||||||
} else { | ||||||||
samplingRate = c.failureFraction | ||||||||
} | ||||||||
// Generate a random float64 number between 0 and 1 | ||||||||
random := rand.Float64() | ||||||||
if random < samplingRate { | ||||||||
err := c.collector.Collect(report) | ||||||||
if err != nil { | ||||||||
log.Printf("Error collecting report: %v", err) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
return err | ||||||||
} | ||||||||
return nil | ||||||||
} else { | ||||||||
fmt.Println("Report was not sent this time") | ||||||||
return nil | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
func sendReport(jsonData []byte, remote *url.URL) (int, error) { | ||||||||
// TODO: return status code of HTTP response | ||||||||
req, err := http.NewRequest("POST", remote.String(), bytes.NewReader(jsonData)) | ||||||||
if err != nil { | ||||||||
debugLog.Printf("Error creating the HTTP request: %s\n", err) | ||||||||
return 0, err | ||||||||
} | ||||||||
|
||||||||
req.Header.Set("Content-Type", "application/json; charset=utf-8") | ||||||||
resp, err := httpClient.Do(req) | ||||||||
amircybersec marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
if err != nil { | ||||||||
log.Printf("Error sending the HTTP request: %s\n", err) | ||||||||
return 0, err | ||||||||
} | ||||||||
defer resp.Body.Close() | ||||||||
// Access the HTTP response status code | ||||||||
fmt.Printf("HTTP Response Status Code: %d\n", resp.StatusCode) | ||||||||
respBody, err := io.ReadAll(resp.Body) | ||||||||
if err != nil { | ||||||||
debugLog.Printf("Error reading the HTTP response body: %s\n", err) | ||||||||
return 0, err | ||||||||
} | ||||||||
debugLog.Printf("Response: %s\n", respBody) | ||||||||
return resp.StatusCode, nil | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
// Copyright 2023 Jigsaw Operations LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// https://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package reporter | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
"testing" | ||
"time" | ||
) | ||
|
||
type ConnectivitySetup struct { | ||
Proxy string `json:"proxy"` | ||
Resolver string `json:"resolver"` | ||
Proto string `json:"proto"` | ||
Prefix string `json:"prefix"` | ||
} | ||
|
||
type ConnectivityError struct { | ||
Op string `json:"operation"` | ||
PosixError string `json:"posixError"` | ||
Msg string `json:"msg"` | ||
} | ||
|
||
func TestIsSuccess(t *testing.T) { | ||
var testReport = ConnectivityReport{ | ||
Connection: nil, | ||
Time: time.Now().UTC().Truncate(time.Second), | ||
DurationMs: 1, | ||
} | ||
|
||
var r Report = testReport | ||
if !r.IsSuccess() { | ||
t.Errorf("Expected false, but got: %v", r.IsSuccess()) | ||
} else { | ||
fmt.Println("IsSuccess Test Passed") | ||
} | ||
} | ||
|
||
func TestSendReportSuccessfully(t *testing.T) { | ||
var testSetup = ConnectivitySetup{ | ||
Proxy: "testProxy", | ||
Resolver: "8.8.8.8", | ||
Proto: "udp", | ||
Prefix: "HTTP1/1", | ||
} | ||
var testErr = ConnectivityError{ | ||
Op: "read", | ||
PosixError: "ETIMEDOUT", | ||
Msg: "i/o timeout", | ||
} | ||
var testReport = ConnectivityReport{ | ||
Connection: testSetup, | ||
Time: time.Now().UTC().Truncate(time.Second), | ||
DurationMs: 1, | ||
Error: testErr, | ||
} | ||
|
||
var r Report = testReport | ||
fmt.Printf("The test report shows success: %v\n", r.IsSuccess()) | ||
u, err := url.Parse("https://script.google.com/macros/s/AKfycbzoMBmftQaR9Aw4jzTB-w4TwkDjLHtSfBCFhh4_2NhTEZAUdj85Qt8uYCKCNOEAwCg4/exec") | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
c := RemoteCollector{ | ||
collectorEndpoint: u, | ||
} | ||
err = c.Collect(r) | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
} | ||
|
||
func TestSendReportUnsuccessfully(t *testing.T) { | ||
var testReport = ConnectivityReport{ | ||
Connection: nil, | ||
Time: time.Now().UTC().Truncate(time.Second), | ||
DurationMs: 1, | ||
} | ||
var r Report = testReport | ||
fmt.Printf("The test report shows success: %v\n", r.IsSuccess()) | ||
u, err := url.Parse("https://google.com") | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
c := RemoteCollector{ | ||
collectorEndpoint: u, | ||
} | ||
err = c.Collect(r) | ||
if err == nil { | ||
t.Errorf("Expected 405 error no error occurred!") | ||
} else { | ||
if err, ok := err.(StatusErr); ok { | ||
if err.StatusCode != 405 { | ||
t.Errorf("Expected 405 error no error occurred!") | ||
} | ||
} | ||
} | ||
} | ||
|
||
func TestSamplingCollector(t *testing.T) { | ||
var testReport = ConnectivityReport{ | ||
Connection: nil, | ||
Time: time.Now().UTC().Truncate(time.Second), | ||
DurationMs: 1, | ||
} | ||
var r Report = testReport | ||
fmt.Printf("The test report shows success: %v\n", r.IsSuccess()) | ||
u, err := url.Parse("https://example.com") | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
c := SamplingCollector{ | ||
collector: &RemoteCollector{ | ||
collectorEndpoint: u, | ||
}, | ||
successFraction: 0.5, | ||
failureFraction: 0.1, | ||
} | ||
err = c.Collect(r) | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
} | ||
|
||
func TestRotatingCollector(t *testing.T) { | ||
var testReport = ConnectivityReport{ | ||
Connection: nil, | ||
Time: time.Now().UTC().Truncate(time.Second), | ||
DurationMs: 1, | ||
} | ||
var r Report = testReport | ||
fmt.Printf("The test report shows success: %v\n", r.IsSuccess()) | ||
u1, err := url.Parse("https://example.com") | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
u2, err := url.Parse("https://google.com") | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
u3, err := url.Parse("https://script.google.com/macros/s/AKfycbzoMBmftQaR9Aw4jzTB-w4TwkDjLHtSfBCFhh4_2NhTEZAUdj85Qt8uYCKCNOEAwCg4/exec") | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
c := RotatingCollector{ | ||
collectors: []CollectorTarget{ | ||
{collector: &RemoteCollector{ | ||
collectorEndpoint: u1, | ||
}, | ||
priority: 1, | ||
maxRetry: 2, | ||
}, | ||
{ | ||
collector: &RemoteCollector{ | ||
collectorEndpoint: u2, | ||
}, | ||
priority: 2, | ||
maxRetry: 3, | ||
}, | ||
{ | ||
collector: &RemoteCollector{ | ||
collectorEndpoint: u3, | ||
}, | ||
priority: 3, | ||
maxRetry: 2, | ||
}, | ||
}, | ||
stopOnSuccess: false, | ||
} | ||
err = c.Collect(r) | ||
if err != nil { | ||
t.Errorf("Expected no error, but got: %v", err) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.