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 Fluent
|
||||||
import Vapor
|
import Vapor
|
||||||
import SwiftSMTP
|
import SwiftSMTP
|
||||||
|
import Clairvoyant
|
||||||
|
|
||||||
typealias PasswordHash = String
|
typealias PasswordHash = String
|
||||||
typealias SessionToken = String
|
typealias SessionToken = String
|
||||||
|
|
||||||
private struct MailConfig {
|
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 smtp: SMTP
|
||||||
|
|
||||||
let mailSender: Mail.User
|
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 {
|
final class SQLiteDatabase {
|
||||||
|
|
||||||
/// A mapping between player name and generated access tokens for a session
|
/// A mapping between player name and generated access tokens for a session
|
||||||
@ -27,19 +49,16 @@ final class SQLiteDatabase {
|
|||||||
|
|
||||||
private let mail: MailConfig?
|
private let mail: MailConfig?
|
||||||
|
|
||||||
init(db: Database, mail: Configuration.EMail?) {
|
private let registeredPlayerCountMetric: Metric<Int>
|
||||||
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)
|
|
||||||
|
|
||||||
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 {
|
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()
|
let token = SessionToken.newToken()
|
||||||
self.sessionTokenForPlayer[name] = token
|
self.sessionTokenForPlayer[name] = token
|
||||||
self.playerNameForToken[token] = name
|
self.playerNameForToken[token] = name
|
||||||
|
|
||||||
|
await updateRegisteredPlayerCount(from: database)
|
||||||
return token
|
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.
|
Send a password reset email.
|
||||||
|
|
||||||
@ -96,8 +126,8 @@ final class SQLiteDatabase {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let recipient = Mail.User(name: name, email: email)
|
let recipient = Mail.User(name: name, email: email)
|
||||||
let mailConfiguration = mailData.mailConfiguration
|
//let mailConfiguration = mailData.mailConfiguration
|
||||||
let url = "\(mailConfiguration.serverDomain)/recovery.html?token=\(token)"
|
let url = "\(mailData.serverDomain)/recovery.html?token=\(token)"
|
||||||
let mail = Mail(
|
let mail = Mail(
|
||||||
from: mailData.mailSender,
|
from: mailData.mailSender,
|
||||||
to: [recipient],
|
to: [recipient],
|
||||||
@ -106,12 +136,12 @@ final class SQLiteDatabase {
|
|||||||
"""
|
"""
|
||||||
Hello \(name),
|
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:
|
To choose a new password, click the following link:
|
||||||
|
|
||||||
\(url)
|
\(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,
|
Regards,
|
||||||
|
|
||||||
@ -169,6 +199,7 @@ final class SQLiteDatabase {
|
|||||||
let user = try await user(named: name, in: database)
|
let user = try await user(named: name, in: database)
|
||||||
try await tables.leaveTable(player: user, in: database)
|
try await tables.leaveTable(player: user, in: database)
|
||||||
try await User.query(on: database).filter(\.$name == name).delete()
|
try await User.query(on: database).filter(\.$name == name).delete()
|
||||||
|
await updateRegisteredPlayerCount(from: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValid(sessionToken token: SessionToken) -> Bool {
|
func isValid(sessionToken token: SessionToken) -> Bool {
|
||||||
|
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import WebSocketKit
|
import WebSocketKit
|
||||||
import Vapor
|
import Vapor
|
||||||
import Fluent
|
import Fluent
|
||||||
|
import Clairvoyant
|
||||||
|
|
||||||
let maximumPlayersPerTable = 4
|
let maximumPlayersPerTable = 4
|
||||||
|
|
||||||
@ -12,25 +13,63 @@ final class TableManagement {
|
|||||||
|
|
||||||
/// All tables indexed by their id
|
/// All tables indexed by their id
|
||||||
private var tables = [UUID : ManageableTable]()
|
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
|
Load the tables from a file in the storage folder
|
||||||
- Throws: Errors when the file could not be read
|
- Throws: Errors when the file could not be read
|
||||||
*/
|
*/
|
||||||
init(db: Database) {
|
init(database: Database) {
|
||||||
Table.query(on: db).with(\.$players).all().whenSuccess { loadedTables in
|
self.tableCountMetric = .init("schafkopf.tables")
|
||||||
for table in loadedTables {
|
self.playingPlayerCountMetric = .init("schafkopf.playing")
|
||||||
guard !table.players.isEmpty else {
|
self.connectedPlayerCountMetric = .init("schafkopf.connected")
|
||||||
_ = table.delete(on: db)
|
|
||||||
continue
|
Task {
|
||||||
}
|
do {
|
||||||
let id = table.id!
|
try await loadTables(from: database)
|
||||||
self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players)
|
} 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.
|
Create a new table with optional players.
|
||||||
- Parameter name: The name of the table
|
- Parameter name: The name of the table
|
||||||
@ -45,6 +84,8 @@ final class TableManagement {
|
|||||||
try await player.update(on: database)
|
try await player.update(on: database)
|
||||||
let waitingTable = WaitingTable(newTable: table, user: player)
|
let waitingTable = WaitingTable(newTable: table, user: player)
|
||||||
self.tables[waitingTable.id] = waitingTable
|
self.tables[waitingTable.id] = waitingTable
|
||||||
|
logTableCount()
|
||||||
|
logPlayingPlayerCount()
|
||||||
return waitingTable.tableInfo(forPlayer: player.name)
|
return waitingTable.tableInfo(forPlayer: player.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +118,7 @@ final class TableManagement {
|
|||||||
player.$table.id = table.id
|
player.$table.id = table.id
|
||||||
try await player.update(on: database)
|
try await player.update(on: database)
|
||||||
table.sendUpdateToAllPlayers()
|
table.sendUpdateToAllPlayers()
|
||||||
|
logPlayingPlayerCount()
|
||||||
return table.tableInfo(forPlayer: player.name)
|
return table.tableInfo(forPlayer: player.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +150,7 @@ final class TableManagement {
|
|||||||
player.$table.id = nil
|
player.$table.id = nil
|
||||||
guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else {
|
guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else {
|
||||||
tables[oldTable.id] = nil
|
tables[oldTable.id] = nil
|
||||||
|
logTableCount()
|
||||||
try await player.update(on: database)
|
try await player.update(on: database)
|
||||||
try await Table.query(on: database).filter(\.$id == oldTable.id).delete()
|
try await Table.query(on: database).filter(\.$id == oldTable.id).delete()
|
||||||
return
|
return
|
||||||
@ -116,6 +159,8 @@ final class TableManagement {
|
|||||||
tables[table.id] = table
|
tables[table.id] = table
|
||||||
#warning("Update points for all players, add penalty if running game")
|
#warning("Update points for all players, add penalty if running game")
|
||||||
table.sendUpdateToAllPlayers()
|
table.sendUpdateToAllPlayers()
|
||||||
|
logPlayingPlayerCount()
|
||||||
|
logConnectedPlayerCount()
|
||||||
try await player.update(on: database)
|
try await player.update(on: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +168,7 @@ final class TableManagement {
|
|||||||
guard let table = currentTable(for: player) else {
|
guard let table = currentTable(for: player) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
defer { logConnectedPlayerCount() }
|
||||||
return table.connect(player: player, using: socket)
|
return table.connect(player: player, using: socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,6 +176,7 @@ final class TableManagement {
|
|||||||
guard let table = currentTable(for: player) else {
|
guard let table = currentTable(for: player) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer { logConnectedPlayerCount() }
|
||||||
table.disconnect(player: player)
|
table.disconnect(player: player)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,5 +246,6 @@ final class TableManagement {
|
|||||||
|
|
||||||
func disconnectAllSockets() {
|
func disconnectAllSockets() {
|
||||||
tables.values.forEach { $0.disconnectAllPlayers() }
|
tables.values.forEach { $0.disconnectAllPlayers() }
|
||||||
|
logConnectedPlayerCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,3 +34,14 @@ protocol ManageableTable {
|
|||||||
|
|
||||||
func disconnectAllPlayers()
|
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)
|
let configPath = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||||
.appendingPathComponent("config.json")
|
.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)
|
configuration.monitoringTokens.map { $0.data(using: .utf8)! }.forEach(accessManager.add)
|
||||||
|
|
||||||
app.http.server.configuration.port = configuration.serverPort
|
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))
|
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
|
|
||||||
let db = app.databases.database(.sqlite, logger: .init(label: "Init"), on: app.databases.eventLoopGroup.next())!
|
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
|
// Gracefully shut down by closing potentially open socket
|
||||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
Loading…
Reference in New Issue
Block a user