Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

Add support for matching patterns and single-item lists #38

Merged
merged 4 commits into from
Aug 11, 2023
Merged
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
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ Releasing

1. Update the `CHANGELOG.md` for the impending release.
2. Run `./release.sh (--patch|--minor|--major)`.
3. Publish the release on the repo's releases tab.
3. Publish the release on the repo's releases tab.
19 changes: 9 additions & 10 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import io.gitlab.arturbosch.detekt.Detekt
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.kotlin.jvm)
Expand Down Expand Up @@ -61,14 +60,6 @@ tasks.withType<JavaCompile>().configureEach {
options.release.set(libs.versions.jvmTarget.get().toInt())
}

tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
allWarningsAsErrors.set(true)
freeCompilerArgs.add("-progressive")
}
}

tasks.withType<Detekt>().configureEach { jvmTarget = libs.versions.jvmTarget.get() }

tasks.withType<DokkaTask>().configureEach {
Expand All @@ -81,7 +72,15 @@ mavenPublishing {
signAllPublications()
}

kotlin { explicitApi() }
kotlin {
explicitApi()
compilerOptions {
jvmTarget.set(libs.versions.jvmTarget.map(JvmTarget::fromTarget))
allWarningsAsErrors.set(true)
progressiveMode.set(true)
optIn.add("kotlin.ExperimentalStdlibApi")
}
}

moshi { enableSealed.set(true) }

Expand Down
46 changes: 35 additions & 11 deletions src/main/kotlin/slack/cli/shellsentry/Issue.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,59 @@ import com.squareup.moshi.JsonClass
* @property message the message shown in the bugsnag report message. Should be human-readable.
* @property logMessage the message shown in the CI log when [matchingText] is found. Should be
* human-readable.
* @property matchingText the matching text to look for in the log.
* @property groupingHash grouping hash for reporting to bugsnag. This should usually be unique, but
* can also be reused across issues that are part of the same general issue.
* @property retrySignal the [RetrySignal] to use when this issue is found.
* @property description an optional description of the issue. Not used in the CLI, just there for
* documentation in the config.
* @property matchingText a list of matching texts to look for in the log.
* @property matchingPatterns a list of matching regexp patterns to look for in the log.
* @property retrySignal the [RetrySignal] to use when this issue is found.
*/
@JsonClass(generateAdapter = true)
internal data class Issue(
internal data class Issue
@JvmOverloads
constructor(
val message: String,
@Json(name = "log_message") val logMessage: String,
@Json(name = "matching_text") val matchingText: String,
@Json(name = "grouping_hash") val groupingHash: String,
@Json(name = "retry_signal") val retrySignal: RetrySignal,
val description: String? = null,
@Json(name = "matching_text") val matchingText: List<String> = emptyList(),
@Json(name = "matching_patterns") val matchingPatterns: List<Regex> = emptyList(),
) {

private fun List<String>.checkContains(errorText: String): Boolean {
return any { it.contains(errorText, ignoreCase = true) }
init {
check(matchingText.isNotEmpty() || matchingPatterns.isNotEmpty()) {
"Issue must have at least one matching text or pattern."
}
}

private inline fun List<String>.checkMatches(check: (line: String) -> Boolean): Boolean {
return any { check(it) }
}

/** Checks the log for this issue and returns a [RetrySignal] if it should be retried. */
@Suppress("ReturnCount")
fun check(lines: List<String>, log: (String) -> Unit): RetrySignal {
return if (lines.checkContains(matchingText)) {
log(logMessage)
retrySignal
} else {
RetrySignal.Unknown
if (matchingText.isNotEmpty()) {
for (matchingText in matchingText) {
if (lines.checkMatches { it.contains(matchingText, ignoreCase = true) }) {
log(logMessage)
return retrySignal
}
}
}

if (matchingPatterns.isNotEmpty()) {
for (pattern in matchingPatterns) {
if (lines.checkMatches { pattern.matches(it) }) {
log(logMessage)
return retrySignal
}
}
}

return RetrySignal.Unknown
}
}

Expand Down
17 changes: 9 additions & 8 deletions src/main/kotlin/slack/cli/shellsentry/KnownIssues.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ internal object KnownIssues {
Issue(
message = "Fake failure",
logMessage = "Detected fake failure. Beep boop.",
matchingText = "FAKE FAILURE NOT REAL",
matchingText = listOf("FAKE FAILURE NOT REAL"),
matchingPatterns = listOf(".*FAKE_FAILURE_[a-zA-Z].*".toRegex()),
groupingHash = "fake-failure",
retrySignal = RetrySignal.Ack
)

val ftlRateLimit =
Issue(
message = "FTL rate limit",
matchingText = "429 Too Many Requests",
matchingText = listOf("429 Too Many Requests"),
logMessage = "Detected FTL rate limit. Retrying in 1 minute.",
groupingHash = "ftl-rate-limit",
retrySignal = RetrySignal.RetryDelayed(1.minutes)
Expand All @@ -44,7 +45,7 @@ internal object KnownIssues {
val oom =
Issue(
message = "Generic OOM",
matchingText = "Java heap space",
matchingText = listOf("Java heap space"),
logMessage = "Detected OOM. Retrying immediately.",
groupingHash = OOM_GROUPING_HASH,
retrySignal = RetrySignal.RetryImmediately
Expand All @@ -53,7 +54,7 @@ internal object KnownIssues {
val ftlInfrastructureFailure =
Issue(
message = "Inconclusive FTL infrastructure failure",
matchingText = "Infrastructure failure",
matchingText = listOf("Infrastructure failure"),
logMessage = "Detected inconclusive FTL infrastructure failure. Retrying immediately.",
groupingHash = "ftl-infrastructure-failure",
retrySignal = RetrySignal.RetryImmediately
Expand All @@ -63,15 +64,15 @@ internal object KnownIssues {
Issue(
message = "Flank timeout",
groupingHash = "flank-timeout",
matchingText = "Canceling flank due to timeout",
matchingText = listOf("Canceling flank due to timeout"),
logMessage = "Detected a flank timeout. Retrying immediately.",
retrySignal = RetrySignal.RetryImmediately
)

val r8Oom =
Issue(
message = "R8 OOM",
matchingText = "Out of space in CodeCache",
matchingText = listOf("Out of space in CodeCache"),
logMessage = "Detected a OOM in R8. Retrying immediately.",
groupingHash = OOM_GROUPING_HASH,
retrySignal = RetrySignal.RetryImmediately
Expand All @@ -81,7 +82,7 @@ internal object KnownIssues {
Issue(
message = "OOM killed by kernel",
groupingHash = OOM_GROUPING_HASH,
matchingText = "Gradle build daemon disappeared unexpectedly",
matchingText = listOf("Gradle build daemon disappeared unexpectedly"),
logMessage = "Detected a OOM that was killed by the kernel. Retrying immediately.",
retrySignal = RetrySignal.RetryImmediately
)
Expand All @@ -90,7 +91,7 @@ internal object KnownIssues {
Issue(
message = "Bugsnag artifact upload failure",
groupingHash = "bugsnag-upload-failure",
matchingText = "Bugsnag request failed to complete",
matchingText = listOf("Bugsnag request failed to complete"),
logMessage = "Detected bugsnag failed to upload. Retrying immediately.",
retrySignal = RetrySignal.RetryImmediately
)
Expand Down
9 changes: 7 additions & 2 deletions src/main/kotlin/slack/cli/shellsentry/ProcessingUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ package slack.cli.shellsentry
import com.squareup.moshi.Moshi
import com.squareup.moshi.addAdapter
import slack.cli.util.DurationJsonAdapter
import slack.cli.util.RegexJsonAdapter
import slack.cli.util.SingleItemListJsonAdapterFactory

internal object ProcessingUtil {
@OptIn(ExperimentalStdlibApi::class)
fun newMoshi(): Moshi {
return Moshi.Builder().addAdapter(DurationJsonAdapter()).build()
return Moshi.Builder()
.add(SingleItemListJsonAdapterFactory())
.addAdapter(DurationJsonAdapter())
.add(RegexJsonAdapter.Factory())
.build()
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/slack/cli/shellsentry/ShellSentryConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlin.reflect.full.declaredMemberProperties

private const val CURRENT_VERSION = 1
internal const val CURRENT_VERSION = 2

/** Represents a configuration for [ShellSentryCli]. */
@JsonClass(generateAdapter = true)
Expand Down
46 changes: 46 additions & 0 deletions src/main/kotlin/slack/cli/util/RegexJsonAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package slack.cli.util

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import com.squareup.moshi.rawType
import java.lang.reflect.Type

/** A simple [Regex] adapter that converts Strings to a [Regex]. */
internal class RegexJsonAdapter(
private val stringAdapter: JsonAdapter<String>,
) : JsonAdapter<Regex>() {
override fun fromJson(reader: JsonReader) = stringAdapter.fromJson(reader)!!.toRegex()

override fun toJson(writer: JsonWriter, value: Regex?) {
error("RegexJsonAdapter is only used for deserialization")
}

internal class Factory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
return when (type.rawType) {
Regex::class.java -> {
RegexJsonAdapter(moshi.adapter<String>().nullSafe()).nullSafe()
}
else -> null
}
}
}
}
87 changes: 87 additions & 0 deletions src/main/kotlin/slack/cli/util/SingleItemListJsonAdapterFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package slack.cli.util

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.rawType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

/** Decodes a List<T> that could also come down the wire as a single element. */
internal class SingleItemListJsonAdapterFactory : JsonAdapter.Factory {
@Suppress("ReturnCount")
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (annotations.isNotEmpty()) return null

val rawType = type.rawType
if (type is ParameterizedType && List::class.java.isAssignableFrom(rawType)) {
// It's a List!
val elementType = Types.collectionElementType(type, rawType)
val delegateListAdapter = moshi.nextAdapter<List<*>>(this, type, annotations)
// Get a reusable FuzzyString adapter to get strings from all JSON primitives
val elementTypeAdapter = moshi.adapter<Any>(elementType)
return ListJsonAdapter(delegateListAdapter, elementTypeAdapter)
}

return null
}

private class ListJsonAdapter<T>(
private val listAdapter: JsonAdapter<List<T>>,
private val collectionTypeAdapter: JsonAdapter<T>,
) : JsonAdapter<List<T>>() {
override fun toJson(writer: JsonWriter, value: List<T>?) {
listAdapter.toJson(writer, value)
}

override fun fromJson(reader: JsonReader): List<T>? {
val result: List<T>?
when (reader.peek()) {
JsonReader.Token.NULL -> {
// Carry over null value
result = reader.nextNull()
}
JsonReader.Token.BEGIN_ARRAY -> {
// Happy path, expected list type
reader.beginArray()
result = buildList {
while (reader.hasNext()) {
add(collectionTypeAdapter.fromJson(reader)!!)
}
}
reader.endArray()
}
JsonReader.Token.BEGIN_OBJECT -> {
throw JsonDataException(
"Expected BEGIN_ARRAY but was BEGIN_OBJECT at path ${reader.path}"
)
}
else -> {
// Single element, try to decode as a single item
result = listOf(collectionTypeAdapter.fromJson(reader)!!)
}
}
return result
}

override fun toString() = "FuzzyJsonAdapter(List<T>)"
}
}
20 changes: 20 additions & 0 deletions src/test/kotlin/slack/cli/shellsentry/ResultProcessorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ class ResultProcessorTest {
check(signal is RetrySignal.Ack)
}

@Test
fun matchingPattern_matches() {
val outputFile = tmpFolder.newFile("logs.txt")
outputFile.writeText("""
FAKE_FAILURE_a
""".trimIndent().padWithTestLogs())
val signal = newProcessor().process(outputFile.toPath(), isAfterRetry = false)
check(signal is RetrySignal.Ack)
}

@Test
fun matchingPattern_doesNotMatch() {
val outputFile = tmpFolder.newFile("logs.txt")
outputFile.writeText("""
FAKE_FAILURE-a
""".trimIndent().padWithTestLogs())
val signal = newProcessor().process(outputFile.toPath(), isAfterRetry = false)
check(signal is RetrySignal.Unknown)
}

@Test
fun parseBuildScan() {
val url = "https://gradle-enterprise.example.com"
Expand Down
Loading