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

feat: OpenTelemetry #666

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
"datasourceTemplate": "github-tags",
"packageNameTemplate": "aws/aws-cli",
"autoReplaceStringTemplate": "{{depName}} = \"ref:{{newVersion}}\""
},
{
"customType": "regex",
"fileMatch": ["\\.ts$"],
"matchStrings": [
"renovate: datasource=(?<datasource>.+?)\\s+\"(?<depName>\\S+):(?<currentValue>\\S+)\""
],
"versioningTemplate": "semver"
}
]
}
2 changes: 0 additions & 2 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,4 @@ awscli = "ref:2.22.32"
java = "corretto-21.0.5.11.1"
shellcheck = "0.10.0"
ktlint = "1.5.0"
go = "1.23.4"
"npm:esbuild" = "0.24.0"
"go:github.com/humanlogio/humanlog/cmd/humanlog" = "0.7.8"
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,18 @@ services:
PGUSER: kitu
ports:
- "127.0.0.1:5432:5432"

collector:
image: otel/opentelemetry-collector:0.114.0
command: ["--config=/etc/otel-collector-config.yml"]
volumes:
- ./otel-config.yml:/etc/otel-collector-config.yml
ports:
- "4318:4318" # OTLP HTTP
depends_on:
- jaeger

jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
13 changes: 13 additions & 0 deletions infra/lib/service-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DatabaseCluster } from "aws-cdk-lib/aws-rds"
import { HostedZone } from "aws-cdk-lib/aws-route53"
import { ITopic } from "aws-cdk-lib/aws-sns"
import { Construct } from "constructs"
import { ManagedPolicy } from "aws-cdk-lib/aws-iam"

export interface ServiceStackProps extends StackProps {
auditLogGroup: ILogGroup
Expand Down Expand Up @@ -135,13 +136,25 @@ export class ServiceStack extends Stack {
sslPolicy: SslPolicy.RECOMMENDED_TLS,
})

this.service.taskDefinition.addContainer("AwsOtelCollector", {
image: ContainerImage.fromRegistry(
// renovate: datasource=docker
"public.ecr.aws/aws-observability/aws-otel-collector:v0.41.1",
),
})

this.service.targetGroup.configureHealthCheck({
...this.service.targetGroup.healthCheck,
path: "/actuator/health",
})

props.auditLogGroup.grantWrite(this.service.service.taskDefinition.taskRole)

// Ref: https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSXrayWriteOnlyAccess.html
this.service.taskDefinition.taskRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName("AWSXrayWriteOnlyAccess"),
)

this.service.service
.metricCpuUtilization()
.createAlarm(this, "CpuUtilization", {
Expand Down
34 changes: 34 additions & 0 deletions otel-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
receivers:
otlp:
protocols:
grpc:
http:
endpoint: 0.0.0.0:4318

processors:
batch:

extensions:
health_check:

exporters:
debug:
otlphttp/jaeger:
endpoint: http://jaeger:4318
otlp/oteltui:
endpoint: oteltui:4317

service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlphttp/jaeger, otlp/oteltui]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlp/oteltui]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlp/oteltui]
4 changes: 1 addition & 3 deletions scripts/start_local_server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ get_secret() {
aws secretsmanager get-secret-value --secret-id "$1" --output text --query SecretString
}

require_command humanlog

require_env SPRING_PROFILES_ACTIVE
require_env KOTLIN_POST_PROCESS_FILE

