Create FUS Gradle plugin

fus-statistics-gradle-plugin can be used by any other Gradle plugins to
collect additional metrics for FUS.

KT-59627
This commit is contained in:
Nataliya.Valtman
2023-08-14 19:35:12 +02:00
committed by Space Team
parent 4ed7504d87
commit 9de791b9ac
16 changed files with 366 additions and 2 deletions
@@ -0,0 +1,19 @@
## Description
Contains a plugin for Feature Usage Statistics(FUS). fus-statistics-gradle-plugin can be used by other Gradle plugins to
collect additional metrics for FUS.
Statistics will be gathered in **kotlin-fus/<build_uid>** file. It is located in ${GRADLE_USER_HOME} directory by default.
You can modify default directory by using the **kotlin.fus.statistics.path** property (for test purpose only).
Created file will contain **key=value** rows fowled by **BUILD FINISHED**
Collection statistics in a file is enabled by default. Further step is to collect data only after approval - KT-59629.
In any case, collected data will be sent only after user's agreement.
### Collect own data
**org.jetbrains.kotlin.fus-statistics-gradle-plugin** plugin should be applied and a new **UsesGradleBuildFusStatisticsService** task
should be created to be able to collect additional data.
In this case, the task will access **GradleBuildFusStatistics** and be able to report new metrics via **reportMetric**.
Analytics teams' approve is required for any collected data.
@@ -0,0 +1,36 @@
import plugins.KotlinBuildPublishingPlugin
plugins {
id("gradle-plugin-common-configuration")
}
dependencies {
commonApi(project(":kotlin-gradle-plugin-api"))
commonApi(project(":kotlin-gradle-plugin"))
}
gradlePlugin {
plugins {
create("fus-statistics-gradle-plugin") {
id = "org.jetbrains.kotlin.fus-statistics-gradle-plugin"
displayName = "FusStatisticsPlugin"
description = displayName
implementationClass = "org.jetbrains.kotlin.gradle.fus.FusStatisticsPlugin"
}
}
}
// Disable releasing for this plugin
// It is not intended to be released publicly
tasks.withType<PublishToMavenRepository>()
.configureEach {
if (name.endsWith("PublicationTo${KotlinBuildPublishingPlugin.REPOSITORY_NAME}Repository")) {
enabled = false
}
}
tasks.named("publishPlugins") {
enabled = false
}
@@ -0,0 +1,24 @@
package org.jetbrains.kotlin.gradle.fus
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.ProviderFactory
import org.jetbrains.kotlin.gradle.fus.internal.GradleBuildFusStatisticsBuildService
import javax.inject.Inject
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
/**
* FusStatisticsPlugin is a Gradle plugin that registers a BuildService to collect and report
* feature usage statistics during the build process.
*/
class FusStatisticsPlugin @Inject constructor(
private val providerFactory: ProviderFactory
) : Plugin<Project> {
override fun apply(project: Project) {
GradleBuildFusStatisticsBuildService.registerIfAbsent(project)
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.fus
import org.gradle.api.Task
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal
/**
* A service interface for build FUS statistics reporting.
*/
interface GradleBuildFusStatisticsService {
/**
* Reports a metric by its name and optionally subproject.
*
* @param name the metric name
* @param value the metric value.
* @param subprojectName the subproject name for which the metric is being reported.
*/
fun reportMetric(name: String, value: String, subprojectName: String? = null)
/**
* @see org.jetbrains.kotlin.gradle.fus.GradleBuildFusStatisticsService.reportMetric(java.lang.String, java.lang.String, java.lang.String)
*/
fun reportMetric(name: String, value: Number, subprojectName: String? = null)
/**
* @see org.jetbrains.kotlin.gradle.fus.GradleBuildFusStatisticsService.reportMetric(java.lang.String, java.lang.String, java.lang.String)
*/
fun reportMetric(name: String, value: Boolean, subprojectName: String? = null)
}
/**
* This task interface provide access to GradleBuildFusStatisticsService.
*
* @property fusStatisticsBuildService holds an instance of GradleBuildFusStatisticsService.
*/
interface UsesGradleBuildFusStatisticsService : Task {
@get:Internal
val fusStatisticsBuildService: Property<GradleBuildFusStatisticsService>
}
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.fus.internal
/**
* Dummy service is used to avoid data collection without user's consent
*/
internal abstract class DummyGradleBuildFusStatisticsService : GradleBuildFusStatisticsBuildService() {
override fun reportMetric(name: String, value: Boolean, subprojectName: String?) {}
override fun reportMetric(name: String, value: Number, subprojectName: String?) {}
override fun reportMetric(name: String, value: String, subprojectName: String?) {}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.fus.internal
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.jetbrains.kotlin.gradle.fus.GradleBuildFusStatisticsService
import org.jetbrains.kotlin.gradle.fus.UsesGradleBuildFusStatisticsService
import java.util.*
abstract class GradleBuildFusStatisticsBuildService : GradleBuildFusStatisticsService,
BuildService<GradleBuildFusStatisticsBuildService.Parameters>, AutoCloseable {
interface Parameters : BuildServiceParameters {
val fusStatisticsRootDirPath: Property<String>
val buildId: Property<String>
}
companion object {
private var statisticsIsEnabled: Boolean = true //KT-59629 Wait for user confirmation before start to collect metrics
private const val FUS_STATISTICS_PATH = "kotlin.fus.statistics.path"
private val serviceClass = GradleBuildFusStatisticsBuildService::class.java
private val serviceName = "${serviceClass.name}_${serviceClass.classLoader.hashCode()}"
fun registerIfAbsent(project: Project): Provider<out GradleBuildFusStatisticsService>? {
project.gradle.sharedServices.registrations.findByName(serviceName)?.let {
@Suppress("UNCHECKED_CAST")
return it.service as Provider<GradleBuildFusStatisticsService>
}
return (if (statisticsIsEnabled) {
project.gradle.sharedServices.registerIfAbsent(serviceName, InternalGradleBuildFusStatisticsService::class.java) {
val customPath: String = if (project.rootProject.hasProperty(FUS_STATISTICS_PATH)) {
project.rootProject.property(FUS_STATISTICS_PATH) as String
} else {
project.gradle.gradleUserHomeDir.path
}
it.parameters.fusStatisticsRootDirPath.set(customPath)
it.parameters.buildId.set(UUID.randomUUID().toString())
}
} else {
project.gradle.sharedServices.registerIfAbsent(serviceName, DummyGradleBuildFusStatisticsService::class.java) {}
}).also { configureTasks(project, it) }
}
private fun configureTasks(project: Project, serviceProvider: Provider<out GradleBuildFusStatisticsBuildService>) {
project.tasks.withType(UsesGradleBuildFusStatisticsService::class.java).configureEach { task ->
task.fusStatisticsBuildService.value(serviceProvider).disallowChanges()
task.usesService(serviceProvider)
}
}
}
}
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.fus.internal
import org.gradle.api.logging.Logging
import java.io.File
import java.io.FileOutputStream
import java.nio.file.Files
import java.util.concurrent.ConcurrentHashMap
internal abstract class InternalGradleBuildFusStatisticsService : GradleBuildFusStatisticsBuildService() {
private val metrics = ConcurrentHashMap<Metric, Any>()
private val log = Logging.getLogger(this.javaClass)
override fun close() {
val reportFile = File(parameters.fusStatisticsRootDirPath.get(), STATISTICS_FOLDER_NAME)
.also { Files.createDirectories(it.toPath()) }
.resolve(parameters.buildId.get())
reportFile.createNewFile()
FileOutputStream(reportFile, true).bufferedWriter().use {
for ((key, value) in metrics) {
it.appendLine("$key=$value")
}
it.appendLine(BUILD_SESSION_SEPARATOR)
}
}
override fun reportMetric(name: String, value: Boolean, subprojectName: String?) {
internalReportMetric(name, value, subprojectName)
}
override fun reportMetric(name: String, value: String, subprojectName: String?) {
internalReportMetric(name, value, subprojectName)
}
override fun reportMetric(name: String, value: Number, subprojectName: String?) {
internalReportMetric(name, value, subprojectName)
}
private fun internalReportMetric(name: String, value: Any, subprojectName: String?) {
val oldValue = metrics.getOrPut(Metric(name, subprojectName)) { value }
if (oldValue != value) {
log.warn("Try to override $name metric: current value is \"$oldValue\", new value is \"$value\"")
}
}
companion object {
private const val STATISTICS_FOLDER_NAME = "kotlin-fus"
private const val BUILD_SESSION_SEPARATOR = "BUILD FINISHED"
}
}
@@ -0,0 +1,15 @@
/*
* Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle.fus.internal
internal data class Metric(val name: String, val projectHash: String?) : Comparable<Metric> {
override fun compareTo(other: Metric) = compareValuesBy(this, other, { it.name }, { it.projectHash })
override fun toString(): String {
val suffix = if (projectHash == null) "" else ".${projectHash}"
return name + suffix
}
}
@@ -320,6 +320,7 @@ tasks.withType<Test> {
dependsOn(":gradle:android-test-fixes:install")
dependsOn(":gradle:gradle-warnings-detector:install")
dependsOn(":gradle:kotlin-compiler-args-properties:install")
dependsOn(":libraries:tools:gradle:fus-statistics-gradle-plugin:install")
dependsOn(":examples:annotation-processor-example:install")
dependsOn(":kotlin-dom-api-compat:install")
if (project.kotlinBuildProperties.isTeamcityBuild) {
@@ -9,6 +9,7 @@ import org.gradle.api.logging.LogLevel
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
import org.junit.jupiter.api.DisplayName
import kotlin.io.path.pathString
@DisplayName("Build FUS statistics")
class BuildFusStatisticsIT : KGPDaemonsBaseTest() {
@@ -42,4 +43,87 @@ class BuildFusStatisticsIT : KGPDaemonsBaseTest() {
}
}
}
@DisplayName("smoke test for fus-statistics-gradle-plugin")
@GradleTest
fun smokeTestForFusStatisticsPlugin(gradleVersion: GradleVersion) {
val metricName = "METRIC_NAME"
val metricValue = 1
project("simpleProject", gradleVersion) {
buildGradle.modify {
"""
${applyFusStatisticPlugin(it)}
${createTestFusTaskClass()}
tasks.register("test-fus", TestFusTask.class).get().doLast {
fusStatisticsBuildService.get().reportMetric("$metricName", $metricValue, null)
}
""".trimIndent()
}
val reportRelativePath = "reports"
build("test-fus", "-Pkotlin.fus.statistics.path=${projectPath.resolve(reportRelativePath).pathString}") {
val fusReport = projectPath.getSingleFileInDir("$reportRelativePath/kotlin-fus")
assertFileContains(
fusReport,
"METRIC_NAME=1",
"BUILD FINISHED"
)
}
}
}
private fun applyFusStatisticPlugin(it: String) = it.replace(
"plugins {",
"""
plugins {
id "org.jetbrains.kotlin.fus-statistics-gradle-plugin" version "${'$'}kotlin_version"
""".trimIndent()
)
private fun createTestFusTaskClass() = """import org.jetbrains.kotlin.gradle.fus.GradleBuildFusStatisticsService
class TestFusTask extends DefaultTask implements org.jetbrains.kotlin.gradle.fus.UsesGradleBuildFusStatisticsService {
private Property<GradleBuildFusStatisticsService> fusStatisticsBuildService = project.objects.property(GradleBuildFusStatisticsService.class)
org.gradle.api.provider.Property getFusStatisticsBuildService(){
return fusStatisticsBuildService
}
}"""
@DisplayName("test override metrics for fus-statistics-gradle-plugin")
@GradleTest
fun testMetricsOverrideForFusStatisticsPlugin(gradleVersion: GradleVersion) {
val metricName = "METRIC_NAME"
val metricValue = 1
project("simpleProject", gradleVersion) {
buildGradle.modify {
"""
${applyFusStatisticPlugin(it)}
${createTestFusTaskClass()}
tasks.register("test-fus", TestFusTask.class).get().doLast {
fusStatisticsBuildService.get().reportMetric("$metricName", $metricValue, null)
}
tasks.register("test-fus-second", TestFusTask.class).get().doLast {
fusStatisticsBuildService.get().reportMetric("$metricName", 2, null)
}
""".trimIndent()
}
val reportRelativePath = "reports"
build("test-fus", "test-fus-second", "-Pkotlin.fus.statistics.path=${projectPath.resolve(reportRelativePath).pathString}") {
assertOutputContains("Try to override $metricName metric: current value is \"1\", new value is \"2\"")
val fusReport = projectPath.getSingleFileInDir("$reportRelativePath/kotlin-fus")
assertFileContains(
fusReport,
"METRIC_NAME=1",
"BUILD FINISHED"
)
}
}
}
}
+3 -2
View File
@@ -6,9 +6,10 @@ To reproduce locally build all artifacts first:
```shell
# clean local m2 from old artifacts to avoid unrelated failures
find ~/.m2/repository/org/jetbrains/kotlin -name "*-1.9.255*" -delete
# up-to-date version is in defaultSnapshotVersion property and should be used instead of "*-2.0.255*"
find ~/.m2/repository/org/jetbrains/kotlin -name "*-2.0.255*" -delete
./gradlew install
cd libraries
mvn install -DskipTests
./mvnw install -DskipTests
```
@@ -28,6 +28,7 @@ class ArtifactsTest {
"org.jetbrains.kotlin.gradle-subplugin-example.gradle.plugin",
"gradle-warnings-detector",
"kotlin-compiler-args-properties",
"fus-statistics-gradle-plugin",
"kotlin-gradle-plugin-tcs-android",
"kotlin-gradle-subplugin-example",
"kotlin-java-example",
@@ -35,6 +36,7 @@ class ArtifactsTest {
"org.jetbrains.kotlin.test.fixes.android.gradle.plugin",
"org.jetbrains.kotlin.test.gradle-warnings-detector.gradle.plugin",
"org.jetbrains.kotlin.test.kotlin-compiler-args-properties.gradle.plugin",
"org.jetbrains.kotlin.fus-statistics-gradle-plugin.gradle.plugin",
)
@Test
@@ -117,6 +117,7 @@ class CodeConformanceTest : TestCase() {
"libraries/tools/gradle/android-test-fixes/build",
"libraries/tools/gradle/gradle-warnings-detector/build",
"libraries/tools/gradle/kotlin-compiler-args-properties/build",
"libraries/tools/gradle/fus-statistics-gradle-plugin/build",
"libraries/tools/kotlin-allopen/build",
"libraries/tools/kotlin-assignment/build",
"libraries/tools/kotlin-gradle-build-metrics/build",
@@ -128,6 +128,7 @@ private val internalPlugins = setOf(
"android-test-fixes",
"gradle-warnings-detector",
"kotlin-compiler-args-properties",
"fus-statistics-gradle-plugin",
)
private val testPlugins = internalPlugins + setOf(
@@ -631,6 +632,7 @@ fun Project.addBomCheckTask() {
project(":gradle:kotlin-compiler-args-properties").path,
project(":kotlin-gradle-build-metrics").path,
project(":kotlin-gradle-statistics").path,
project(":libraries:tools:gradle:fus-statistics-gradle-plugin").path
)
val projectPath = this@addBomCheckTask.path
@@ -155,6 +155,7 @@ fun Project.configureKotlinCompilationOptions() {
":gradle:android-test-fixes",
":gradle:gradle-warnings-detector",
":gradle:kotlin-compiler-args-properties",
":libraries:tools:gradle:fus-statistics-gradle-plugin",
":js:js.config",
":kotlin-allopen",
":kotlin-assignment",
+1
View File
@@ -217,6 +217,7 @@ include ":kotlin-imports-dumper-compiler-plugin",
":gradle:kotlin-compiler-args-properties",
":gradle:regression-benchmark-templates",
":gradle:regression-benchmarks",
":libraries:tools:gradle:fus-statistics-gradle-plugin",
":kotlin-tooling-metadata",
":kotlin-tooling-core",
":kotlin-allopen",