Add more metrics
This commit is contained in:
parent
0a5139fd39
commit
d30222911d
12
Sources/App/Extensions/Sequence+Extensions.swift
Normal file
12
Sources/App/Extensions/Sequence+Extensions.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
extension Sequence {
|
||||
|
||||
func count(where isIncluded: (Element) -> Bool) -> Int {
|
||||
reduce(0) { isIncluded($1) ? $0 + 1 : $0 }
|
||||
}
|
||||
|
||||
func sum(converting: (Element) -> Int) -> Int {
|
||||
reduce(0) { $0 + converting($1) }
|
||||
}
|
||||
}
|
@ -2,19 +2,41 @@ import Foundation
|
||||
import Fluent
|
||||
import Vapor
|
||||
import SwiftSMTP
|
||||
import Clairvoyant
|
||||
|
||||
typealias PasswordHash = String
|
||||
typealias SessionToken = String
|
||||
|
||||
private struct MailConfig {
|
||||
|
||||
let mailConfiguration: Configuration.EMail
|
||||
/// The url to the root of the server
|
||||
let serverDomain: String
|
||||
|
||||
/// The number of minutes until a password reset token is no longer valid
|
||||
let tokenExpiryDuration: Int
|
||||
|
||||
let smtp: SMTP
|
||||
|
||||
let mailSender: Mail.User
|
||||
}
|
||||
|
||||
private extension Configuration.EMail {
|
||||
|
||||
var mailConfig: MailConfig {
|
||||
let smtp = SMTP(
|
||||
hostname: emailHostname,
|
||||
email: email,
|
||||
password: password)
|
||||
let mailSender = Mail.User(name: "Schafkopf Server", email: email)
|
||||
|
||||
return .init(
|
||||
serverDomain: serverDomain,
|
||||
tokenExpiryDuration: tokenExpiryDuration,
|
||||
smtp: smtp,
|
||||
mailSender: mailSender)
|
||||
}
|
||||
}
|
||||
|
||||
final class SQLiteDatabase {
|
||||
|
||||
/// A mapping between player name and generated access tokens for a session
|
||||
@ -27,19 +49,16 @@ final class SQLiteDatabase {
|
||||
|
||||
private let mail: MailConfig?
|
||||
|
||||
init(db: Database, mail: Configuration.EMail?) {
|
||||
self.tables = TableManagement(db: db)
|
||||
guard let mail else {
|
||||
self.mail = nil
|
||||
return
|
||||
}
|
||||
let smtp = SMTP(
|
||||
hostname: mail.emailHostname,
|
||||
email: mail.email,
|
||||
password: mail.password)
|
||||
let mailSender = Mail.User(name: "Schafkopf Server", email: mail.email)
|
||||
private let registeredPlayerCountMetric: Metric<Int>
|
||||
|
||||
self.mail = .init(mailConfiguration: mail, smtp: smtp, mailSender: mailSender)
|
||||
init(database: Database, mail: Configuration.EMail?) {
|
||||
self.registeredPlayerCountMetric = Metric("schafkopf.players")
|
||||
self.tables = TableManagement(database: database)
|
||||
self.mail = mail?.mailConfig
|
||||
|
||||
Task {
|
||||
await updateRegisteredPlayerCount(from: database)
|
||||
}
|
||||
}
|
||||
|
||||
func registerPlayer(named name: PlayerName, hash: PasswordHash, email: String?, in database: Database) async throws -> SessionToken {
|
||||
@ -62,9 +81,20 @@ final class SQLiteDatabase {
|
||||
let token = SessionToken.newToken()
|
||||
self.sessionTokenForPlayer[name] = token
|
||||
self.playerNameForToken[token] = name
|
||||
|
||||
await updateRegisteredPlayerCount(from: database)
|
||||
return token
|
||||
}
|
||||
|
||||
func updateRegisteredPlayerCount(from database: Database) async {
|
||||
do {
|
||||
let count = try await User.query(on: database).count()
|
||||
registeredPlayerCountMetric.update(count)
|
||||
} catch {
|
||||
print("Failed to update player count metric: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Send a password reset email.
|
||||
|
||||
@ -96,8 +126,8 @@ final class SQLiteDatabase {
|
||||
return
|
||||
}
|
||||
let recipient = Mail.User(name: name, email: email)
|
||||
let mailConfiguration = mailData.mailConfiguration
|
||||
let url = "\(mailConfiguration.serverDomain)/recovery.html?token=\(token)"
|
||||
//let mailConfiguration = mailData.mailConfiguration
|
||||
let url = "\(mailData.serverDomain)/recovery.html?token=\(token)"
|
||||
let mail = Mail(
|
||||
from: mailData.mailSender,
|
||||
to: [recipient],
|
||||
@ -106,12 +136,12 @@ final class SQLiteDatabase {
|
||||
"""
|
||||
Hello \(name),
|
||||
|
||||
a reset of your account password has been requested for the Schafkopf Server at \(mailConfiguration.serverDomain).
|
||||
a reset of your account password has been requested for the Schafkopf Server at \(mailData.serverDomain).
|
||||
To choose a new password, click the following link:
|
||||
|
||||
\(url)
|
||||
|
||||
The link will expire in \(mailConfiguration.tokenExpiryDuration) minutes. If you didn't request the password reset, you don't have to do anything.
|
||||
The link will expire in \(mailData.tokenExpiryDuration) minutes. If you didn't request the password reset, you don't have to do anything.
|
||||
|
||||
Regards,
|
||||
|
||||
@ -169,6 +199,7 @@ final class SQLiteDatabase {
|
||||
let user = try await user(named: name, in: database)
|
||||
try await tables.leaveTable(player: user, in: database)
|
||||
try await User.query(on: database).filter(\.$name == name).delete()
|
||||
await updateRegisteredPlayerCount(from: database)
|
||||
}
|
||||
|
||||
func isValid(sessionToken token: SessionToken) -> Bool {
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import WebSocketKit
|
||||
import Vapor
|
||||
import Fluent
|
||||
import Clairvoyant
|
||||
|
||||
let maximumPlayersPerTable = 4
|
||||
|
||||
@ -13,24 +14,62 @@ final class TableManagement {
|
||||
/// All tables indexed by their id
|
||||
private var tables = [UUID : ManageableTable]()
|
||||
|
||||
/// The metric to log the current number of tables
|
||||
private var tableCountMetric: Metric<Int>
|
||||
|
||||
/// The metric describing the number of players currently sitting at a table
|
||||
private let playingPlayerCountMetric: Metric<Int>
|
||||
|
||||
/// The metric describing the number of players currently connected via a websocket
|
||||
private let connectedPlayerCountMetric: Metric<Int>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Load the tables from a file in the storage folder
|
||||
- Throws: Errors when the file could not be read
|
||||
*/
|
||||
init(db: Database) {
|
||||
Table.query(on: db).with(\.$players).all().whenSuccess { loadedTables in
|
||||
for table in loadedTables {
|
||||
guard !table.players.isEmpty else {
|
||||
_ = table.delete(on: db)
|
||||
continue
|
||||
}
|
||||
let id = table.id!
|
||||
self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players)
|
||||
init(database: Database) {
|
||||
self.tableCountMetric = .init("schafkopf.tables")
|
||||
self.playingPlayerCountMetric = .init("schafkopf.playing")
|
||||
self.connectedPlayerCountMetric = .init("schafkopf.connected")
|
||||
|
||||
Task {
|
||||
do {
|
||||
try await loadTables(from: database)
|
||||
} catch {
|
||||
print("Failed to load tables: \(error)")
|
||||
}
|
||||
print("\(self.tables.count) tables loaded")
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTables(from database: Database) async throws {
|
||||
try await Table.query(on: database).with(\.$players).all().forEach { table in
|
||||
guard !table.players.isEmpty else {
|
||||
_ = table.delete(on: database)
|
||||
return
|
||||
}
|
||||
let id = table.id!
|
||||
self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players)
|
||||
}
|
||||
print("\(tables.count) tables loaded")
|
||||
logTableCount()
|
||||
logPlayingPlayerCount()
|
||||
logConnectedPlayerCount()
|
||||
}
|
||||
|
||||
private func logTableCount() {
|
||||
tableCountMetric.update(tables.count)
|
||||
}
|
||||
|
||||
private func logPlayingPlayerCount() {
|
||||
let count = tables.values.sum { $0.playerCount }
|
||||
}
|
||||
|
||||
private func logConnectedPlayerCount() {
|
||||
let count = tables.values.sum { $0.numberOfConnectedPlayers }
|
||||
}
|
||||
|
||||
/**
|
||||
Create a new table with optional players.
|
||||
- Parameter name: The name of the table
|
||||
@ -45,6 +84,8 @@ final class TableManagement {
|
||||
try await player.update(on: database)
|
||||
let waitingTable = WaitingTable(newTable: table, user: player)
|
||||
self.tables[waitingTable.id] = waitingTable
|
||||
logTableCount()
|
||||
logPlayingPlayerCount()
|
||||
return waitingTable.tableInfo(forPlayer: player.name)
|
||||
}
|
||||
|
||||
@ -77,6 +118,7 @@ final class TableManagement {
|
||||
player.$table.id = table.id
|
||||
try await player.update(on: database)
|
||||
table.sendUpdateToAllPlayers()
|
||||
logPlayingPlayerCount()
|
||||
return table.tableInfo(forPlayer: player.name)
|
||||
}
|
||||
|
||||
@ -108,6 +150,7 @@ final class TableManagement {
|
||||
player.$table.id = nil
|
||||
guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else {
|
||||
tables[oldTable.id] = nil
|
||||
logTableCount()
|
||||
try await player.update(on: database)
|
||||
try await Table.query(on: database).filter(\.$id == oldTable.id).delete()
|
||||
return
|
||||
@ -116,6 +159,8 @@ final class TableManagement {
|
||||
tables[table.id] = table
|
||||
#warning("Update points for all players, add penalty if running game")
|
||||
table.sendUpdateToAllPlayers()
|
||||
logPlayingPlayerCount()
|
||||
logConnectedPlayerCount()
|
||||
try await player.update(on: database)
|
||||
}
|
||||
|
||||
@ -123,6 +168,7 @@ final class TableManagement {
|
||||
guard let table = currentTable(for: player) else {
|
||||
return false
|
||||
}
|
||||
defer { logConnectedPlayerCount() }
|
||||
return table.connect(player: player, using: socket)
|
||||
}
|
||||
|
||||
@ -130,6 +176,7 @@ final class TableManagement {
|
||||
guard let table = currentTable(for: player) else {
|
||||
return
|
||||
}
|
||||
defer { logConnectedPlayerCount() }
|
||||
table.disconnect(player: player)
|
||||
}
|
||||
|
||||
@ -199,5 +246,6 @@ final class TableManagement {
|
||||
|
||||
func disconnectAllSockets() {
|
||||
tables.values.forEach { $0.disconnectAllPlayers() }
|
||||
logConnectedPlayerCount()
|
||||
}
|
||||
}
|
||||
|
@ -34,3 +34,14 @@ protocol ManageableTable {
|
||||
|
||||
func disconnectAllPlayers()
|
||||
}
|
||||
|
||||
extension ManageableTable {
|
||||
|
||||
var numberOfConnectedPlayers: Int {
|
||||
allPlayers.count { $0.isConnected }
|
||||
}
|
||||
|
||||
var playerCount: Int {
|
||||
allPlayers.count
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,17 @@ public func configure(_ app: Application) throws {
|
||||
|
||||
let configPath = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||
.appendingPathComponent("config.json")
|
||||
let configuration = try Configuration(loadFromUrl: configPath)
|
||||
let configuration: Configuration
|
||||
|
||||
do {
|
||||
configuration = try Configuration(loadFromUrl: configPath)
|
||||
} catch {
|
||||
status.update(.initializationFailure)
|
||||
monitor.log("Failed to read configuration: \(error)")
|
||||
// Note: If configuration can't be loaded, then the server will run on the wrong port
|
||||
// and access to metrics is impossible, since no tokens are loaded
|
||||
return
|
||||
}
|
||||
configuration.monitoringTokens.map { $0.data(using: .utf8)! }.forEach(accessManager.add)
|
||||
|
||||
app.http.server.configuration.port = configuration.serverPort
|
||||
@ -55,7 +65,7 @@ public func configure(_ app: Application) throws {
|
||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||
|
||||
let db = app.databases.database(.sqlite, logger: .init(label: "Init"), on: app.databases.eventLoopGroup.next())!
|
||||
server = SQLiteDatabase(db: db, mail: configuration.mail)
|
||||
server = SQLiteDatabase(database: db, mail: configuration.mail)
|
||||
|
||||
// Gracefully shut down by closing potentially open socket
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
||||
|
Loading…
Reference in New Issue
Block a user