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
}
}