mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 15:06:37 +08:00
[优化] 日志查询优化
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestListNodeAccessLogsPaginatedAcrossShards(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "node_access_log_pagination.db")
|
||||
if err := registerSharding(db, "sqlite"); err != nil {
|
||||
t.Fatalf("register sharding: %v", err)
|
||||
}
|
||||
if err := autoMigrateAll(db); err != nil {
|
||||
t.Fatalf("auto migrate db: %v", err)
|
||||
}
|
||||
previousDB := DB
|
||||
DB = db
|
||||
t.Cleanup(func() {
|
||||
DB = previousDB
|
||||
})
|
||||
|
||||
now := time.Now().UTC()
|
||||
for index := range 15 {
|
||||
record := &NodeAccessLog{
|
||||
NodeID: "node-page",
|
||||
LoggedAt: now.Add(-time.Duration(index) * time.Minute),
|
||||
RemoteAddr: fmt.Sprintf("203.0.113.%d", (index%5)+1),
|
||||
Host: "example.com",
|
||||
Path: fmt.Sprintf("/path-%02d", index),
|
||||
StatusCode: 200,
|
||||
}
|
||||
if err := db.Create(record).Error; err != nil {
|
||||
t.Fatalf("seed access log %d: %v", index, err)
|
||||
}
|
||||
}
|
||||
|
||||
query := NodeAccessLogQuery{
|
||||
NodeID: "node-page",
|
||||
Page: 1,
|
||||
PageSize: 5,
|
||||
SortBy: "logged_at",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
page, err := ListNodeAccessLogs(query)
|
||||
if err != nil {
|
||||
t.Fatalf("ListNodeAccessLogs failed: %v", err)
|
||||
}
|
||||
if len(page) != 5 {
|
||||
t.Fatalf("expected 5 rows, got %d", len(page))
|
||||
}
|
||||
if page[0].Path != "/path-05" || page[4].Path != "/path-09" {
|
||||
t.Fatalf("unexpected page ordering: %+v", page)
|
||||
}
|
||||
|
||||
totalRecords, totalIPs, err := CountNodeAccessLogs(query)
|
||||
if err != nil {
|
||||
t.Fatalf("CountNodeAccessLogs failed: %v", err)
|
||||
}
|
||||
if totalRecords != 15 {
|
||||
t.Fatalf("expected total_records=15, got %d", totalRecords)
|
||||
}
|
||||
if totalIPs != 5 {
|
||||
t.Fatalf("expected total_ip=5, got %d", totalIPs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAccessLogOptimizedQueriesMatchReference(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "node_access_log_correctness.db")
|
||||
if err := registerSharding(db, "sqlite"); err != nil {
|
||||
t.Fatalf("register sharding: %v", err)
|
||||
}
|
||||
if err := autoMigrateAll(db); err != nil {
|
||||
t.Fatalf("auto migrate db: %v", err)
|
||||
}
|
||||
previousDB := DB
|
||||
DB = db
|
||||
t.Cleanup(func() {
|
||||
DB = previousDB
|
||||
})
|
||||
|
||||
now := time.Now().UTC()
|
||||
records := []*NodeAccessLog{
|
||||
{NodeID: "node-a", LoggedAt: now.Add(-5 * time.Minute), RemoteAddr: "1.1.1.1", Host: "a.example.com", Path: "/alpha", StatusCode: 200},
|
||||
{NodeID: "node-a", LoggedAt: now.Add(-4 * time.Minute), RemoteAddr: "2.2.2.2", Host: "a.example.com", Path: "/beta", StatusCode: 404},
|
||||
{NodeID: "node-b", LoggedAt: now.Add(-3 * time.Minute), RemoteAddr: "1.1.1.1", Host: "b.example.com", Path: "/gamma", StatusCode: 502},
|
||||
{NodeID: "node-b", LoggedAt: now.Add(-2 * time.Minute), RemoteAddr: " 3.3.3.3 ", Host: "b.example.com", Path: "/delta", StatusCode: 200},
|
||||
{NodeID: "node-b", LoggedAt: now.Add(-1 * time.Minute), RemoteAddr: "", Host: "b.example.com", Path: "/empty-ip", StatusCode: 200},
|
||||
}
|
||||
for _, record := range records {
|
||||
if err := db.Create(record).Error; err != nil {
|
||||
t.Fatalf("seed access log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
baseQuery := NodeAccessLogQuery{
|
||||
Since: now.Add(-10 * time.Minute),
|
||||
SortBy: "logged_at",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
reference, err := listNodeAccessLogsAcrossShards(baseQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("reference list failed: %v", err)
|
||||
}
|
||||
referenceTotal, referenceIPs, err := countNodeAccessLogsReference(baseQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("reference count failed: %v", err)
|
||||
}
|
||||
|
||||
totalRecords, totalIPs, err := CountNodeAccessLogs(baseQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("CountNodeAccessLogs failed: %v", err)
|
||||
}
|
||||
if totalRecords != referenceTotal {
|
||||
t.Fatalf("total_records mismatch: got %d want %d", totalRecords, referenceTotal)
|
||||
}
|
||||
if totalIPs != referenceIPs {
|
||||
t.Fatalf("total_ip mismatch: got %d want %d", totalIPs, referenceIPs)
|
||||
}
|
||||
if totalRecords != int64(len(reference)) {
|
||||
t.Fatalf("total_records should equal reference rows: got %d want %d", totalRecords, len(reference))
|
||||
}
|
||||
|
||||
for page := range 3 {
|
||||
query := baseQuery
|
||||
query.Page = page
|
||||
query.PageSize = 2
|
||||
pageRows, err := ListNodeAccessLogs(query)
|
||||
if err != nil {
|
||||
t.Fatalf("ListNodeAccessLogs page %d failed: %v", page, err)
|
||||
}
|
||||
start, end := paginateBounds(len(reference), page, query.PageSize)
|
||||
if start >= len(reference) {
|
||||
if len(pageRows) != 0 {
|
||||
t.Fatalf("page %d expected empty slice, got %d rows", page, len(pageRows))
|
||||
}
|
||||
continue
|
||||
}
|
||||
want := reference[start:end]
|
||||
if !nodeAccessLogsEqual(pageRows, want) {
|
||||
t.Fatalf("page %d mismatch:\n got=%+v\nwant=%+v", page, pageRows, want)
|
||||
}
|
||||
}
|
||||
|
||||
filteredQuery := NodeAccessLogQuery{
|
||||
NodeID: "node-a",
|
||||
Since: baseQuery.Since,
|
||||
SortBy: "status_code",
|
||||
SortOrder: "asc",
|
||||
Page: 0,
|
||||
PageSize: 10,
|
||||
}
|
||||
filteredReference, err := listNodeAccessLogsAcrossShards(filteredQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("filtered reference list failed: %v", err)
|
||||
}
|
||||
filteredRows, err := ListNodeAccessLogs(filteredQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("filtered ListNodeAccessLogs failed: %v", err)
|
||||
}
|
||||
if !nodeAccessLogsEqual(filteredRows, filteredReference) {
|
||||
t.Fatalf("filtered list mismatch:\n got=%+v\nwant=%+v", filteredRows, filteredReference)
|
||||
}
|
||||
filteredTotal, filteredIPs, err := CountNodeAccessLogs(filteredQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("filtered CountNodeAccessLogs failed: %v", err)
|
||||
}
|
||||
wantFilteredTotal, wantFilteredIPs, err := countNodeAccessLogsReference(filteredQuery)
|
||||
if err != nil {
|
||||
t.Fatalf("filtered reference count failed: %v", err)
|
||||
}
|
||||
if filteredTotal != wantFilteredTotal || filteredIPs != wantFilteredIPs {
|
||||
t.Fatalf("filtered count mismatch: got (%d,%d) want (%d,%d)", filteredTotal, filteredIPs, wantFilteredTotal, wantFilteredIPs)
|
||||
}
|
||||
}
|
||||
|
||||
func countNodeAccessLogsReference(query NodeAccessLogQuery) (int64, int64, error) {
|
||||
all, err := listNodeAccessLogsAcrossShards(query)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
ips := make(map[string]struct{})
|
||||
for _, item := range all {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
if trimmed := strings.TrimSpace(item.RemoteAddr); trimmed != "" {
|
||||
ips[trimmed] = struct{}{}
|
||||
}
|
||||
}
|
||||
return int64(len(all)), int64(len(ips)), nil
|
||||
}
|
||||
|
||||
func nodeAccessLogsEqual(left []*NodeAccessLog, right []*NodeAccessLog) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == nil || right[index] == nil {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if left[index].ID != right[index].ID ||
|
||||
left[index].NodeID != right[index].NodeID ||
|
||||
!left[index].LoggedAt.Equal(right[index].LoggedAt) ||
|
||||
left[index].RemoteAddr != right[index].RemoteAddr ||
|
||||
left[index].Host != right[index].Host ||
|
||||
left[index].Path != right[index].Path ||
|
||||
left[index].StatusCode != right[index].StatusCode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestNodeAccessLogAggregationsMatchReference(t *testing.T) {
|
||||
db := openBareTestSQLiteDB(t, "node_access_log_agg.db")
|
||||
if err := registerSharding(db, "sqlite"); err != nil {
|
||||
t.Fatalf("register sharding: %v", err)
|
||||
}
|
||||
if err := autoMigrateAll(db); err != nil {
|
||||
t.Fatalf("auto migrate db: %v", err)
|
||||
}
|
||||
previousDB := DB
|
||||
DB = db
|
||||
t.Cleanup(func() {
|
||||
DB = previousDB
|
||||
})
|
||||
|
||||
now := time.Date(2026, 3, 19, 8, 12, 30, 0, time.UTC)
|
||||
records := []*NodeAccessLog{
|
||||
{NodeID: "node-folded", LoggedAt: now.Add(-4 * time.Minute), RemoteAddr: "203.0.113.1", Host: "alpha.example.com", Path: "/first", StatusCode: 200},
|
||||
{NodeID: "node-folded", LoggedAt: now.Add(-3 * time.Minute), RemoteAddr: "203.0.113.1", Host: "alpha.example.com", Path: "/second", StatusCode: 502},
|
||||
{NodeID: "node-folded", LoggedAt: now.Add(-2 * time.Minute), RemoteAddr: "203.0.113.2", Host: "beta.example.com", Path: "/third", StatusCode: 404},
|
||||
}
|
||||
for _, record := range records {
|
||||
if err := db.Create(record).Error; err != nil {
|
||||
t.Fatalf("seed access log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
since := now.Add(-10 * time.Minute)
|
||||
bucketRows, err := buildNodeAccessLogBucketRows(NodeAccessLogBucketQuery{
|
||||
NodeID: "node-folded", Since: since, FoldMinutes: 5, SortBy: "request_count", SortOrder: "desc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeAccessLogBucketRows failed: %v", err)
|
||||
}
|
||||
referenceBuckets := referenceBucketRows(records, 5, "request_count", "desc")
|
||||
if !bucketRowsEqual(bucketRows, referenceBuckets) {
|
||||
t.Fatalf("bucket rows mismatch:\n got=%+v\nwant=%+v", bucketRows, referenceBuckets)
|
||||
}
|
||||
|
||||
if len(bucketRows) == 0 {
|
||||
t.Fatal("expected bucket rows before bucket ip verification")
|
||||
}
|
||||
bucketStartedAt := time.Unix(bucketRows[0].BucketEpoch, 0).UTC()
|
||||
bucketIPRows, err := buildNodeAccessLogBucketIPRows(NodeAccessLogBucketIPQuery{
|
||||
NodeID: "node-folded", BucketStartedAt: bucketStartedAt, FoldMinutes: 5, SortBy: "request_count", SortOrder: "desc",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeAccessLogBucketIPRows failed: %v", err)
|
||||
}
|
||||
referenceBucketIPs := referenceBucketIPRows(records, bucketStartedAt, 5, "request_count", "desc")
|
||||
if !bucketIPRowsEqual(bucketIPRows, referenceBucketIPs) {
|
||||
if len(bucketIPRows) > 0 && len(referenceBucketIPs) > 0 {
|
||||
t.Fatalf("bucket ip rows mismatch:\n got=%+v\nwant=%+v", *bucketIPRows[0], *referenceBucketIPs[0])
|
||||
}
|
||||
t.Fatalf("bucket ip rows mismatch:\n got=%+v\nwant=%+v", bucketIPRows, referenceBucketIPs)
|
||||
}
|
||||
|
||||
recentSince := now.Add(-150 * time.Minute)
|
||||
summaryRows, err := buildNodeAccessLogIPSummaryRows(NodeAccessLogIPSummaryQuery{
|
||||
NodeID: "node-folded", Since: since, SortBy: "total_requests", SortOrder: "desc",
|
||||
}, recentSince)
|
||||
if err != nil {
|
||||
t.Fatalf("buildNodeAccessLogIPSummaryRows failed: %v", err)
|
||||
}
|
||||
referenceSummaries := referenceIPSummaryRows(records, since, recentSince, "total_requests", "desc")
|
||||
if !ipSummaryRowsEqual(summaryRows, referenceSummaries) {
|
||||
t.Fatalf("ip summary rows mismatch:\n got=%+v\nwant=%+v", summaryRows, referenceSummaries)
|
||||
}
|
||||
|
||||
trendRows, err := queryIPTrendRows(NodeAccessLogIPTrendQuery{
|
||||
NodeID: "node-folded", RemoteAddr: "203.0.113.1", Since: since, BucketMinutes: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("queryIPTrendRows failed: %v", err)
|
||||
}
|
||||
referenceTrend := referenceIPTrendRows(records, "203.0.113.1", 5)
|
||||
if !trendRowsEqual(trendRows, referenceTrend) {
|
||||
t.Fatalf("trend rows mismatch:\n got=%+v\nwant=%+v", trendRows, referenceTrend)
|
||||
}
|
||||
}
|
||||
|
||||
func referenceBucketRows(records []*NodeAccessLog, foldMinutes int, sortBy string, sortOrder string) []*NodeAccessLogBucketRow {
|
||||
type bucketAccumulator struct {
|
||||
requestCount int64
|
||||
uniqueIPs map[string]struct{}
|
||||
uniqueHosts map[string]struct{}
|
||||
successCount int64
|
||||
clientErrorCount int64
|
||||
serverErrorCount int64
|
||||
}
|
||||
accumulators := make(map[int64]*bucketAccumulator)
|
||||
for _, item := range records {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
bucketEpoch := bucketEpochForTime(item.LoggedAt, foldMinutes)
|
||||
accumulator := accumulators[bucketEpoch]
|
||||
if accumulator == nil {
|
||||
accumulator = &bucketAccumulator{
|
||||
uniqueIPs: make(map[string]struct{}),
|
||||
uniqueHosts: make(map[string]struct{}),
|
||||
}
|
||||
accumulators[bucketEpoch] = accumulator
|
||||
}
|
||||
accumulator.requestCount++
|
||||
if trimmed := strings.TrimSpace(item.RemoteAddr); trimmed != "" {
|
||||
accumulator.uniqueIPs[trimmed] = struct{}{}
|
||||
}
|
||||
if trimmed := strings.TrimSpace(item.Host); trimmed != "" {
|
||||
accumulator.uniqueHosts[trimmed] = struct{}{}
|
||||
}
|
||||
switch {
|
||||
case item.StatusCode < 400:
|
||||
accumulator.successCount++
|
||||
case item.StatusCode < 500:
|
||||
accumulator.clientErrorCount++
|
||||
default:
|
||||
accumulator.serverErrorCount++
|
||||
}
|
||||
}
|
||||
rows := make([]*NodeAccessLogBucketRow, 0, len(accumulators))
|
||||
for bucketEpoch, accumulator := range accumulators {
|
||||
rows = append(rows, &NodeAccessLogBucketRow{
|
||||
BucketEpoch: bucketEpoch,
|
||||
RequestCount: accumulator.requestCount,
|
||||
UniqueIPCount: int64(len(accumulator.uniqueIPs)),
|
||||
UniqueHostCount: int64(len(accumulator.uniqueHosts)),
|
||||
SuccessCount: accumulator.successCount,
|
||||
ClientErrorCount: accumulator.clientErrorCount,
|
||||
ServerErrorCount: accumulator.serverErrorCount,
|
||||
})
|
||||
}
|
||||
sortNodeAccessLogBucketRows(rows, sortBy, sortOrder)
|
||||
return rows
|
||||
}
|
||||
|
||||
func referenceBucketIPRows(records []*NodeAccessLog, bucketStartedAt time.Time, foldMinutes int, sortBy string, sortOrder string) []*NodeAccessLogBucketIPRow {
|
||||
type accumulator struct {
|
||||
requestCount int64
|
||||
successCount int64
|
||||
clientErrorCount int64
|
||||
serverErrorCount int64
|
||||
lastSeenAt time.Time
|
||||
}
|
||||
accumulators := make(map[string]*accumulator)
|
||||
until := bucketStartedAt.Add(time.Duration(foldMinutes) * time.Minute)
|
||||
for _, item := range records {
|
||||
if item == nil || item.LoggedAt.Before(bucketStartedAt) || !item.LoggedAt.Before(until) {
|
||||
continue
|
||||
}
|
||||
remoteAddr := strings.TrimSpace(item.RemoteAddr)
|
||||
if remoteAddr == "" {
|
||||
continue
|
||||
}
|
||||
acc := accumulators[remoteAddr]
|
||||
if acc == nil {
|
||||
acc = &accumulator{}
|
||||
accumulators[remoteAddr] = acc
|
||||
}
|
||||
acc.requestCount++
|
||||
switch {
|
||||
case item.StatusCode < 400:
|
||||
acc.successCount++
|
||||
case item.StatusCode < 500:
|
||||
acc.clientErrorCount++
|
||||
default:
|
||||
acc.serverErrorCount++
|
||||
}
|
||||
if item.LoggedAt.After(acc.lastSeenAt) {
|
||||
acc.lastSeenAt = item.LoggedAt
|
||||
}
|
||||
}
|
||||
rows := make([]*NodeAccessLogBucketIPRow, 0, len(accumulators))
|
||||
for remoteAddr, acc := range accumulators {
|
||||
rows = append(rows, &NodeAccessLogBucketIPRow{
|
||||
RemoteAddr: remoteAddr,
|
||||
RequestCount: acc.requestCount,
|
||||
SuccessCount: acc.successCount,
|
||||
ClientErrorCount: acc.clientErrorCount,
|
||||
ServerErrorCount: acc.serverErrorCount,
|
||||
LastSeenEpoch: acc.lastSeenAt.Unix(),
|
||||
})
|
||||
}
|
||||
sortNodeAccessLogBucketIPRows(rows, sortBy, sortOrder)
|
||||
return rows
|
||||
}
|
||||
|
||||
func referenceIPSummaryRows(records []*NodeAccessLog, since time.Time, recentSince time.Time, sortBy string, sortOrder string) []*NodeAccessLogIPSummaryRow {
|
||||
type accumulator struct {
|
||||
totalRequests int64
|
||||
recentRequests int64
|
||||
lastSeenAt time.Time
|
||||
}
|
||||
accumulators := make(map[string]*accumulator)
|
||||
for _, item := range records {
|
||||
if item == nil || item.LoggedAt.Before(since) {
|
||||
continue
|
||||
}
|
||||
remoteAddr := strings.TrimSpace(item.RemoteAddr)
|
||||
if remoteAddr == "" {
|
||||
continue
|
||||
}
|
||||
acc := accumulators[remoteAddr]
|
||||
if acc == nil {
|
||||
acc = &accumulator{}
|
||||
accumulators[remoteAddr] = acc
|
||||
}
|
||||
acc.totalRequests++
|
||||
if !recentSince.IsZero() && !item.LoggedAt.Before(recentSince) {
|
||||
acc.recentRequests++
|
||||
}
|
||||
if item.LoggedAt.After(acc.lastSeenAt) {
|
||||
acc.lastSeenAt = item.LoggedAt
|
||||
}
|
||||
}
|
||||
rows := make([]*NodeAccessLogIPSummaryRow, 0, len(accumulators))
|
||||
for remoteAddr, acc := range accumulators {
|
||||
rows = append(rows, &NodeAccessLogIPSummaryRow{
|
||||
RemoteAddr: remoteAddr,
|
||||
TotalRequests: acc.totalRequests,
|
||||
RecentRequests: acc.recentRequests,
|
||||
LastSeenEpoch: acc.lastSeenAt.Unix(),
|
||||
})
|
||||
}
|
||||
sortNodeAccessLogIPSummaryRows(rows, sortBy, sortOrder)
|
||||
return rows
|
||||
}
|
||||
|
||||
func referenceIPTrendRows(records []*NodeAccessLog, remoteAddr string, bucketMinutes int) []*NodeAccessLogTrendPointRow {
|
||||
buckets := make(map[int64]int64)
|
||||
for _, item := range records {
|
||||
if item == nil || strings.TrimSpace(item.RemoteAddr) != remoteAddr {
|
||||
continue
|
||||
}
|
||||
buckets[bucketEpochForTime(item.LoggedAt, bucketMinutes)]++
|
||||
}
|
||||
rows := make([]*NodeAccessLogTrendPointRow, 0, len(buckets))
|
||||
for bucketEpoch, requestCount := range buckets {
|
||||
rows = append(rows, &NodeAccessLogTrendPointRow{BucketEpoch: bucketEpoch, RequestCount: requestCount})
|
||||
}
|
||||
sort.Slice(rows, func(i int, j int) bool { return rows[i].BucketEpoch < rows[j].BucketEpoch })
|
||||
return rows
|
||||
}
|
||||
|
||||
func bucketRowsEqual(left []*NodeAccessLogBucketRow, right []*NodeAccessLogBucketRow) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == nil || right[index] == nil {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if *left[index] != *right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func bucketIPRowsEqual(left []*NodeAccessLogBucketIPRow, right []*NodeAccessLogBucketIPRow) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == nil || right[index] == nil {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if *left[index] != *right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ipSummaryRowsEqual(left []*NodeAccessLogIPSummaryRow, right []*NodeAccessLogIPSummaryRow) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == nil || right[index] == nil {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if *left[index] != *right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func trendRowsEqual(left []*NodeAccessLogTrendPointRow, right []*NodeAccessLogTrendPointRow) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == nil || right[index] == nil {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if *left[index] != *right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestNodeAccessLogOrderClauseMatchesSort(t *testing.T) {
|
||||
if got := nodeAccessLogOrderClause("logged_at", "desc"); got != "logged_at DESC, id DESC" {
|
||||
t.Fatalf("unexpected logged_at order clause: %q", got)
|
||||
}
|
||||
if got := nodeAccessLogOrderClause("status_code", "asc"); got != "status_code ASC, logged_at ASC, id ASC" {
|
||||
t.Fatalf("unexpected status_code order clause: %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user