fix(stats): calculate mileage without previous-day baseline
This commit is contained in:
@@ -74,8 +74,11 @@ type dailySourceLast struct {
|
||||
Phone string
|
||||
DeviceID string
|
||||
SourceEndpoint string
|
||||
FirstTS time.Time
|
||||
TS time.Time
|
||||
FirstTotalKM float64
|
||||
TotalKM float64
|
||||
RawSampleCount int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -233,21 +236,17 @@ func addSamples(aggregates map[string]*metricAgg, samples []stats.MetricSample)
|
||||
SourceEndpoint: sample.SourceEndpoint,
|
||||
FirstEventTime: sample.EventTime,
|
||||
LatestEventTime: sample.EventTime,
|
||||
QualityStatus: stats.QualityNoPreviousBaseline,
|
||||
QualityReason: "scan_backfill_missing_previous_source",
|
||||
QualityStatus: stats.QualityOK,
|
||||
QualityReason: "current_day_first_sample",
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sample.TotalMileageKM < agg.FirstKM {
|
||||
agg.FirstKM = sample.TotalMileageKM
|
||||
}
|
||||
if sample.TotalMileageKM > agg.LatestKM {
|
||||
agg.LatestKM = sample.TotalMileageKM
|
||||
}
|
||||
if agg.FirstEventTime.IsZero() || sample.EventTime.Before(agg.FirstEventTime) {
|
||||
agg.FirstKM = sample.TotalMileageKM
|
||||
agg.FirstEventTime = sample.EventTime
|
||||
}
|
||||
if sample.EventTime.After(agg.LatestEventTime) {
|
||||
agg.LatestKM = sample.TotalMileageKM
|
||||
agg.LatestEventTime = sample.EventTime
|
||||
agg.SourceEndpoint = sample.SourceEndpoint
|
||||
if strings.TrimSpace(sample.Phone) != "" {
|
||||
@@ -339,22 +338,6 @@ func clearBackfillTargetMileage(ctx context.Context, db *sql.DB, vin string, sta
|
||||
}
|
||||
|
||||
func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[string]*metricAgg, error) {
|
||||
dates, err := dateRangeWithPrevious(cfg.DateFrom, cfg.DateTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastByProtocolDate := map[envelope.Protocol]map[string]map[string][]dailySourceLast{}
|
||||
for _, protocol := range cfg.Protocols {
|
||||
lastByProtocolDate[protocol] = map[string]map[string][]dailySourceLast{}
|
||||
for _, date := range dates {
|
||||
rows, err := queryDailyLastSourceRows(ctx, db, cfg, protocol, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastByProtocolDate[protocol][date] = rows
|
||||
slog.Info("daily last loaded", "protocol", protocol, "date", date, "vehicles", len(rows))
|
||||
}
|
||||
}
|
||||
targetDates, err := dateRange(cfg.DateFrom, cfg.DateTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -362,9 +345,15 @@ func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[s
|
||||
aggregates := map[string]*metricAgg{}
|
||||
for _, protocol := range cfg.Protocols {
|
||||
for _, date := range targetDates {
|
||||
prevDate := previousDate(date)
|
||||
current := lastByProtocolDate[protocol][date]
|
||||
previous := lastByProtocolDate[protocol][prevDate]
|
||||
current, err := queryDailyLastSourceRows(ctx, db, cfg, protocol, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
previous, err := queryPreviousLastSourceRows(ctx, db, cfg, protocol, date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slog.Info("daily last loaded", "protocol", protocol, "date", date, "vehicles", len(current), "previousVehicles", len(previous))
|
||||
for vin, currentSources := range current {
|
||||
previousBySource := map[string]dailySourceLast{}
|
||||
for _, previousRow := range previous[vin] {
|
||||
@@ -372,29 +361,8 @@ func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[s
|
||||
}
|
||||
for _, currentRow := range currentSources {
|
||||
key := vin + "|" + date + "|" + string(protocol) + "|" + currentRow.SourceKey
|
||||
agg := &metricAgg{
|
||||
VIN: vin,
|
||||
Date: date,
|
||||
Protocol: protocol,
|
||||
FirstKM: currentRow.TotalKM,
|
||||
LatestKM: currentRow.TotalKM,
|
||||
Count: 1,
|
||||
SourceKey: currentRow.SourceKey,
|
||||
Phone: currentRow.Phone,
|
||||
DeviceID: currentRow.DeviceID,
|
||||
SourceEndpoint: currentRow.SourceEndpoint,
|
||||
FirstEventTime: currentRow.TS,
|
||||
LatestEventTime: currentRow.TS,
|
||||
QualityStatus: stats.QualityNoPreviousBaseline,
|
||||
QualityReason: "missing_previous_source",
|
||||
}
|
||||
if previousRow, ok := previousBySource[currentRow.SourceKey]; ok {
|
||||
agg.FirstKM = previousRow.TotalKM
|
||||
agg.FirstEventTime = previousRow.TS
|
||||
agg.QualityStatus = stats.QualityOK
|
||||
agg.QualityReason = "same_source_previous_day"
|
||||
}
|
||||
aggregates[key] = agg
|
||||
previousRow, hasPrevious := previousBySource[currentRow.SourceKey]
|
||||
aggregates[key] = aggregateFromDailySource(date, protocol, currentRow, previousRow, hasPrevious)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,6 +370,40 @@ func buildLastDiffAggregates(ctx context.Context, db *sql.DB, cfg config) (map[s
|
||||
return aggregates, nil
|
||||
}
|
||||
|
||||
func aggregateFromDailySource(date string, protocol envelope.Protocol, current dailySourceLast, previous dailySourceLast, hasPrevious bool) *metricAgg {
|
||||
firstKM := current.FirstTotalKM
|
||||
firstEventTime := current.FirstTS
|
||||
qualityReason := "current_day_first_sample"
|
||||
if hasPrevious {
|
||||
firstKM = previous.TotalKM
|
||||
firstEventTime = previous.TS
|
||||
qualityReason = "historical_source_baseline"
|
||||
}
|
||||
if firstEventTime.IsZero() {
|
||||
firstEventTime = current.TS
|
||||
}
|
||||
count := current.RawSampleCount
|
||||
if count <= 0 {
|
||||
count = 1
|
||||
}
|
||||
return &metricAgg{
|
||||
VIN: current.VIN,
|
||||
Date: date,
|
||||
Protocol: protocol,
|
||||
FirstKM: firstKM,
|
||||
LatestKM: current.TotalKM,
|
||||
Count: count,
|
||||
SourceKey: current.SourceKey,
|
||||
Phone: current.Phone,
|
||||
DeviceID: current.DeviceID,
|
||||
SourceEndpoint: current.SourceEndpoint,
|
||||
FirstEventTime: firstEventTime,
|
||||
LatestEventTime: current.TS,
|
||||
QualityStatus: stats.QualityOK,
|
||||
QualityReason: qualityReason,
|
||||
}
|
||||
}
|
||||
|
||||
type trustedChoice struct {
|
||||
current dailySourceLast
|
||||
previous dailySourceLast
|
||||
@@ -443,10 +445,30 @@ func queryDailyLastSourceRows(ctx context.Context, db *sql.DB, cfg config, proto
|
||||
fmt.Sprintf("protocol = '%s'", quote(string(protocol))),
|
||||
realtimeMileageFramePredicate(),
|
||||
}
|
||||
sqlText := fmt.Sprintf(`SELECT vin, phone, device_id, source_endpoint, LAST(ts), LAST(parsed_json)
|
||||
sqlText := fmt.Sprintf(`SELECT vin, phone, device_id, source_endpoint, FIRST(ts), FIRST(parsed_json), LAST(ts), LAST(parsed_json), COUNT(*)
|
||||
FROM %s.raw_frames
|
||||
WHERE %s
|
||||
GROUP BY vin, phone, device_id, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join(where, " AND "))
|
||||
return querySourceRows(ctx, db, cfg, protocol, sqlText, true)
|
||||
}
|
||||
|
||||
func queryPreviousLastSourceRows(ctx context.Context, db *sql.DB, cfg config, protocol envelope.Protocol, date string) (map[string][]dailySourceLast, error) {
|
||||
where := []string{
|
||||
fmt.Sprintf("ts < '%s 00:00:00'", quote(date)),
|
||||
"parse_status = 'OK'",
|
||||
"vin IS NOT NULL",
|
||||
"vin <> ''",
|
||||
fmt.Sprintf("protocol = '%s'", quote(string(protocol))),
|
||||
realtimeMileageFramePredicate(),
|
||||
}
|
||||
sqlText := fmt.Sprintf(`SELECT vin, phone, device_id, source_endpoint, LAST(ts), LAST(parsed_json), COUNT(*)
|
||||
FROM %s.raw_frames
|
||||
WHERE %s
|
||||
GROUP BY vin, phone, device_id, source_endpoint`, ident(cfg.TDengineDatabase), strings.Join(where, " AND "))
|
||||
return querySourceRows(ctx, db, cfg, protocol, sqlText, false)
|
||||
}
|
||||
|
||||
func querySourceRows(ctx context.Context, db *sql.DB, cfg config, protocol envelope.Protocol, sqlText string, includeFirst bool) (map[string][]dailySourceLast, error) {
|
||||
rows, err := db.QueryContext(ctx, sqlText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -458,25 +480,32 @@ GROUP BY vin, phone, device_id, source_endpoint`, ident(cfg.TDengineDatabase), s
|
||||
var phone string
|
||||
var deviceID string
|
||||
var sourceEndpoint string
|
||||
var firstTS time.Time
|
||||
var firstParsedJSON string
|
||||
var ts time.Time
|
||||
var parsedJSON string
|
||||
if err := rows.Scan(&vin, &phone, &deviceID, &sourceEndpoint, &ts, &parsedJSON); err != nil {
|
||||
return nil, err
|
||||
var rawSampleCount int64
|
||||
if includeFirst {
|
||||
if err := rows.Scan(&vin, &phone, &deviceID, &sourceEndpoint, &firstTS, &firstParsedJSON, &ts, &parsedJSON, &rawSampleCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := rows.Scan(&vin, &phone, &deviceID, &sourceEndpoint, &ts, &parsedJSON, &rawSampleCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firstTS = ts
|
||||
firstParsedJSON = parsedJSON
|
||||
}
|
||||
fields := fieldsForStats(protocol, vin, parsedJSON)
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: protocol,
|
||||
VIN: strings.TrimSpace(vin),
|
||||
EventTimeMS: time.Now().UnixMilli(),
|
||||
ReceivedAtMS: time.Now().UnixMilli(),
|
||||
Fields: fields,
|
||||
ParsedFields: fields,
|
||||
ParseStatus: envelope.ParseOK,
|
||||
}
|
||||
samples, err := stats.SamplesFromEnvelope(env, cfg.Location)
|
||||
if err != nil || len(samples) == 0 {
|
||||
latestTotalKM, ok := mileageFromParsed(protocol, vin, parsedJSON, ts, cfg.Location)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
firstTotalKM := latestTotalKM
|
||||
if includeFirst {
|
||||
if parsedFirst, ok := mileageFromParsed(protocol, vin, firstParsedJSON, firstTS, cfg.Location); ok {
|
||||
firstTotalKM = parsedFirst
|
||||
}
|
||||
}
|
||||
sourceKey := normalizedSourceKey(string(protocol), phone, deviceID, sourceEndpoint)
|
||||
if sourceKey == "" {
|
||||
continue
|
||||
@@ -487,8 +516,11 @@ GROUP BY vin, phone, device_id, source_endpoint`, ident(cfg.TDengineDatabase), s
|
||||
Phone: strings.TrimSpace(phone),
|
||||
DeviceID: strings.TrimSpace(deviceID),
|
||||
SourceEndpoint: strings.TrimSpace(sourceEndpoint),
|
||||
FirstTS: firstTS.In(cfg.Location),
|
||||
TS: ts,
|
||||
TotalKM: samples[0].TotalMileageKM,
|
||||
FirstTotalKM: firstTotalKM,
|
||||
TotalKM: latestTotalKM,
|
||||
RawSampleCount: rawSampleCount,
|
||||
}
|
||||
key := row.VIN + "|" + row.SourceKey
|
||||
if existing, ok := latestBySource[key]; !ok || row.TS.After(existing.TS) {
|
||||
@@ -505,6 +537,24 @@ GROUP BY vin, phone, device_id, source_endpoint`, ident(cfg.TDengineDatabase), s
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func mileageFromParsed(protocol envelope.Protocol, vin string, parsedJSON string, eventTime time.Time, loc *time.Location) (float64, bool) {
|
||||
fields := fieldsForStats(protocol, vin, parsedJSON)
|
||||
env := envelope.FrameEnvelope{
|
||||
Protocol: protocol,
|
||||
VIN: strings.TrimSpace(vin),
|
||||
EventTimeMS: eventTime.UnixMilli(),
|
||||
ReceivedAtMS: eventTime.UnixMilli(),
|
||||
Fields: fields,
|
||||
ParsedFields: fields,
|
||||
ParseStatus: envelope.ParseOK,
|
||||
}
|
||||
samples, err := stats.SamplesFromEnvelope(env, loc)
|
||||
if err != nil || len(samples) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return samples[0].TotalMileageKM, true
|
||||
}
|
||||
|
||||
func normalizedSourceKey(protocol string, phone string, deviceID string, endpoint string) string {
|
||||
sourceIP := stats.NormalizeSourceIP(endpoint)
|
||||
return stats.SourceKey(envelope.Protocol(protocol), phone, deviceID, sourceIP)
|
||||
|
||||
Reference in New Issue
Block a user