Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple metrics requests #239

Open
wants to merge 29 commits into
base: backend
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a701399
[+] add Mikrometer dependencies
kiselev-danil Sep 23, 2023
25d8601
[+] add empty micrometer module
kiselev-danil Sep 23, 2023
d90eedc
Merge branch 'backend' into backendApp/feature/logs_and_metrics
kiselev-danil Sep 28, 2023
e295cb1
[+] add micrometer metrics
kiselev-danil Oct 1, 2023
45575a4
Merge branch 'backend' into backendApp/feature/logs_and_metrics
kiselev-danil Oct 14, 2023
19c31bd
[+] routing for metrics
kiselev-danil Oct 18, 2023
672dc8a
[+] add metrics routing file
kiselev-danil Oct 18, 2023
0a293fe
[+] add prometheus config
kiselev-danil Oct 24, 2023
c2a36c0
[+] add prometheus docker container
kiselev-danil Oct 24, 2023
743512a
[~] changed app container url
kiselev-danil Oct 30, 2023
80b911c
[~] change target address
kiselev-danil Oct 31, 2023
8e0975f
[~] specify network mode for prometheus container
kiselev-danil Oct 31, 2023
0c9c1c8
Merge branch 'backend' into backendApp/feature/logs_and_metrics
kiselev-danil Dec 22, 2023
45921b0
Merge branch 'backend' into backendApp/feature/logs_and_metrics
kiselev-danil Feb 17, 2024
6608644
[~] move metrics routes into specified packaged
kiselev-danil Feb 27, 2024
7338844
[+] add default time zone
kiselev-danil Feb 28, 2024
48d3064
[+] add metrics for office occupation calc
kiselev-danil Feb 28, 2024
9e4ce39
[~] rename metrics route file
kiselev-danil Feb 28, 2024
6e483ed
[+] add notifications logging
zavyalov-daniil Feb 29, 2024
212fdbd
[+] add DI module for metrics
kiselev-danil Mar 12, 2024
8d31b66
[~] move metrics route file into routes package
kiselev-danil Mar 12, 2024
c77ee62
[-] remove micrometer and prometheus
kiselev-danil Mar 12, 2024
ab06cd0
[~] fix exception description
kiselev-danil Mar 12, 2024
0e2ff8c
[+] add swagger for metrics
kiselev-danil Mar 12, 2024
e41789e
[~] change cicle type
kiselev-danil Mar 12, 2024
15b28aa
Merge branch 'backend' into backendApp/feature/logs_and_metrics
kiselev-danil Mar 12, 2024
bd6bcc1
[~] fix return value and number of days calculation
kiselev-danil Mar 13, 2024
fea1f36
[~] change example dates to be more actual
kiselev-danil Mar 13, 2024
6ca14a2
[~] fix cheirs-hours
kiselev-danil Mar 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions effectiveOfficeBackend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ val postgresql_driver_version: String by project
val snakeyaml_version: String by project
val liquibase_version: String by project
val mockito_version: String by project
val prometheus_version: String by project

