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 all 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
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
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
@@ -0,0 +1,17 @@
package office.effective.features.metrics.di

import office.effective.common.constants.BookingConstants
import office.effective.features.booking.service.BookingService
import office.effective.features.metrics.service.MetricsService
import org.koin.dsl.module

val metricsDiModule = module(createdAtStart = true) {
single {
MetricsService(
get(),
get(),
BookingService(get(), get(), get(), get()),
BookingConstants
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package office.effective.features.metrics.routes

import io.github.smiley4.ktorswaggerui.dsl.get
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import office.effective.common.swagger.SwaggerDocument
import office.effective.features.metrics.routes.swagger.pecentOfFreeTime
import office.effective.features.metrics.routes.swagger.percentOfFreeWorkspaces
import office.effective.features.metrics.service.MetricsService
import org.koin.core.context.GlobalContext
import java.time.Instant

fun Route.metrics() {
route("metrics") {
val metricsService: MetricsService = GlobalContext.get().get()
get("/percentOfFreeWorkspaces", SwaggerDocument.percentOfFreeWorkspaces()) {
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.calculateOfficeOccupancy(startTime, endTime))
return@get
}

get("/pecentOfFreeTime", SwaggerDocument.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,88 @@
package office.effective.features.metrics.routes.swagger;

import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute
import io.ktor.http.*
import office.effective.common.constants.BookingConstants
import office.effective.common.swagger.SwaggerDocument
import java.time.Instant
import kotlin.Unit;

fun SwaggerDocument.percentOfFreeWorkspaces(): OpenApiRoute.() -> Unit = {
description = "Какой процент переговорок(и рабочих мест) был свободен весь указанный промежуток времени.\n" +
" * На коротких промежутках времени подходит как датчик занятости офиса в на момент середины отрезка"


request {
queryParameter<Long>("range_from") {
description = "Lower bound. Should be lover than range_to."

example = 1692927200000
required = true
allowEmptyValue = false

}
queryParameter<Long>("range_to") {
description = "Upper bound. Should be greater than range_from."
example = 1697027200000
required = true
allowEmptyValue = false
}
}

response {
HttpStatusCode.OK to {
description = "Returns Map<String, Double>, with content (from 0 to 1) of free regular and meeting workspaces was free all the time you specified."
body<Map<String, Double>> {
val res: MutableMap<String, Double> = mutableMapOf("regular" to 0.25, "meeting" to 0.99);
}
}
}
}

fun SwaggerDocument.pecentOfFreeTime(): OpenApiRoute.() -> Unit = {
description = "Какой долю от всего количества миллисекунд в промежутке времени, пока офис был открыт, была свободна каждая из переговорок"


request {
queryParameter<Long>("range_from") {
description = "Lower bound. Should be lover than range_to. "

example = 1704049261000
required = true
allowEmptyValue = false

}
queryParameter<Long>("range_to") {
description = "Upper bound. Should be greater than range_from."
example = 1704049261000
required = true
allowEmptyValue = false
}

queryParameter<Long>("day_starts") {
description = "Lower bound. Hour in the day. " +
"Default value: ${office.effective.common.constants.BookingConstants.MIN_SEARCH_START_TIME} " +
"(${java.time.Instant.ofEpochMilli(office.effective.common.constants.BookingConstants.MIN_SEARCH_START_TIME)}). "

example = 8
required = false
allowEmptyValue = false

}
queryParameter<Long>("day_ends") {
description = "Upper bound. Hour in the day."
example = 20
required = false
allowEmptyValue = false
}
}

response {
HttpStatusCode.OK to {
description = "Returns Map<String, Double>, with content (from 0 to 1) of free each workspaces was free all the time you specified."
body<Map<String, Double>> {
val res: MutableMap<String, Double> = mutableMapOf("regular" to 0.25, "meeting" to 0.99);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package office.effective.features.metrics.service

import office.effective.common.constants.BookingConstants
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.ZoneId
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) :MutableMap<String, Double>{
var res: MutableMap<String, Double> = mutableMapOf();
val numberOfTheDays = calcNumberOfTheDays(startTime, endTime)
val meetingWorkspaces = workspaceRepository.findAllByTag("meeting")
var dtStart: LocalDateTime =
LocalDateTime.ofEpochSecond(
startTime.toEpochMilli()/1000,
0,
ZoneId.of(defaultTimeZone).rules.getOffset(startTime)
).toLocalDate()
.atStartOfDay()

for (workspace in meetingWorkspaces) {
var globalWorkspaceOccupationTime = 0L;
var globalWorkspaceFreeTime = 0L
for (i in 0..numberOfTheDays) {
val dailyGapStart = dtStart.plusDays(i.toLong()).plusHours(startDay.toLong())
.toInstant(ZoneId.of(defaultTimeZone).rules.getOffset(startTime))
val dailyGapEnd = dtStart.plusDays(i.toLong()).plusHours(endDay.toLong())
.toInstant(ZoneId.of(defaultTimeZone).rules.getOffset(endTime))

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] = (1.0-((globalWorkspaceOccupationTime.toDouble()) / (globalWorkspaceFreeTime.toDouble())))*100

}
return res
}

/**
* Сколько миллисекунд внутри выбранного промежутка занимает бронь
* */
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,
ZoneId.of(defaultTimeZone).rules.getOffset(startTime)
).toLocalDate()
val endDate =
LocalDateTime.ofEpochSecond(endTime.toEpochMilli(), 0, ZoneId.of(defaultTimeZone).rules.getOffset(endTime))
.toLocalDate()
return (ChronoUnit.DAYS.between(startdate, endDate)/1000).toInt()
}

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


}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import office.effective.common.di.commonDiModule
import office.effective.features.booking.di.bookingDiModule
import office.effective.common.di.calendarDiModule
import office.effective.features.auth.di.authDiModule
import office.effective.features.metrics.di.metricsDiModule
import office.effective.features.user.di.userDIModule
import office.effective.features.workspace.DI.workspaceDiModule
import org.koin.ktor.plugin.Koin
Expand All @@ -22,7 +23,8 @@ fun Application.configureDI() {
bookingDiModule,
authDiModule,
calendarDiModule,
firebaseDiModule
firebaseDiModule,
metricsDiModule
)
}
}
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.routes.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