Skip to content

Commit

Permalink
Add error reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
Isti01 committed Oct 23, 2024
1 parent d505850 commit 2c5d7d1
Show file tree
Hide file tree
Showing 15 changed files with 488 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package hu.bme.sch.cmsch.component.errorlog

import hu.bme.sch.cmsch.model.RoleType
import hu.bme.sch.cmsch.util.getUserOrNull
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.util.Optional

@RestController()
@RequestMapping("/api")
class ErrorLogApiController(
private val errorLogComponent: Optional<ErrorLogComponent>,
private val errorLogService: Optional<ErrorLogService>
) {

data class ErrorReportDto(val message: String?, val stack: String?, val userAgent: String?, val href: String?)

@PostMapping("/error/submit")
@Transactional(isolation = Isolation.SERIALIZABLE)
fun submitError(auth: Authentication?, @RequestBody error: ErrorReportDto): ResponseEntity<Any> {
if (error.message == null && error.stack == null && error.userAgent == null && error.href == null)
return ResponseEntity.badRequest().build()

val role = auth?.getUserOrNull()?.role ?: RoleType.GUEST
if (!errorLogComponent.map { it.minRole.isAvailableForRole(role) }.orElse(true))
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()

errorLogService.ifPresent {
it.submit(
message = error.message ?: "",
stack = error.stack ?: "",
userAgent = error.userAgent ?: "",
href = error.href ?: "",
role = role
)
}

return ResponseEntity.ok().build()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package hu.bme.sch.cmsch.component.errorlog

import hu.bme.sch.cmsch.component.*
import hu.bme.sch.cmsch.component.app.ComponentSettingService
import hu.bme.sch.cmsch.model.RoleType
import hu.bme.sch.cmsch.service.ControlPermissions
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.core.env.Environment
import org.springframework.stereotype.Service

@Service
@ConditionalOnProperty(
prefix = "hu.bme.sch.cmsch.component.load",
name = ["errorlog"],
havingValue = "true",
matchIfMissing = false
)
class ErrorLogComponent(
componentSettingService: ComponentSettingService,
env: Environment
) : ComponentBase(
"errorlog",
"/errorlog",
"Kliens hibaüzenetek",
ControlPermissions.PERMISSION_CONTROL_ERROR_LOG,
listOf(ErrorLogEntity::class),
componentSettingService, env
) {

final override val allSettings by lazy {
listOf(errorLogGroup, menuDisplayName, minRole, receiveReports)
}

val errorLogGroup = SettingProxy(componentSettingService, component,
"errorLogGroup", "", type = SettingType.COMPONENT_GROUP, persist = false,
fieldName = "Kliens hibák",
description = ""
)

final override val menuDisplayName = SettingProxy(componentSettingService, component,
"menuDisplayName", "Kliens hibák", serverSideOnly = true,
fieldName = "Menü neve", description = "Ez lesz a neve a menünek"
)

final override val minRole = MinRoleSettingProxy(componentSettingService, component,
"minRole", MinRoleSettingProxy.ALL_ROLES, minRoleToEdit = RoleType.SUPERUSER,
fieldName = "Jogosultságok", description = "Melyik roleok küldhetnek hibajelentéseket"
)

val receiveReports = SettingProxy(componentSettingService, component, "receiveReports", "true",
type = SettingType.BOOLEAN, fieldName = "Kliens hibajelentések fogadása", serverSideOnly = true
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package hu.bme.sch.cmsch.component.errorlog

import hu.bme.sch.cmsch.component.ComponentApiBase
import hu.bme.sch.cmsch.component.app.MenuService
import hu.bme.sch.cmsch.service.AdminMenuService
import hu.bme.sch.cmsch.service.AuditLogService
import hu.bme.sch.cmsch.service.ControlPermissions.PERMISSION_CONTROL_ERROR_LOG
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("/admin/control/component/errorlog")
@ConditionalOnBean(ErrorLogComponent::class)
class ErrorLogComponentController(
adminMenuService: AdminMenuService,
component: ErrorLogComponent,
menuService: MenuService,
auditLogService: AuditLogService
) : ComponentApiBase(
adminMenuService,
ErrorLogComponent::class.java,
component,
PERMISSION_CONTROL_ERROR_LOG,
"Kliens hibaüzenetek",
"Kliens hibaüzenetek testreszabása",
menuService = menuService,
auditLogService = auditLogService
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package hu.bme.sch.cmsch.component.errorlog

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.context.annotation.Configuration

@Configuration
@ConditionalOnBean(ErrorLogComponent::class)
@EntityScan(basePackageClasses = [ErrorLogComponent::class])
class ErrorLogComponentEntityConfiguration
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package hu.bme.sch.cmsch.component.errorlog

import com.fasterxml.jackson.databind.ObjectMapper
import hu.bme.sch.cmsch.controller.admin.OneDeepEntityPage
import hu.bme.sch.cmsch.controller.admin.calculateSearchSettings
import hu.bme.sch.cmsch.service.AdminMenuService
import hu.bme.sch.cmsch.service.AuditLogService
import hu.bme.sch.cmsch.service.ImplicitPermissions
import hu.bme.sch.cmsch.service.ImportService
import hu.bme.sch.cmsch.service.StaffPermissions
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.core.env.Environment
import org.springframework.stereotype.Controller
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("/admin/control/errorlog")
@ConditionalOnBean(ErrorLogComponent::class)
class ErrorLogController(
repo: ErrorLogRepository,
importService: ImportService,
adminMenuService: AdminMenuService,
component: ErrorLogComponent,
auditLog: AuditLogService,
objectMapper: ObjectMapper,
transactionManager: PlatformTransactionManager,
env: Environment
) : OneDeepEntityPage<ErrorLogEntity>(
"errorlog",
ErrorLogEntity::class, ::ErrorLogEntity,
"Hibaüzenet", "Hibaüzenetek",
"A jelentett hibaüzenetek megtekintése",

transactionManager,
repo,
importService,
adminMenuService,
component,
auditLog,
objectMapper,
env,

showPermission = StaffPermissions.PERMISSION_SHOW_ERROR_LOG,
createPermission = ImplicitPermissions.PERMISSION_NOBODY,
editPermission = ImplicitPermissions.PERMISSION_NOBODY,
deletePermission = StaffPermissions.PERMISSION_DELETE_ERROR_LOG,

createEnabled = false,
editEnabled = false,
deleteEnabled = true,
importEnabled = false,
exportEnabled = true,

adminMenuIcon = "error",
adminMenuPriority = 1,
searchSettings = calculateSearchSettings<ErrorLogEntity>(true)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package hu.bme.sch.cmsch.component.errorlog

import com.fasterxml.jackson.annotation.JsonView
import hu.bme.sch.cmsch.admin.*
import hu.bme.sch.cmsch.component.EntityConfig
import hu.bme.sch.cmsch.dto.Edit
import hu.bme.sch.cmsch.dto.FullDetails
import hu.bme.sch.cmsch.dto.Preview
import hu.bme.sch.cmsch.model.ManagedEntity
import hu.bme.sch.cmsch.model.RoleType
import hu.bme.sch.cmsch.service.StaffPermissions
import jakarta.persistence.*
import org.hibernate.Hibernate
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.core.env.Environment


@Entity
@Table(
name = "errorLog",
uniqueConstraints = [UniqueConstraint(columnNames = ["message", "stack", "userAgent", "href", "role"])]
)
@ConditionalOnBean(ErrorLogComponent::class)
data class ErrorLogEntity(
@Id
@GeneratedValue
@field:JsonView(value = [Edit::class])
@Column(nullable = false)
@property:GenerateInput(type = INPUT_TYPE_HIDDEN, visible = true, ignore = true)
@property:GenerateOverview(renderer = OVERVIEW_TYPE_ID, columnName = "ID", order = -1)
override var id: Int = 0,

@field:JsonView(value = [Edit::class, Preview::class, FullDetails::class])
@Column(nullable = false, length = 2048)
@property:GenerateInput(maxLength = 512, type = INPUT_TYPE_BLOCK_TEXT, order = 1, label = "Hiba")
@property:GenerateOverview(columnName = "Hiba", order = 1, useForSearch = true)
@property:ImportFormat
var message: String = "",

@field:JsonView(value = [Edit::class, Preview::class, FullDetails::class])
@Column(nullable = false, length = 25000)
@property:GenerateInput(maxLength = 2048, type = INPUT_TYPE_BLOCK_TEXT, order = 2, label = "Stacktrace")
@property:GenerateOverview(visible = false, columnName = "Stacktrace", order = 2, useForSearch = true)
@property:ImportFormat
var stack: String = "",

@field:JsonView(value = [Edit::class, Preview::class, FullDetails::class])
@Column(nullable = false, length = 1024)
@property:GenerateInput(maxLength = 512, type = INPUT_TYPE_BLOCK_TEXT, order = 3, label = "User Agent")
@property:GenerateOverview(visible = false, columnName = "User Agent", order = 3, useForSearch = true)
@property:ImportFormat
var userAgent: String = "",

@field:JsonView(value = [Edit::class, Preview::class, FullDetails::class])
@Column(nullable = false, length = 2048)
@property:GenerateInput(maxLength = 512, order = 4, label = "href")
@property:GenerateOverview(columnName = "href", order = 4, useForSearch = true)
@property:ImportFormat
var href: String = "",

@field:JsonView(value = [Edit::class, FullDetails::class])
@Enumerated(EnumType.STRING)
@JdbcTypeCode(SqlTypes.VARCHAR)
@property:GenerateInput(
type = INPUT_TYPE_BLOCK_SELECT, order = 5, label = "Jogkör",
source = ["GUEST", "BASIC", "ATTENDEE", "PRIVILEGED", "STAFF", "ADMIN", "SUPERUSER"],
minimumRole = RoleType.ADMIN, note = "BASIC = belépett, STAFF = rendező, ADMIN = minden jog"
)
@property:GenerateOverview(visible = true, columnName = "Jelentő jogköre", order = 5)
@property:ImportFormat
var role: RoleType = RoleType.GUEST,

@field:JsonView(value = [Edit::class, Preview::class, FullDetails::class])
@Column(nullable = false)
@property:GenerateInput(type = INPUT_TYPE_NUMBER, order = 6, label = "Ennyiszer jelentve")
@property:GenerateOverview(
visible = true,
columnName = "Ennyiszer jelentve",
order = 6,
renderer = OVERVIEW_TYPE_NUMBER
)
@property:ImportFormat
var count: Long = 1,

@field:JsonView(value = [Edit::class, Preview::class, FullDetails::class])
@Column(nullable = false)
@property:GenerateInput(type = INPUT_TYPE_DATE, order = 7, label = "Utoljára jelentve")
@property:GenerateOverview(
visible = true,
columnName = "Utoljára jelentve",
order = 7,
renderer = OVERVIEW_TYPE_DATE
)
@property:ImportFormat
var lastReportedAt: Long = 0,
) : ManagedEntity {

override fun getEntityConfig(env: Environment) = EntityConfig(
name = "ErrorLogEntity",
view = "control/errorlog",
showPermission = StaffPermissions.PERMISSION_SHOW_ERROR_LOG
)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
other as ErrorLogEntity

return id != 0 && id == other.id
}

override fun hashCode(): Int = javaClass.hashCode()

@Override
override fun toString(): String {
return this::class.simpleName + "(id = $id )"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package hu.bme.sch.cmsch.component.errorlog

import hu.bme.sch.cmsch.model.RoleType
import hu.bme.sch.cmsch.repository.EntityPageDataSource
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
import java.util.Optional

@Repository
@ConditionalOnBean(ErrorLogComponent::class)
interface ErrorLogRepository : CrudRepository<ErrorLogEntity, Int>, EntityPageDataSource<ErrorLogEntity, Int> {

@Query("select e from ErrorLogEntity e order by e.lastReportedAt desc")
override fun findAll(): List<ErrorLogEntity>

fun findByMessageAndStackAndUserAgentAndHrefAndRole(
message: String,
stack: String,
userAgent: String,
href: String,
role: RoleType
): Optional<ErrorLogEntity>

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package hu.bme.sch.cmsch.component.errorlog

import hu.bme.sch.cmsch.model.RoleType
import hu.bme.sch.cmsch.service.TimeService
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Isolation
import org.springframework.transaction.annotation.Transactional
import kotlin.jvm.optionals.getOrNull

@Service
@ConditionalOnBean(ErrorLogComponent::class)
class ErrorLogService(
private val errorLogComponent: ErrorLogComponent,
private val errorLogRepository: ErrorLogRepository,
private val clock: TimeService
) {

@Transactional(isolation = Isolation.SERIALIZABLE)
fun submit(message: String, stack: String, userAgent: String, href: String, role: RoleType) {
if (!errorLogComponent.receiveReports.isValueTrue()) return

val existingLog = errorLogRepository
.findByMessageAndStackAndUserAgentAndHrefAndRole(message, stack, userAgent, href, role).getOrNull()

val logToSave = existingLog ?: ErrorLogEntity(
message = message,
stack = stack,
userAgent = userAgent,
href = href,
role = role,
)
logToSave.apply {
count = count + 1
lastReportedAt = clock.getTimeInSeconds()
}

errorLogRepository.save(logToSave)
}

}
Loading

0 comments on commit 2c5d7d1

Please sign in to comment.