cd "$REPO_ROOT"/server
./mvnw spring-boot:run | humanlog --truncate-length 9999
./mvnw spring-boot:run
20 changes: 20 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom</artifactId>
<version>2.10.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
Expand Down Expand Up @@ -180,6 +187,14 @@
<artifactId>db-scheduler-log-spring-boot-starter</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
Expand All @@ -194,6 +209,11 @@
<artifactId>opentelemetry-semconv-incubating</artifactId>
<version>${opentelemetry-semconv.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-aws-sdk-2.2-autoconfigure</artifactId>
<version>2.10.0-alpha</version>
</dependency>
<dependency>
<groupId>no.bekk.db-scheduler-ui</groupId>
<artifactId>db-scheduler-ui-starter</artifactId>
Expand Down
10 changes: 2 additions & 8 deletions server/src/main/kotlin/fi/oph/kitu/ErrorHandler.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package fi.oph.kitu

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
Expand All @@ -18,18 +16,14 @@ data class RestErrorMessage(

@ControllerAdvice
class GlobalControllerExceptionHandler {
private val logger: Logger = LoggerFactory.getLogger(GlobalControllerExceptionHandler::class.java)

@ExceptionHandler
fun handleRestClientException(ex: RestClientException): ResponseEntity<RestErrorMessage> {
logger.error(ex.stackTraceToString())
return ResponseEntity(
fun handleRestClientException(ex: RestClientException): ResponseEntity<RestErrorMessage> =
ResponseEntity(
RestErrorMessage(
status = HttpStatus.SERVICE_UNAVAILABLE.value(),
error = "Service Unavailable",
message = "Call to external API failed",
),
HttpStatus.SERVICE_UNAVAILABLE,
)
}
}
28 changes: 7 additions & 21 deletions server/src/main/kotlin/fi/oph/kitu/csvparsing/CsvParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,25 @@ import com.fasterxml.jackson.dataformat.csv.CsvMapper
import com.fasterxml.jackson.dataformat.csv.CsvSchema
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import fi.oph.kitu.logging.add
import io.opentelemetry.api.trace.Span
import org.ietf.jgss.Oid
import org.slf4j.spi.LoggingEventBuilder
import java.io.ByteArrayOutputStream
import java.lang.RuntimeException
import kotlin.reflect.full.findAnnotation

class CsvParser(
val event: LoggingEventBuilder,
val columnSeparator: Char = ',',
val lineSeparator: String = "\n",
val useHeader: Boolean = false,
val quoteChar: Char = '"',
) {
init {
event.add(
"serialization.schema.args.columnSeparator" to columnSeparator.toString(),
"serialization.schema.args.lineSeparator" to lineSeparator,
"serialization.schema.args.useHeader" to useHeader,
"serialization.schema.args.quoteChar" to quoteChar,
)
}

inline fun <reified T> getSchema(csvMapper: CsvMapper): CsvSchema {
event.add("serialization.schema.args.type" to T::class.java.name)

return csvMapper
inline fun <reified T> getSchema(csvMapper: CsvMapper): CsvSchema =
csvMapper
.typedSchemaFor(T::class.java)
.withColumnSeparator(columnSeparator)
.withLineSeparator(lineSeparator)
.withUseHeader(useHeader)
.withQuoteChar(quoteChar)
}

inline fun <reified T> CsvMapper.Builder.withFeatures(): CsvMapper.Builder {
val mapperFeatures = T::class.findAnnotation<Features>()?.features
Expand Down Expand Up @@ -85,12 +72,9 @@ class CsvParser(
*/
inline fun <reified T> convertCsvToData(csvString: String): List<T> {
if (csvString.isBlank()) {
event.add("serialization.isEmptyList" to true)
return emptyList()
}

event.add("serialization.isEmptyList" to false)

val csvMapper = getCsvMapper<T>()
val schema = getSchema<T>(csvMapper)

Expand All @@ -115,11 +99,13 @@ class CsvParser(
return data
}

val span = Span.current()

// add all errors to log
errors.forEachIndexed { i, error ->
event.add("serialization.error[$i].index" to i)
span.setAttribute("serialization.error[$i].index", i.toLong())
for (kvp in error.keyValues) {
event.add("serialization.error[$i].${kvp.first}" to kvp.second)
span.setAttribute("serialization.error[$i].${kvp.first}", kvp.second.toString())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ class KoealustaScheduledTasks {
Tasks
.recurring("Koto-import", Schedules.parseSchedule(koealustaImportSchedule), Instant::class.java)
.initialData(Instant.EPOCH)
.executeStateful { taskInstance, _ -> koealustaService.importSuoritukset(taskInstance.data) }
.executeStateful { taskInstance, _ ->
koealustaService.importSuoritukset(taskInstance.data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import fi.oph.kitu.PeerService
import fi.oph.kitu.logging.Logging
import fi.oph.kitu.logging.add
import fi.oph.kitu.logging.addHttpResponse
import fi.oph.kitu.logging.withEventAndPerformanceCheck
import fi.oph.kitu.oppijanumero.addValidationExceptions
import org.slf4j.LoggerFactory
import io.opentelemetry.instrumentation.annotations.WithSpan
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
Expand All @@ -24,7 +21,6 @@ class KoealustaService(
private val jacksonObjectMapper: ObjectMapper,
private val mappingService: KoealustaMappingService,
) {
private val logger = LoggerFactory.getLogger(javaClass)
private val auditLogger = Logging.auditLogger()

@Value("\${kitu.kotoutumiskoulutus.koealusta.wstoken}")
Expand Down Expand Up @@ -57,62 +53,44 @@ class KoealustaService(
}
}

fun importSuoritukset(from: Instant) =
logger
.atInfo()
.withEventAndPerformanceCheck { event ->
event.add("from" to from)

val response =
restClient
.get()
.uri(
"/webservice/rest/server.php?wstoken={token}&wsfunction={function}&moodlewsrestformat=json&from={from}",
mapOf<String?, Any>(
"token" to koealustaToken,
"function" to "local_completion_export_get_completions",
"from" to from.epochSecond,
),
).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<String>()

event
.add("request.token" to koealustaToken)
.addHttpResponse(PeerService.Koealusta, uri = "/webservice/rest/server.php", response)

if (response.body == null) {
return@withEventAndPerformanceCheck from
}

val suorituksetResponse =
tryParseMoodleResponse<KoealustaSuorituksetResponse>(response.body!!)
@WithSpan
fun importSuoritukset(from: Instant): Instant {
val response =
restClient
.get()
.uri(
"/webservice/rest/server.php?wstoken={token}&wsfunction={function}&moodlewsrestformat=json&from={from}",
mapOf<String?, Any>(
"token" to koealustaToken,
"function" to "local_completion_export_get_completions",
"from" to from.epochSecond,
),
).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<String>()

if (response.body == null) {
return from
}

val suoritukset =
try {
mappingService.convertToEntity(suorituksetResponse)
} catch (ex: KoealustaMappingService.Error.ValidationFailure) {
event.addValidationExceptions(ex.oppijanumeroExceptions, ex.validationErrors)
throw ex
}
val suorituksetResponse =
tryParseMoodleResponse<KoealustaSuorituksetResponse>(response.body!!)

val savedSuoritukset = kielitestiSuoritusRepository.saveAll(suoritukset)
val suoritukset =
mappingService.convertToEntity(suorituksetResponse)

event.add("db.saved" to savedSuoritukset.count())
val savedSuoritukset = kielitestiSuoritusRepository.saveAll(suoritukset)

for (suoritus in savedSuoritukset) {
auditLogger
.atInfo()
.add(
"principal" to "koealusta.import",
"peer.service" to PeerService.Koealusta.value,
"suoritus.id" to suoritus.id,
).log("Kielitesti suoritus imported")
}
for (suoritus in savedSuoritukset) {
auditLogger
.atInfo()
.add(
"principal" to "koealusta.import",
"peer.service" to PeerService.Koealusta.value,
"suoritus.id" to suoritus.id,
).log("Kielitesti suoritus imported")
}

return@withEventAndPerformanceCheck suoritukset.maxOfOrNull { it.timeCompleted } ?: from
}.apply {
addDefaults("koealusta.importSuoritukset")
addDatabaseLogs()
}.getOrThrow()
return suoritukset.maxOfOrNull { it.timeCompleted } ?: from
}
}
Loading
Loading