Skip to content

Commit

Permalink
New table for information about errors (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
mczachurski authored Oct 22, 2024
1 parent 7954f2b commit 9e09310
Show file tree
Hide file tree
Showing 49 changed files with 950 additions and 116 deletions.
11 changes: 1 addition & 10 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "d9f447ac036f89b821b53fe46d5486a56ebc71ec2c177c4470bd954d73ab0ab7",
"originHash" : "133d12efbdc66b04efed84bc874c1a9d5fa072f82a5e3b36fed3e5ee7ea77739",
"pins" : [
{
"identity" : "async-http-client",
Expand Down Expand Up @@ -46,15 +46,6 @@
"version" : "2.0.0"
}
},
{
"identity" : "extendedlogging",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Mikroservices/ExtendedLogging.git",
"state" : {
"revision" : "7cca1292f08696acb3fdaf061fdfa9877c71a867",
"version" : "2.0.7"
}
},
{
"identity" : "fluent",
"kind" : "remoteSourceControl",
Expand Down
4 changes: 0 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ let package = Package(

// 🔑 Google Recaptcha for securing anonymous endpoints.
.package(url: "https://github.com/Mikroservices/Recaptcha.git", from: "2.0.0"),

// 📘 Custom logger handlers.
.package(url: "https://github.com/Mikroservices/ExtendedLogging.git", from: "2.0.7"),

// 📒 Library provides mechanism for reading configuration files.
.package(url: "https://github.com/Mikroservices/ExtendedConfiguration.git", from: "1.0.0"),
Expand Down Expand Up @@ -105,7 +102,6 @@ let package = Package(
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "JWT", package: "jwt"),
.product(name: "Logging", package: "swift-log"),
.product(name: "ExtendedLogging", package: "ExtendedLogging"),
.product(name: "ExtendedError", package: "ExtendedError"),
.product(name: "ExtendedConfiguration", package: "ExtendedConfiguration"),
.product(name: "Recaptcha", package: "Recaptcha"),
Expand Down
6 changes: 3 additions & 3 deletions Sources/VernissageServer/Application+Configure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ extension Application {
try self.register(collection: RulesController())
try self.register(collection: UserAliasesController())
try self.register(collection: HealthController())
try self.register(collection: ErrorItemsController())
}

private func registerMiddlewares() {
Expand Down Expand Up @@ -178,9 +179,6 @@ extension Application {
self.settings.configuration.all().forEach { (key: String, value: Any) in
self.logger.info("Configuration: '\(key)', value: '\(value)'.")
}

self.logger.info("Sentry API DSN: \(Environment.get("SENTRY_DSN") ?? "<not set>")")
self.logger.info("Sentry WEB DSN: \(Environment.get("SENTRY_DSN_WEB") ?? "<not set>")")
}

private func configureDatabase(clearDatabase: Bool = false) throws {
Expand Down Expand Up @@ -305,6 +303,7 @@ extension Application {
self.migrations.add(TrendingUser.AddAmountField())
self.migrations.add(FeaturedStatus.ChangeUniqueIndex())
self.migrations.add(FeaturedUser.ChangeUniqueIndex())
self.migrations.add(ErrorItem.CreateErrorItems())

try await self.autoMigrate()
}
Expand Down Expand Up @@ -401,6 +400,7 @@ extension Application {
// Schedule different jobs.
self.queues.schedule(ClearAttachmentsJob()).hourly().at(15)
self.queues.schedule(TrendingJob()).hourly().at(30)
self.queues.schedule(ClearErrorItemsJob()).daily().at(.midnight)

// Run scheduled jobs in process.
try self.queues.startScheduledJobs()
Expand Down
221 changes: 221 additions & 0 deletions Sources/VernissageServer/Controllers/ErrorItemsController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
//
// https://mczachurski.dev
// Copyright © 2024 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//

import Vapor
import Fluent
import ActivityPubKit

extension ErrorItemsController: RouteCollection {

@_documentation(visibility: private)
static let uri: PathComponent = .constant("error-items")

func boot(routes: RoutesBuilder) throws {
let errorItemsGroup = routes
.grouped("api")
.grouped("v1")
.grouped(ErrorItemsController.uri)
.grouped(UserAuthenticator())
.grouped(UserPayload.guardMiddleware())

errorItemsGroup
.grouped(UserPayload.guardIsModeratorMiddleware())
.grouped(EventHandlerMiddleware(.errorList))
.get(use: list)

errorItemsGroup
.grouped(XsrfTokenValidatorMiddleware())
.grouped(EventHandlerMiddleware(.errorCreate))
.post(use: create)

errorItemsGroup
.grouped(UserPayload.guardIsModeratorMiddleware())
.grouped(XsrfTokenValidatorMiddleware())
.grouped(EventHandlerMiddleware(.errorDelete))
.delete(":id", use: delete)
}
}

/// Exposing list of errors.
///
/// Thanks to that controller we can save and rerturn all errors which has been
/// recorded in the system (client & server).
///
/// > Important: Base controller URL: `/api/v1/error-items`.
struct ErrorItemsController {

/// Exposing list of errors.
///
/// > Important: Endpoint URL: `/api/v1/error-items`.
///
/// **CURL request:**
///
/// ```bash
/// curl "https://example.com/api/v1/error-items" \
/// -X GET \
/// -H "Content-Type: application/json" \
/// -H "Authorization: Bearer [ACCESS_TOKEN]" \
/// ```
///
/// **Example response body:**
///
/// ```json
/// [{
/// "code": "CTcoTAfGp8",
/// "message": "Unexpected client error.",
/// "exception": "{\n \"message\": \"This cannot be null!\"\n}",
/// "source": "client"
/// }]
/// ```
///
/// - Parameters:
/// - request: The Vapor request to the endpoint.
///
/// - Returns: List of errors.
@Sendable
func list(request: Request) async throws -> PaginableResultDto<ErrorItemDto> {
let page: Int = request.query["page"] ?? 0
let size: Int = request.query["size"] ?? 10
let query: String? = request.query["query"] ?? nil

let errorItemsFromDatabaseQueryBuilder = ErrorItem.query(on: request.db)

if let query, query.isEmpty == false {
errorItemsFromDatabaseQueryBuilder
.group(.or) { group in
group
.filter(\.$code == query)
.filter(\.$message ~~ query)
.filter(\.$exception ~~ query)
}
}

let errorItemsFromDatabase = try await errorItemsFromDatabaseQueryBuilder
.sort(\.$createdAt, .descending)
.paginate(PageRequest(page: page, per: size))

let errorItemDtos = errorItemsFromDatabase.items.map { ErrorItemDto(from: $0) }

return PaginableResultDto(
data: errorItemDtos,
page: errorItemsFromDatabase.metadata.page,
size: errorItemsFromDatabase.metadata.per,
total: errorItemsFromDatabase.metadata.total
)
}

/// Create new error item.
///
/// The endpoint can be used for creating new error information.
///
/// > Important: Endpoint URL: `/api/v1/error-items`.
///
/// **CURL request:**
///
/// ```bash
/// curl "https://example.com/api/v1/error-items" \
/// -X POST \
/// -H "Content-Type: application/json" \
/// -H "Authorization: Bearer [ACCESS_TOKEN]" \
/// -d '{ ... }'
/// ```
///
/// **Example request body:**
///
/// ```json
/// {
/// "code": "CTcoTAfGp8",
/// "message": "Unexpected client error.",
/// "exception": "{\n \"message\": \"This cannot be null!\"\n}",
/// "source": "client"
/// }
/// ```
///
/// **Example response body:**
///
/// ```json
/// {
/// "id": "7428256478005299812",
/// "code": "CTcoTAfGp8",
/// "message": "Unexpected client error.",
/// "exception": "{\n \"message\": \"This cannot be null!\"\n}",
/// "source": "client",
/// "createdAt": "2024-10-21T15:48:57.455Z",
/// }
/// ```
///
/// - Parameters:
/// - request: The Vapor request to the endpoint.
///
/// - Returns: New added entity.
@Sendable
func create(request: Request) async throws -> Response {
let errorItemDto = try request.content.decode(ErrorItemDto.self)
try ErrorItemDto.validate(content: request)

let userAgent = request.headers[.userAgent].first


let id = request.application.services.snowflakeService.generate()
let errorItem = ErrorItem(id: id,
source: errorItemDto.source.translate(),
code: errorItemDto.code,
message: errorItemDto.message,
exception: errorItemDto.exception,
userAgent: userAgent,
clientVersion: errorItemDto.clientVersion,
serverVersion: Constants.version)

try await errorItem.save(on: request.db)
return try await createNewErrorItemResponse(on: request, errorItem: errorItem)
}

/// Delete error from the database.
///
/// The endpoint can be used for deleting existing error.
///
/// > Important: Endpoint URL: `/api/v1/error-items/:id`.
///
/// **CURL request:**
///
/// ```bash
/// curl "https://example.com/api/v1/error-items/7267938074834522113" \
/// -X DELETE \
/// -H "Content-Type: application/json" \
/// -H "Authorization: Bearer [ACCESS_TOKEN]"
/// ```
///
/// - Parameters:
/// - request: The Vapor request to the endpoint.
///
/// - Returns: Http status code.
@Sendable
func delete(request: Request) async throws -> HTTPStatus {
guard let errorItemIdString = request.parameters.get("id", as: String.self) else {
throw ErrorItemError.incorrectErrorItemId
}

guard let errorItemId = errorItemIdString.toId() else {
throw ErrorItemError.incorrectErrorItemId
}

guard let errorItem = try await ErrorItem.find(errorItemId, on: request.db) else {
throw EntityNotFoundError.errorItemNotFound
}

try await errorItem.delete(on: request.db)
return HTTPStatus.ok
}

private func createNewErrorItemResponse(on request: Request, errorItem: ErrorItem) async throws -> Response {
let errorItemDto = ErrorItemDto(from: errorItem)

var headers = HTTPHeaders()
headers.replaceOrAdd(name: .location, value: "/\(ErrorItemsController.uri)/\(errorItem.stringId() ?? "")")

return try await errorItemDto.encodeResponse(status: .created, headers: headers, for: request)
}
}
12 changes: 7 additions & 5 deletions Sources/VernissageServer/Controllers/HealthController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ struct HealthController {
_ = try await User.query(on: request.db).first()
return true
} catch {
request.logger.error("Database health check error: \(error.localizedDescription).")
await request.logger.store("Database health check error.", error, on: request.application)
return false
}
}
Expand All @@ -91,7 +91,7 @@ struct HealthController {
_ = try await request.application.redis.get(key: "health-check")
return true
} catch {
request.logger.error("Redis queue health check error: \(error.localizedDescription).")
await request.logger.store("Redis queue health check error.", error, on: request.application)
return false
}
}
Expand All @@ -102,7 +102,9 @@ struct HealthController {
_ = try await webPushService.check(on: request)
return true
} catch {
request.logger.error("WebPush service health check error: \(error.localizedDescription).")
await request.logger.store("WebPush service health check error.", error, on: request.application)
await request.logger.store("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse eget magna eu felis viverra luctus. Donec vehicula lectus urna, ac facilisis leo sodales nec. Fusce id blandit turpis. Ut vestibulum odio ut neque semper ornare. Ut at diam nisl. Curabitur mollis lacus elit, eu placerat neque aliquam in. Quisque non eros velit. Duis posuere dui eget interdum vestibulum. Sed laoreet semper rutrum.", error, on: request.application)

return false
}
}
Expand All @@ -118,10 +120,10 @@ struct HealthController {

return true
} catch let error as S3ErrorType {
request.logger.error("Storage health check error: \(error.message ?? "").")
await request.logger.store("Storage health check error.", error, on: request.application)
return false
} catch {
request.logger.error("Storage health check error: \(error.localizedDescription).")
await request.logger.store("Storage health check error.", error, on: request.application)
return false
}
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/VernissageServer/Controllers/RulesController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ struct RulesController {
/// **CURL request:**
///
/// ```bash
/// curl "https://example.com/api/v1/instance-blocked-domains" \
/// curl "https://example.com/api/v1/rules" \
/// -X POST \
/// -H "Content-Type: application/json" \
/// -H "Authorization: Bearer [ACCESS_TOKEN]" \
Expand Down Expand Up @@ -271,7 +271,7 @@ struct RulesController {
let ruleDto = RuleDto(from: rule)

var headers = HTTPHeaders()
headers.replaceOrAdd(name: .location, value: "/\(RulesController.uri)/@\(rule.stringId() ?? "")")
headers.replaceOrAdd(name: .location, value: "/\(RulesController.uri)/\(rule.stringId() ?? "")")

return try await ruleDto.encodeResponse(status: .created, headers: headers, for: request)
}
Expand Down
10 changes: 4 additions & 6 deletions Sources/VernissageServer/Controllers/SettingsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ struct SettingsController {
///
/// ```json
/// {
/// "webSentryDsn": "https://1b0056907f5jf6261eb05d3ebd451c33@o4506755493336736.ingest.sentry.io/4506853429433344"
/// "maximumNumberOfInvitations": 2,
/// "isOpenAIEnabled": false
/// }
/// ```
///
Expand All @@ -153,18 +154,15 @@ struct SettingsController {
if let publicSettingsFromCache: PublicSettingsDto = try? await request.cache.get(publicSettingsKey) {
return publicSettingsFromCache
}

let webSentryDsn = Environment.get("SENTRY_DSN_WEB") ?? ""


let settingsFromDatabase = try await Setting.query(on: request.db).all()
let settings = SettingsDto(basedOn: settingsFromDatabase)
let webPushVapidPublicKey = settings.isWebPushEnabled ? settings.webPushVapidPublicKey : nil

let appplicationSettings = request.application.settings.cached
let s3Address = appplicationSettings?.s3Address

let publicSettingsDto = PublicSettingsDto(webSentryDsn: webSentryDsn,
maximumNumberOfInvitations: settings.maximumNumberOfInvitations,
let publicSettingsDto = PublicSettingsDto(maximumNumberOfInvitations: settings.maximumNumberOfInvitations,
isOpenAIEnabled: settings.isOpenAIEnabled,
webPushVapidPublicKey: webPushVapidPublicKey,
s3Address: s3Address,
Expand Down
Loading

0 comments on commit 9e09310

Please sign in to comment.