2018-09-05 18:40:37 +02:00
|
|
|
package database
|
|
|
|
|
|
|
|
import (
|
2018-09-07 12:55:27 +02:00
|
|
|
"database/sql"
|
2018-09-10 12:41:28 +02:00
|
|
|
"encoding/json"
|
2018-09-10 16:29:14 +02:00
|
|
|
"reflect"
|
2018-09-05 18:40:37 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/apex/log"
|
|
|
|
"github.com/ooni/probe-cli/utils"
|
|
|
|
"github.com/pkg/errors"
|
2018-09-07 15:16:20 +02:00
|
|
|
db "upper.io/db.v3"
|
2018-09-05 18:40:37 +02:00
|
|
|
"upper.io/db.v3/lib/sqlbuilder"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ListMeasurements given a result ID
|
2018-09-07 15:16:20 +02:00
|
|
|
func ListMeasurements(sess sqlbuilder.Database, resultID int64) ([]MeasurementURLNetwork, error) {
|
|
|
|
measurements := []MeasurementURLNetwork{}
|
2018-09-05 18:40:37 +02:00
|
|
|
|
2018-09-07 15:16:20 +02:00
|
|
|
req := sess.Select(
|
2018-09-10 18:03:32 +02:00
|
|
|
"measurements.id as msmt_tbl_id",
|
2018-09-11 15:40:42 +02:00
|
|
|
"measurements.is_done as measurement_is_done",
|
|
|
|
"measurements.start_time as measurement_start_time",
|
|
|
|
"measurements.runtime as measurement_runtime",
|
2018-09-07 15:16:20 +02:00
|
|
|
"networks.id as network_id",
|
2018-09-11 18:06:15 +02:00
|
|
|
"networks.country_code as network_country_code",
|
2018-09-07 15:16:20 +02:00
|
|
|
"results.id as result_id",
|
2018-09-11 15:40:42 +02:00
|
|
|
"results.start_time as result_start_time",
|
|
|
|
"results.is_done as result_is_done",
|
|
|
|
"results.runtime as result_runtime",
|
2018-09-11 16:36:09 +02:00
|
|
|
"results.test_group_name as test_group_name",
|
2018-09-07 15:16:20 +02:00
|
|
|
"urls.id as url_id",
|
2018-09-11 18:06:15 +02:00
|
|
|
"urls.country_code as url_country_code",
|
2018-09-07 15:16:20 +02:00
|
|
|
db.Raw("networks.*"),
|
|
|
|
db.Raw("urls.*"),
|
|
|
|
db.Raw("measurements.*"),
|
2018-09-12 15:41:54 +02:00
|
|
|
db.Raw("results.*"),
|
2018-09-07 15:16:20 +02:00
|
|
|
).From("results").
|
|
|
|
Join("measurements").On("results.id = measurements.result_id").
|
|
|
|
Join("networks").On("results.network_id = networks.id").
|
|
|
|
LeftJoin("urls").On("urls.id = measurements.url_id").
|
|
|
|
OrderBy("measurements.start_time").
|
|
|
|
Where("results.id = ?", resultID)
|
2018-09-05 18:40:37 +02:00
|
|
|
|
2018-09-07 15:16:20 +02:00
|
|
|
if err := req.All(&measurements); err != nil {
|
|
|
|
log.Errorf("failed to run query %s: %v", req.String(), err)
|
|
|
|
return measurements, err
|
|
|
|
}
|
2018-09-05 18:40:37 +02:00
|
|
|
return measurements, nil
|
|
|
|
}
|
|
|
|
|
2018-09-10 15:03:52 +02:00
|
|
|
// GetResultTestKeys returns a list of TestKeys for a given measurements
|
|
|
|
func GetResultTestKeys(sess sqlbuilder.Database, resultID int64) (string, error) {
|
|
|
|
res := sess.Collection("measurements").Find("result_id", resultID)
|
|
|
|
defer res.Close()
|
|
|
|
|
2018-09-11 18:41:15 +02:00
|
|
|
var (
|
|
|
|
msmt Measurement
|
|
|
|
tk PerformanceTestKeys
|
|
|
|
)
|
2018-09-10 15:03:52 +02:00
|
|
|
for res.Next(&msmt) {
|
|
|
|
if msmt.TestName == "web_connectivity" {
|
|
|
|
break
|
|
|
|
}
|
2018-09-11 18:41:15 +02:00
|
|
|
// We only really care about performance keys
|
|
|
|
if msmt.TestName == "ndt" || msmt.TestName == "dash" {
|
|
|
|
if err := json.Unmarshal([]byte(msmt.TestKeys), &tk); err != nil {
|
|
|
|
log.WithError(err).Error("failed to parse testKeys")
|
|
|
|
return "{}", err
|
|
|
|
}
|
2018-09-10 15:03:52 +02:00
|
|
|
}
|
|
|
|
}
|
2018-09-11 18:41:15 +02:00
|
|
|
b, err := json.Marshal(tk)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("failed to serialize testKeys")
|
|
|
|
return "{}", err
|
|
|
|
}
|
|
|
|
return string(b), nil
|
2018-09-10 15:03:52 +02:00
|
|
|
}
|
|
|
|
|
2018-09-10 12:41:28 +02:00
|
|
|
// GetMeasurementCounts returns the number of anomalous and total measurement for a given result
|
|
|
|
func GetMeasurementCounts(sess sqlbuilder.Database, resultID int64) (uint64, uint64, error) {
|
|
|
|
var (
|
|
|
|
totalCount uint64
|
|
|
|
anmlyCount uint64
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
col := sess.Collection("measurements")
|
|
|
|
|
|
|
|
// XXX these two queries can be done with a single query
|
|
|
|
totalCount, err = col.Find("result_id", resultID).
|
|
|
|
Count()
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("failed to get total count")
|
|
|
|
return totalCount, anmlyCount, err
|
|
|
|
}
|
|
|
|
|
|
|
|
anmlyCount, err = col.Find("result_id", resultID).
|
|
|
|
And(db.Cond{"is_anomaly": true}).Count()
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("failed to get anmly count")
|
|
|
|
return totalCount, anmlyCount, err
|
|
|
|
}
|
|
|
|
|
2018-09-10 16:56:32 +02:00
|
|
|
log.Debugf("counts: %d, %d, %d", resultID, totalCount, anmlyCount)
|
2018-09-10 12:41:28 +02:00
|
|
|
return totalCount, anmlyCount, err
|
|
|
|
}
|
|
|
|
|
2018-09-05 18:40:37 +02:00
|
|
|
// ListResults return the list of results
|
2018-09-07 15:16:20 +02:00
|
|
|
func ListResults(sess sqlbuilder.Database) ([]ResultNetwork, []ResultNetwork, error) {
|
|
|
|
doneResults := []ResultNetwork{}
|
|
|
|
incompleteResults := []ResultNetwork{}
|
2018-09-05 18:40:37 +02:00
|
|
|
|
2018-09-07 15:16:20 +02:00
|
|
|
req := sess.Select(
|
|
|
|
"networks.id AS network_id",
|
2018-09-10 16:56:32 +02:00
|
|
|
"results.id AS result_id",
|
2018-09-07 15:16:20 +02:00
|
|
|
db.Raw("networks.*"),
|
2018-09-10 16:56:32 +02:00
|
|
|
db.Raw("results.*"),
|
2018-09-07 15:16:20 +02:00
|
|
|
).From("results").
|
|
|
|
Join("networks").On("results.network_id = networks.id").
|
|
|
|
OrderBy("results.start_time")
|
2018-09-05 18:40:37 +02:00
|
|
|
|
2018-09-07 15:16:20 +02:00
|
|
|
if err := req.Where("is_done = true").All(&doneResults); err != nil {
|
|
|
|
return doneResults, incompleteResults, errors.Wrap(err, "failed to get result done list")
|
|
|
|
}
|
|
|
|
if err := req.Where("is_done = false").All(&incompleteResults); err != nil {
|
|
|
|
return doneResults, incompleteResults, errors.Wrap(err, "failed to get result done list")
|
|
|
|
}
|
2018-09-05 18:40:37 +02:00
|
|
|
|
|
|
|
return doneResults, incompleteResults, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateMeasurement writes the measurement to the database a returns a pointer
|
|
|
|
// to the Measurement
|
2018-09-07 12:55:27 +02:00
|
|
|
func CreateMeasurement(sess sqlbuilder.Database, reportID sql.NullString, testName string, resultID int64, reportFilePath string, urlID sql.NullInt64) (*Measurement, error) {
|
|
|
|
msmt := Measurement{
|
|
|
|
ReportID: reportID,
|
|
|
|
TestName: testName,
|
|
|
|
ResultID: resultID,
|
|
|
|
ReportFilePath: reportFilePath,
|
|
|
|
URLID: urlID,
|
2018-09-11 15:19:08 +02:00
|
|
|
IsFailed: false,
|
|
|
|
IsDone: false,
|
2018-09-07 12:55:27 +02:00
|
|
|
// XXX Do we want to have this be part of something else?
|
|
|
|
StartTime: time.Now().UTC(),
|
|
|
|
TestKeys: "",
|
|
|
|
}
|
2018-09-05 18:40:37 +02:00
|
|
|
|
2018-09-07 12:55:27 +02:00
|
|
|
newID, err := sess.Collection("measurements").Insert(msmt)
|
2018-09-05 18:40:37 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "creating measurement")
|
|
|
|
}
|
2018-09-07 12:55:27 +02:00
|
|
|
msmt.ID = newID.(int64)
|
|
|
|
return &msmt, nil
|
2018-09-05 18:40:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// CreateResult writes the Result to the database a returns a pointer
|
|
|
|
// to the Result
|
2018-09-07 12:55:27 +02:00
|
|
|
func CreateResult(sess sqlbuilder.Database, homePath string, testGroupName string, networkID int64) (*Result, error) {
|
|
|
|
startTime := time.Now().UTC()
|
2018-09-05 18:40:37 +02:00
|
|
|
|
2018-09-07 12:55:27 +02:00
|
|
|
p, err := utils.MakeResultsDir(homePath, testGroupName, startTime)
|
2018-09-05 18:40:37 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2018-09-07 12:55:27 +02:00
|
|
|
|
|
|
|
result := Result{
|
|
|
|
TestGroupName: testGroupName,
|
|
|
|
StartTime: startTime,
|
|
|
|
NetworkID: networkID,
|
|
|
|
}
|
|
|
|
result.MeasurementDir = p
|
|
|
|
log.Debugf("Creating result %v", result)
|
|
|
|
|
|
|
|
newID, err := sess.Collection("results").Insert(result)
|
2018-09-05 18:40:37 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "creating result")
|
|
|
|
}
|
2018-09-07 12:55:27 +02:00
|
|
|
result.ID = newID.(int64)
|
|
|
|
return &result, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateNetwork will create a new network in the network table
|
|
|
|
func CreateNetwork(sess sqlbuilder.Database, location *utils.LocationInfo) (*Network, error) {
|
|
|
|
network := Network{
|
|
|
|
ASN: location.ASN,
|
|
|
|
CountryCode: location.CountryCode,
|
|
|
|
NetworkName: location.NetworkName,
|
2018-09-07 14:06:08 +02:00
|
|
|
// On desktop we consider it to always be wifi
|
|
|
|
NetworkType: "wifi",
|
2018-09-07 12:55:27 +02:00
|
|
|
IP: location.IP,
|
|
|
|
}
|
|
|
|
newID, err := sess.Collection("networks").Insert(network)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
network.ID = newID.(int64)
|
|
|
|
return &network, nil
|
2018-09-05 18:40:37 +02:00
|
|
|
}
|
2018-09-07 15:23:29 +02:00
|
|
|
|
|
|
|
// CreateOrUpdateURL will create a new URL entry to the urls table if it doesn't
|
|
|
|
// exists, otherwise it will update the category code of the one already in
|
|
|
|
// there.
|
2018-09-12 18:47:57 +02:00
|
|
|
func CreateOrUpdateURL(sess sqlbuilder.Database, urlStr string, categoryCode string, countryCode string) (int64, error) {
|
|
|
|
var url URL
|
2018-09-07 15:23:29 +02:00
|
|
|
|
2018-09-12 18:47:57 +02:00
|
|
|
tx, err := sess.NewTx(nil)
|
2018-09-07 15:23:29 +02:00
|
|
|
if err != nil {
|
2018-09-12 18:47:57 +02:00
|
|
|
log.WithError(err).Error("failed to create transaction")
|
2018-09-07 15:23:29 +02:00
|
|
|
return 0, err
|
|
|
|
}
|
2018-09-12 18:47:57 +02:00
|
|
|
res := tx.Collection("urls").Find(
|
|
|
|
db.Cond{"url": urlStr, "country_code": countryCode},
|
|
|
|
)
|
|
|
|
err = res.One(&url)
|
2018-09-07 15:23:29 +02:00
|
|
|
|
2018-09-12 18:47:57 +02:00
|
|
|
if err == db.ErrNoMoreRows {
|
|
|
|
url = URL{
|
|
|
|
URL: sql.NullString{String: urlStr, Valid: true},
|
|
|
|
CategoryCode: sql.NullString{String: categoryCode, Valid: true},
|
|
|
|
CountryCode: sql.NullString{String: countryCode, Valid: true},
|
|
|
|
}
|
|
|
|
newID, insErr := tx.Collection("urls").Insert(url)
|
|
|
|
if insErr != nil {
|
2018-09-07 15:23:29 +02:00
|
|
|
log.Error("Failed to insert into the URLs table")
|
2018-09-12 18:47:57 +02:00
|
|
|
return 0, insErr
|
2018-09-07 15:23:29 +02:00
|
|
|
}
|
2018-09-12 18:47:57 +02:00
|
|
|
url.ID = sql.NullInt64{Int64: newID.(int64), Valid: true}
|
|
|
|
} else if err != nil {
|
|
|
|
log.WithError(err).Error("Failed to get single result")
|
|
|
|
return 0, err
|
2018-09-07 15:23:29 +02:00
|
|
|
} else {
|
2018-09-12 18:47:57 +02:00
|
|
|
url.CategoryCode = sql.NullString{String: categoryCode, Valid: true}
|
|
|
|
res.Update(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = tx.Commit()
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("Failed to write to the URL table")
|
|
|
|
return 0, err
|
2018-09-07 15:23:29 +02:00
|
|
|
}
|
|
|
|
|
2018-09-12 18:47:57 +02:00
|
|
|
log.Debugf("returning url %d", url.ID.Int64)
|
|
|
|
|
|
|
|
return url.ID.Int64, nil
|
2018-09-07 15:23:29 +02:00
|
|
|
}
|
2018-09-10 12:41:28 +02:00
|
|
|
|
|
|
|
// AddTestKeys writes the summary to the measurement
|
|
|
|
func AddTestKeys(sess sqlbuilder.Database, msmt *Measurement, tk interface{}) error {
|
2018-09-10 16:29:14 +02:00
|
|
|
var (
|
|
|
|
isAnomaly bool
|
|
|
|
isAnomalyValid bool
|
|
|
|
)
|
2018-09-10 12:41:28 +02:00
|
|
|
tkBytes, err := json.Marshal(tk)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Error("failed to serialize summary")
|
|
|
|
}
|
2018-09-10 16:29:14 +02:00
|
|
|
|
|
|
|
// This is necessary so that we can extract from the the opaque testKeys just
|
|
|
|
// the IsAnomaly field of bool type.
|
|
|
|
// Maybe generics are not so bad after-all, heh golang?
|
|
|
|
isAnomalyValue := reflect.ValueOf(tk).FieldByName("IsAnomaly")
|
|
|
|
if isAnomalyValue.IsValid() == true && isAnomalyValue.Kind() == reflect.Bool {
|
|
|
|
isAnomaly = isAnomalyValue.Bool()
|
|
|
|
isAnomalyValid = true
|
|
|
|
}
|
2018-09-10 12:41:28 +02:00
|
|
|
msmt.TestKeys = string(tkBytes)
|
2018-09-10 16:29:14 +02:00
|
|
|
msmt.IsAnomaly = sql.NullBool{Bool: isAnomaly, Valid: isAnomalyValid}
|
2018-09-10 12:41:28 +02:00
|
|
|
|
|
|
|
err = sess.Collection("measurements").Find("id", msmt.ID).Update(msmt)
|
|
|
|
if err != nil {
|
2018-09-10 16:29:14 +02:00
|
|
|
log.WithError(err).Error("failed to update measurement")
|
2018-09-10 12:41:28 +02:00
|
|
|
return errors.Wrap(err, "updating measurement")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|