Skip to content

Commit

Permalink
Add support for matching patterns and single-item lists (#38)
Browse files Browse the repository at this point in the history
* Add single-item list and regex adapters

* Increment config version

* Implement matching patterns support in config

* detekt
  • Loading branch information
ZacSweers committed Aug 11, 2023
1 parent abd7443 commit f34baab
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 36 deletions.
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

0 comments on commit f34baab

Please sign in to comment.