add Film observability with trace id and metrics (#44)

* add Film observability with trace id and metrics

* test: provide meter registry in FilmServiceTest
This commit was merged in pull request #44.
This commit is contained in:
skettiks
2026-05-19 16:13:10 +03:00
committed by GitHub
parent 051b4ca80b
commit e956c058ac
5 changed files with 219 additions and 31 deletions
@@ -14,6 +14,10 @@ import com.project.movienight.config.FilmServiceProperties
import com.project.movienight.domain.exception.BlockedValueException
import com.project.movienight.domain.exception.EntityNotFoundException
import com.project.movienight.domain.model.Film
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.util.UUID
@@ -22,48 +26,117 @@ class FilmService(
private val filmRepository: FilmRepositoryPort,
private val idGenerator: IdGenerator,
private val filmConfig: FilmServiceProperties,
private val meterRegistry: MeterRegistry,
) : CreateFilmUseCase,
EditFilmUseCase,
DeleteFilmUseCase,
GetFilmByIdUseCase,
GetAllFilmsUseCase,
SearchFilmByTitleUseCase {
override fun create(command: CreateFilmCommand): Film {
if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "title")
}
if (filmConfig.isBlocked(command.description)) {
throw BlockedValueException(target = "Film", field = "description")
}
private val log = LoggerFactory.getLogger(javaClass)
val film =
Film(
id = idGenerator.generateId(),
title = command.title,
description = command.description,
override fun create(command: CreateFilmCommand): Film {
val sample = Timer.start(meterRegistry)
try {
log.debug(
"Create film request received: title='{}', descriptionLength={}",
command.title,
command.description.length,
)
return filmRepository.save(film)
if (filmConfig.isBlocked(command.title)) {
log.debug("Create film blocked by title policy: title='{}'", command.title)
filmBlockedCounter.increment()
throw BlockedValueException(target = "Film", field = "title")
}
if (filmConfig.isBlocked(command.description)) {
log.debug("Create film blocked by description policy")
filmBlockedCounter.increment()
throw BlockedValueException(target = "Film", field = "description")
}
val film =
Film(
id = idGenerator.generateId(),
title = command.title,
description = command.description,
)
val saved = filmRepository.save(film)
filmCreatedCounter.increment()
log.info("Film created: id='{}', title='{}'", saved.id, saved.title)
return saved
} finally {
sample.stop(createFilmTimer)
}
}
override fun edit(
id: UUID,
command: EditFilmCommand,
): Film {
if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "title")
}
if (filmConfig.isBlocked(command.description)) {
throw BlockedValueException(target = "Film", field = "description")
}
val sample = Timer.start(meterRegistry)
var film = filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString())
film = film.copy(title = command.title, description = command.description)
return filmRepository.save(film)
try {
log.debug("Edit film with id: {}", id)
if (filmConfig.isBlocked(command.title)) {
log.debug("Edit film blocked by title policy: title='{}'", command.title)
filmBlockedCounter.increment()
throw BlockedValueException(target = "Film", field = "title")
}
if (filmConfig.isBlocked(command.description)) {
log.debug("Edit film blocked by description policy")
filmBlockedCounter.increment()
throw BlockedValueException(target = "Film", field = "description")
}
val film = filmRepository.findById(id)
if (film == null) {
log.debug("Film not found for edit: id='{}'", id)
throw EntityNotFoundException(entity = "Film", id = id.toString())
}
val updatedFilm =
film.copy(
title = command.title,
description = command.description,
)
val saved = filmRepository.save(updatedFilm)
filmEditedCounter.increment()
log.info("Film edited: id='{}'", saved.id)
return saved
} finally {
sample.stop(editFilmTimer)
}
}
override fun delete(id: UUID) {
filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString())
filmRepository.deleteById(id)
val sample = Timer.start(meterRegistry)
try {
log.debug("Delete film with id: {}", id)
val film = filmRepository.findById(id)
if (film == null) {
log.debug("Film not found for delete: id='{}'", id)
throw EntityNotFoundException(entity = "Film", id = id.toString())
}
filmRepository.deleteById(id)
filmDeletedCounter.increment()
log.info("Film deleted: id='{}'", id)
} finally {
sample.stop(deleteFilmTimer)
}
}
override fun getById(id: UUID): Film =
@@ -72,4 +145,46 @@ class FilmService(
override fun getAll(): List<Film> = filmRepository.findAll()
override fun searchByTitle(title: String): Film? = filmRepository.findByTitle(title)
private val filmCreatedCounter =
Counter
.builder("film_created_total")
.description("Total number of created films")
.register(meterRegistry)
private val filmEditedCounter =
Counter
.builder("film_edited_total")
.description("Total number of successfully edited films")
.register(meterRegistry)
private val filmDeletedCounter =
Counter
.builder("film_deleted_total")
.description("Total number of successfully deleted films")
.register(meterRegistry)
private val filmBlockedCounter =
Counter
.builder("films.blocked")
.description("Total blocked film operations")
.register(meterRegistry)
private val createFilmTimer =
Timer
.builder("films.create.duration")
.description("Film creation duration")
.register(meterRegistry)
private val editFilmTimer =
Timer
.builder("films.edit.duration")
.description("Film edit duration")
.register(meterRegistry)
private val deleteFilmTimer =
Timer
.builder("films.delete.duration")
.description("Film deletion duration")
.register(meterRegistry)
}