Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 7 additions & 18 deletions client/src/pages/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Helmet } from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { postRequest } from '../../api';
import { errorAlert } from '../../util';
import { useAdminStore, useClockStore, useFlagsStore, useOptionsStore } from '../../store';
import { useAdminStore } from '../../store';
import AdminStatsPanel from '../../components/admin/AdminStatsPanel';
import AdminTable from '../../components/admin/tables/AdminTable';
import AdminToggleSwitch from '../../components/admin/AdminToggleSwitch';
Expand All @@ -19,19 +19,14 @@ import JudgesTable from '../../components/admin/tables/JudgesTable';

const Admin = () => {
const navigate = useNavigate();
const fetchStats = useAdminStore((state) => state.fetchStats);
const fetchClock = useClockStore((state) => state.fetchClock);
const fetchProjects = useAdminStore((state) => state.fetchProjects);
const fetchJudges = useAdminStore((state) => state.fetchJudges);
const fetchOptions = useOptionsStore((state) => state.fetchOptions);
const fetchFlags = useFlagsStore((state) => state.fetchFlags);
const fetchDashboard = useAdminStore((state) => state.fetchDashboard);

const [showProjects, setShowProjects] = useState(true);
const [loading, setLoading] = useState(true);
const [lastUpdate, setLastUpdate] = useState(new Date());

useEffect(() => {
// Check if user logged in
// Check if user logged in, then load all dashboard data in one request.
async function checkLoggedIn() {
const loggedInRes = await postRequest<OkResponse>('/admin/auth', 'admin', null);
if (loggedInRes.status === 401) {
Expand All @@ -41,27 +36,21 @@ const Admin = () => {
}
if (loggedInRes.status === 200) {
setLoading(false);
fetchDashboard();
return;
}

errorAlert(loggedInRes);
}

checkLoggedIn();
fetchOptions();
fetchFlags();
}, []);

useEffect(() => {
// Set 15 seconds interval to refresh admin stats and clock
// Poll once every 15 seconds using a single /admin/dashboard request
// instead of the previous 6 separate API calls.
const refresh = setInterval(async () => {
// Fetch stats and clock
fetchStats();
fetchClock();
fetchProjects();
fetchJudges();
fetchOptions();
fetchFlags();
fetchDashboard();
setLastUpdate(new Date());
}, 15000);

Expand Down
20 changes: 20 additions & 0 deletions client/src/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface AdminStore {
fetchJudgeStats: () => Promise<void>;
projectStats: ProjectStats;
fetchProjectStats: () => Promise<void>;
fetchDashboard: () => Promise<void>;
}

const useAdminStore = create<AdminStore>()((set) => ({
Expand Down Expand Up @@ -85,6 +86,25 @@ const useAdminStore = create<AdminStore>()((set) => ({
}
set({ projectStats: statsRes.data as ProjectStats });
},

// fetchDashboard replaces the 6 separate polling calls the admin page
// previously made every 15 seconds. One HTTP request, one DB round-trip
// set, all stores updated atomically.
fetchDashboard: async () => {
const selectedTrack = useOptionsStore.getState().selectedTrack;
const trackParam = selectedTrack !== '' ? `?track=${encodeURIComponent(selectedTrack)}` : '';
const res = await getRequest<DashboardResponse>(`/admin/dashboard${trackParam}`, 'admin');
if (res.status !== 200) {
errorAlert(res);
return;
}
const data = res.data as DashboardResponse;

set({ stats: data.stats, projects: data.projects, judges: data.judges });
useClockStore.setState({ clock: data.clock });
useOptionsStore.setState({ options: data.options });
useFlagsStore.setState({ flags: data.flags });
},
}));

interface ClockStore {
Expand Down
9 changes: 9 additions & 0 deletions client/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,12 @@ interface GroupInfo {
names: string[];
enabled: boolean;
}

interface DashboardResponse {
stats: Stats;
clock: ClockState;
projects: Project[];
judges: Judge[];
options: Options;
flags: Flag[];
}
68 changes: 7 additions & 61 deletions server/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ package logging
import (
"context"
"fmt"
"server/database"
"server/models"
"strings"
"sync"
"time"

"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

type Logger struct {
Mutex sync.Mutex
Memory []string
DbRef *mongo.Database
}

type LogType int
Expand All @@ -28,16 +25,14 @@ const (
Judge
)

const DbLogLimit = 10000

func NewLogger(db *mongo.Database) (*Logger, error) {
// Get all logs from the db
// Load historical log entries from the database so the admin log
// view still shows entries from before the current server session.
dbLogs, err := GetAllDbLogs(db)
if err != nil {
return nil, err
}

// Create a new logger and add all logs from the database
memory := []string{}
for _, log := range dbLogs {
memory = append(memory, log.Entries...)
Expand All @@ -46,91 +41,43 @@ func NewLogger(db *mongo.Database) (*Logger, error) {
return &Logger{
Mutex: sync.Mutex{},
Memory: memory,
DbRef: db,
}, nil
}

// GetAllDbLogs retrieves all logs from the database.
func GetAllDbLogs(db *mongo.Database) ([]*models.Log, error) {
// Get all logs from the database, sorted in ascending order of time
cursor, err := db.Collection("logs").Find(context.Background(), gin.H{}, options.Find().SetSort(gin.H{"time": 1}))
cursor, err := db.Collection("logs").Find(context.Background(), map[string]interface{}{}, options.Find().SetSort(map[string]interface{}{"time": 1}))
if err != nil {
return nil, err
}

// Iterate through the cursor and decode each log
logs := []*models.Log{}
for cursor.Next(context.Background()) {
var log models.Log
err := cursor.Decode(&log)
if err != nil {
return nil, err
}

logs = append(logs, &log)
}

// If no logs, create one
if len(logs) == 0 {
_, err = db.Collection("logs").InsertOne(context.Background(), models.NewLog())
if err != nil {
return nil, err
}
}

return logs, nil
}

func (l *Logger) writeToDb(item string) error {
return database.WithTransaction(l.DbRef, func(sc mongo.SessionContext) error {
// Insert the log into the database
res := l.DbRef.Collection("logs").FindOneAndUpdate(
sc,
gin.H{},
gin.H{"$push": gin.H{"entries": item}, "$inc": gin.H{"count": 1}},
options.FindOneAndUpdate().SetSort(gin.H{"time": -1}),
)

// Check for errors
if res.Err() != nil {
return res.Err()
}

// Get the log from the result
var log models.Log
err := res.Decode(&log)
if err != nil {
return err
}

// If the log is too long, create a new log in the db
if log.Count > DbLogLimit {
_, err = l.DbRef.Collection("logs").InsertOne(sc, models.NewLog())
if err != nil {
return err
}
}

return nil
})
}

// Logf logs a message to the log file.
// Logf logs a message to the in-memory log.
// The time is prepended to the message and a newline is appended.
// Note: entries are no longer written to MongoDB on every call — doing so
// required a serialised majority-write transaction per API request and was
// the primary source of lock contention under concurrent judge load.
func (l *Logger) Logf(t LogType, message string, args ...interface{}) error {
l.Mutex.Lock()
defer l.Mutex.Unlock()

// Form output string
userInput := fmt.Sprintf(message, args...)
output := fmt.Sprintf("[%s] %s | %s", time.Now().Format(time.RFC3339), typeToString(t), userInput)

// Write to memory
l.Memory = append(l.Memory, output)

// Write to database
l.writeToDb(output)

return nil
}

Expand All @@ -140,7 +87,6 @@ func (l *Logger) SystemLogf(message string, args ...interface{}) error {
}

// JudgeLogf logs a message with the Judge LogType.
// This will include a judge and project (if defined) in the log message.
func (l *Logger) JudgeLogf(judge *models.Judge, message string, args ...interface{}) error {
judgeDisplay := ""
if judge != nil {
Expand Down
Loading
Loading