plugins {
kotlin("jvm") version "1.8.22"
Expand Down Expand Up @@ -67,6 +68,10 @@ dependencies {
implementation("com.google.oauth-client:google-oauth-client-jetty:1.34.1")
implementation("com.google.firebase:firebase-admin:8.2.0")

implementation("io.ktor:ktor-server-metrics-micrometer:$ktor_version")
implementation("io.micrometer:micrometer-registry-prometheus:$prometheus_version")
implementation("io.ktor:ktor-server-metrics-micrometer-jvm:2.3.2")

liquibaseRuntime("org.liquibase:liquibase-core:$liquibase_version")
liquibaseRuntime("org.postgresql:postgresql:$postgresql_driver_version")
liquibaseRuntime("org.yaml:snakeyaml:$snakeyaml_version")
Expand Down
9 changes: 9 additions & 0 deletions effectiveOfficeBackend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ services:
timeout: 5s
retries: 10

prometheus:
container_name: prometheusForKtor
image: prom/prometheus:latest
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
network_mode: host


volumes:
pgdata: {}
3 changes: 2 additions & 1 deletion effectiveOfficeBackend/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ postgresql_driver_version=42.5.4
ktorm_version=3.6.0
liquibase_version=4.20.0
snakeyaml_version=2.0
mockito_version=4.0.0
mockito_version=4.0.0
prometheus_version=1.11.4
9 changes: 9 additions & 0 deletions effectiveOfficeBackend/prometheus/prometheus.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global:
scrape_interval: 15s

scrape_configs:
- job_name: 'ktor_micrometer'
metrics_path: '/metrics'
scrape_interval: 5s
static_configs:
- targets: ['0.0.0.0:8080']
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ fun Application.module() {
configureValidation()
configureExceptionHandling()
configureSwagger()
configureMicrometer()
install(CustomAuthorizationPlugin)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ object BookingConstants {
val WORKSPACE_CALENDAR: String = System.getenv("WORKSPACE_CALENDAR")
?: config.propertyOrNull("calendar.workspaceCalendar")?.getString()
?: throw Exception("Environment and config file does not contain workspace Google calendar id")
val DEFAULT_TIME_ZONE: String = config.propertyOrNull("calendar.timeZone")?.getString()
?: throw Exception("Config file does not contain default timeZone value")

const val UNTIL_FORMAT = "yyyyMMdd"
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package office.effective.common.di

import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import office.effective.common.utils.DatabaseTransactionManager
import office.effective.common.utils.impl.DatabaseTransactionManagerImpl
import office.effective.common.utils.UuidValidator
Expand All @@ -25,4 +27,5 @@ val commonDiModule = module(createdAtStart = true) {
}
single<DatabaseTransactionManager> { DatabaseTransactionManagerImpl(get()) }
single { UuidValidator() }
single { PrometheusMeterRegistry(PrometheusConfig.DEFAULT) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package office.effective.features.metrics.service

import office.effective.common.constants.BookingConstants
import office.effective.config
import office.effective.features.booking.service.BookingService
import office.effective.features.workspace.repository.WorkspaceRepository
import office.effective.features.workspace.service.WorkspaceService
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit

class MetricsService(
val workspaceService: WorkspaceService,
val workspaceRepository: WorkspaceRepository,
val bookingService: BookingService,
constants: BookingConstants
) {
private val defaultTimeZone = constants.DEFAULT_TIME_ZONE

/**
* regular
* meeting
*/

/**
* Какой процент переговорок(и рабочих мест) был свободен весь указанный промежуток времени.
* На коротких промежутках времени подходит как датчик занятости офиса в на момент середины отрезка
* */
fun calculateOfficeOccupancy(startTime: Instant, endTime: Instant): Map<String, Double> {
val res: MutableMap<String, Double> = mutableMapOf();

val freeRegularWorkspaces = workspaceService.findAllFreeByPeriod("regular", startTime, endTime).count()
val totalRegularWorkspaces = workspaceRepository.findAllByTag("regular").count()
val partOfFreeRegular: Double = (freeRegularWorkspaces.toDouble()) / (totalRegularWorkspaces.toDouble())
res["regular"] = partOfFreeRegular
val freeMeetingWorkspaces = workspaceService.findAllFreeByPeriod("meeting", startTime, endTime).count()
val totalMeetingWorkspaces = workspaceRepository.findAllByTag("meeting").count()
val partOfFreeMeeting: Double = (freeMeetingWorkspaces.toDouble()) / (totalMeetingWorkspaces.toDouble())
res["meeting"] = partOfFreeMeeting
return res;
}

//todo стул-часы, какая доля место-часов занята за промежуток времени. Учитывать, что люди в офисе не могут быть 24 часа

/**
* Получить: промежуток внутри дня, когда офис мог быть использован.
* Получить: анализируемый промежуток времени.
* Вычислить: количество дней, которые есть в промежутке времени, количество часов суммарное
* Вычислить: Для каждой переговорки: получить за этот промежуток времени, для каждого из дней, для промежутка рабочего времени: список броней
* Вычислить: Для каждой переговорки: получить за этот промежуток времени, для каждого из дней, для промежутка рабочего времени: получить суммарное время списка броней, получить время простоя
* Вычислить: добавить к суммарному времени простоя за промежуток времени
* */
fun calcPlaceHours(startDay: Int, endDay: Int, startTime: Instant, endTime: Instant) {
var res: MutableMap<String, Double> = mutableMapOf();
val numberOfTheDays = calcNumberOfTheDays(startTime, endTime)
val meetingWorkspaces = workspaceRepository.findAllByTag("meeting")
var dtStart: LocalDateTime =
LocalDateTime.ofEpochSecond(startTime.toEpochMilli(), 0, ZoneOffset.of(defaultTimeZone)).toLocalDate()
.atStartOfDay()

meetingWorkspaces.forEach { workspace ->
run {
var globalWorkspaceOccupationTime= 0L;
var globalWorkspaceFreeTime = 0L
for (i in 0..numberOfTheDays) {
val dailyGapStart = dtStart.plusDays(i.toLong()).plusHours(endDay.toLong())
.toInstant(ZoneOffset.of(defaultTimeZone))
val dailyGapEnd = dtStart.plusDays(i.toLong()).plusHours(endDay.toLong())
.toInstant(ZoneOffset.of(defaultTimeZone))

var occupationTime = 0L;
val freeTime = dailyGapEnd.toEpochMilli() - dailyGapStart.toEpochMilli(); // Кол-во свободного времени в дне

globalWorkspaceFreeTime += freeTime;
bookingService.findAll(
bookingRangeFrom = dailyGapStart.toEpochMilli(),
bookingRangeTo = dailyGapEnd.toEpochMilli(),
workspaceId = workspace.id
).map { booking ->
occupationTime += getSingleBookingOccupancy(
startBooking = booking.beginBooking,
endBooking = booking.endBooking,
gapStart = dailyGapStart,
gapEnd = dailyGapEnd
)
}
occupationTime = if (occupationTime > freeTime) freeTime else occupationTime
globalWorkspaceOccupationTime += occupationTime;
}
res[workspace.name] = (globalWorkspaceOccupationTime.toDouble())/(globalWorkspaceFreeTime.toDouble())
}
}

}

/**
* Сколько миллисекунд внутри выбранного промежутка занимает бронь
* */
fun getSingleBookingOccupancy(
startBooking: Instant,
endBooking: Instant,
gapStart: Instant,
gapEnd: Instant
): Long {
var start = startBooking;
var end = endBooking;
if (startBooking.isBefore(gapStart)) {
start = gapStart
}
if(endBooking.isAfter(gapEnd)){
end = gapEnd
}
return end.toEpochMilli() - start.toEpochMilli()
}

fun calcNumberOfTheDays(startTime: Instant, endTime: Instant): Int {
val startdate =
LocalDateTime.ofEpochSecond(startTime.toEpochMilli(), 0, ZoneOffset.of(defaultTimeZone)).toLocalDate()
val endDate =
LocalDateTime.ofEpochSecond(endTime.toEpochMilli(), 0, ZoneOffset.of(defaultTimeZone)).toLocalDate()
return ChronoUnit.DAYS.between(startdate, endDate).toInt()
}

fun startOfTheDay(dt: Instant): Instant {
var date: LocalDateTime = LocalDateTime.ofEpochSecond(dt.epochSecond, 0, ZoneOffset.of(defaultTimeZone))
return date
.toLocalDate()
.atStartOfDay()
.toInstant(ZoneOffset.of(defaultTimeZone))
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package office.effective.features.metrics.service

import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.micrometer.prometheus.PrometheusMeterRegistry
import org.koin.core.context.GlobalContext
import java.time.Instant

fun Route.metrics() {
route("metrics") {
val metricsService: MetricsService = GlobalContext.get().get()
get("/micrometer") {
val appMicrometerRegistry: PrometheusMeterRegistry = GlobalContext.get().get()
call.respond(appMicrometerRegistry.scrape())
}

get("/percentOfFreeWorkspaces") {
val startTime: Instant = Instant.ofEpochMilli(call.request.queryParameters["range_from"].let {
it?.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long")
})
val endTime: Instant = Instant.ofEpochMilli(call.request.queryParameters["range_to"].let {
it?.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long")
})
call.respond(metricsService.calculateOfficeOccupancy(startTime, endTime))
return@get
}

get("/pecentOfFreeTime") {
val dayStarts: Int = call.request.queryParameters["day_starts"].let {
it?.toInt() ?: throw BadRequestException("day_starts can't be parsed to Int")
}
val dayEnds: Int =call.request.queryParameters["day_ends"].let {
it?.toInt() ?: throw BadRequestException("day_ends can't be parsed to Int")
}
val startTime: Instant = Instant.ofEpochMilli(call.request.queryParameters["range_from"].let {
it?.toLongOrNull() ?: throw BadRequestException("range_from can't be parsed to Long")
})
val endTime: Instant = Instant.ofEpochMilli(call.request.queryParameters["range_to"].let {
it?.toLongOrNull() ?: throw BadRequestException("range_to can't be parsed to Long")
})
call.respond(metricsService.calcPlaceHours(dayStarts, dayEnds, startTime, endTime))
return@get
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package office.effective.plugins

import io.ktor.server.application.*
import io.ktor.server.metrics.micrometer.*
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics
import io.micrometer.core.instrument.binder.system.ProcessorMetrics
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig
import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import org.koin.core.context.GlobalContext
import java.time.Duration

fun Application.configureMicrometer() {
val appMicrometerRegistry : PrometheusMeterRegistry = GlobalContext.get().get()

install(MicrometerMetrics) {
registry = appMicrometerRegistry

distributionStatisticConfig = DistributionStatisticConfig.Builder()
.percentilesHistogram(true)
.maximumExpectedValue(Duration.ofSeconds(20).toNanos().toDouble())
.serviceLevelObjectives(
Duration.ofMillis(100).toNanos().toDouble(),
Duration.ofMillis(500).toNanos().toDouble()
)
.build()

meterBinders = listOf(
JvmMemoryMetrics(),
JvmGcMetrics(),
ProcessorMetrics()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import office.effective.features.user.routes.userRouting
import office.effective.features.booking.routes.bookingRouting
import office.effective.features.metrics.service.metrics
import office.effective.features.notifications.routes.calendarNotificationsRouting
import office.effective.features.workspace.routes.workspaceRouting

Expand All @@ -17,6 +18,8 @@ fun Application.configureRouting() {
userRouting()
bookingRouting()
calendarNotificationsRouting()
metrics()

}

}
1 change: 1 addition & 0 deletions effectiveOfficeBackend/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ calendar {
minTime = "1692727200000"
defaultCalendar = "effective.office@effective.band"
workspaceCalendar = "c_46707d19c716de0d5d28b52082edfeb03376269e7da5fea78e43fcb15afda57e@group.calendar.google.com"
timeZone = "Asia/Omsk"
}
liquibase {
changelogFile = "changelog/changelog-master.yaml"
Expand Down