Skip to content

Commit

Permalink
chore: clean up observability
Browse files Browse the repository at this point in the history
  • Loading branch information
kmruiz committed May 7, 2024
1 parent cca3183 commit d8b2205
Show file tree
Hide file tree
Showing 11 changed files with 183 additions and 51 deletions.
6 changes: 5 additions & 1 deletion gradle/diktat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
- name: MISSING_KDOC_ON_FUNCTION
enabled: false
- name: GENERIC_VARIABLE_WRONG_DECLARATION
enabled: false
enabled: false
- name: TOO_MANY_PARAMETERS
enabled: true
configuration:
maxParameterListSize: '10'
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ package com.mongodb.jbplugin.observability

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.PermanentInstallationID
import com.intellij.openapi.components.Service
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.project.Project

/**
* @param gson
Expand All @@ -42,30 +40,25 @@ internal class LogMessageBuilder(private val gson: Gson, message: String) {
* log.info(logMessage?.message("My message").put("someOtherProp", 25).build())
* }
* ```
*
* @param project
*/
@Service
internal class LogMessage {
internal class LogMessage(private val project: Project) {
private val gson = GsonBuilder().generateNonExecutableJson().disableJdkUnsafe().create()

fun message(key: String): LogMessageBuilder {
val userId = getOrDefault("<userId>") { PermanentInstallationID.get() }
val osName = getOrDefault("<osName>") { SystemInfo.getOsNameAndVersion() }
val arch = getOrDefault("<arch>") { SystemInfo.OS_ARCH }
val jvmVendor = getOrDefault("<jvmVendor>") { SystemInfo.JAVA_VENDOR }
val jvmVersion = getOrDefault("<jvmVersion>") { SystemInfo.JAVA_VERSION }
val buildVersion = getOrDefault("<fullVersion>") { ApplicationInfo.getInstance().fullVersion }
val applicationName = getOrDefault("<fullApplicationName>") {
ApplicationInfo.getInstance().fullApplicationName
}
val runtimeInformationService = project.getService(RuntimeInformationService::class.java)
val runtimeInformation = runtimeInformationService.get()

return LogMessageBuilder(gson, key)
.put("userId", userId)
.put("os", osName)
.put("arch", arch)
.put("jvmVendor", jvmVendor)
.put("jvmVersion", jvmVersion)
.put("buildVersion", buildVersion)
.put("ide", applicationName)
.put("userId", runtimeInformation.userId)
.put("os", runtimeInformation.osName)
.put("arch", runtimeInformation.arch)
.put("jvmVendor", runtimeInformation.jvmVendor)
.put("jvmVersion", runtimeInformation.jvmVersion)
.put("buildVersion", runtimeInformation.buildVersion)
.put("ide", runtimeInformation.applicationName)
}

private fun <T> getOrDefault(default: T, supplier: () -> T): T {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Contains all runtime information relevant for observability.
*/

package com.mongodb.jbplugin.observability

import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.PermanentInstallationID
import com.intellij.openapi.components.Service
import com.intellij.openapi.util.SystemInfo

/**
* Represents all the gathered information from the host machine.
*
* @property userId
* @property osName
* @property arch
* @property jvmVendor
* @property jvmVersion
* @property buildVersion
* @property applicationName
*/
data class RuntimeInformation(
val userId: String,
val osName: String,
val arch: String,
val jvmVendor: String,
val jvmVersion: String,
val buildVersion: String,
val applicationName: String,
)

/**
* Computes, if possible, the current runtime information. It provides a method that
* returns a RuntimeInformation object.
*
* Do not use RuntimeInformation for feature toggling.
*
* @see RuntimeInformation
*/
@Service
class RuntimeInformationService {
private val userId = getOrDefault("<userId>") { PermanentInstallationID.get() }
private val osName = getOrDefault("<osName>") { SystemInfo.getOsNameAndVersion() }
private val arch = getOrDefault("<arch>") { SystemInfo.OS_ARCH }
private val jvmVendor = getOrDefault("<jvmVendor>") { SystemInfo.JAVA_VENDOR }
private val jvmVersion = getOrDefault("<jvmVersion>") { SystemInfo.JAVA_VERSION }
private val buildVersion = getOrDefault("<fullVersion>") { ApplicationInfo.getInstance().fullVersion }
private val applicationName = getOrDefault("<fullApplicationName>") {
ApplicationInfo.getInstance().fullApplicationName
}

fun get(): RuntimeInformation = RuntimeInformation(
userId,
osName,
arch,
jvmVendor,
jvmVersion,
buildVersion,
applicationName
)

private fun <T> getOrDefault(default: T, supplier: () -> T): T {
return try {
supplier()
} catch (ex: Throwable) {
return default
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,15 @@ internal enum class TelemetryProperty(val publicName: String)
* TelemetryService.
*
* @property name Name of the event
* @property userId Identifier of the user emitting an event
* @property properties Map of fields sent to Segment.
* @see TelemetryService
*/
internal sealed class TelemetryEvent(
internal val name: String,
internal val userId: String,
internal val properties: Map<TelemetryProperty, Any>
) {
/**
* @property jbId
*/
internal data class PluginActivated(val jbId: String) : TelemetryEvent(
internal data object PluginActivated : TelemetryEvent(
name = "plugin-activated",
userId = jbId,
properties = emptyMap()
)
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package com.mongodb.jbplugin.observability

import com.intellij.openapi.components.Service
import com.intellij.openapi.project.Project
import com.segment.analytics.Analytics
import com.segment.analytics.messages.IdentifyMessage
import com.segment.analytics.messages.TrackMessage

/**
* This telemetry service is used to send events to Segment. Should be used within
* probes, no directly. That is why it's marked as internal.
*
* @param project
*/
@Service
internal class TelemetryService {
internal class TelemetryService(private val project: Project) {
internal var analytics: Analytics = Analytics.builder("KEY").build()

fun sendEvent(event: TelemetryEvent) {
val runtimeInformationService = project.getService(RuntimeInformationService::class.java)
val runtimeInfo = runtimeInformationService.get()

val message = when (event) {
is TelemetryEvent.PluginActivated -> IdentifyMessage.builder().userId(event.userId) else ->
TrackMessage.builder(event.name).userId(event.userId)
is TelemetryEvent.PluginActivated -> IdentifyMessage.builder().userId(runtimeInfo.userId)
else ->
TrackMessage.builder(event.name).userId(runtimeInfo.userId)
.properties(event.properties.entries.associate {
it.key.publicName to it.value
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mongodb.jbplugin.observability.probe

import com.intellij.openapi.application.PermanentInstallationID
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
Expand All @@ -12,18 +11,17 @@ import com.mongodb.jbplugin.observability.TelemetryService
private val logger: Logger = logger<PluginActivatedProbe>()

/**
* @param project
* This probe is emitted when the plugin is activated (started).
*
* @param project Project where the plugin is set up
*/
@Service
class PluginActivatedProbe(private val project: Project) {
fun pluginActivated() {
val telemetry = project.getService(TelemetryService::class.java)
val logMessage = project.getService(LogMessage::class.java)

val userId = PermanentInstallationID.get()
telemetry.sendEvent(
TelemetryEvent.PluginActivated(userId)
)
telemetry.sendEvent(TelemetryEvent.PluginActivated)

logger.info(
logMessage.message("Plugin activated.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@

package com.mongodb.jbplugin

import com.google.gson.Gson
import com.intellij.openapi.project.Project
import com.mongodb.jbplugin.observability.LogMessage
import com.mongodb.jbplugin.observability.LogMessageBuilder
import com.mongodb.jbplugin.observability.RuntimeInformation
import com.mongodb.jbplugin.observability.RuntimeInformationService
import com.mongodb.jbplugin.observability.TelemetryService
import com.mongodb.jbplugin.observability.probe.PluginActivatedProbe
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.mock

/**
Expand All @@ -20,17 +25,78 @@ import org.mockito.kotlin.mock
* @param telemetryService
* @param pluginActivatedProbe
* @param logMessage
* @param runtimeInformationService
* @return A mock project to be used in dependency injection.
*/
internal fun mockProject(
telemetryService: TelemetryService = mock<TelemetryService>(),
runtimeInformationService: RuntimeInformationService = mockRuntimeInformationService(),
pluginActivatedProbe: PluginActivatedProbe = mock<PluginActivatedProbe>(),
logMessage: LogMessage = LogMessage(),
logMessage: LogMessage = mockLogMessage(),
): Project {
val project = mock<Project>()
`when`(project.getService(TelemetryService::class.java)).thenReturn(telemetryService)
`when`(project.getService(RuntimeInformationService::class.java)).thenReturn(runtimeInformationService)
`when`(project.getService(PluginActivatedProbe::class.java)).thenReturn(pluginActivatedProbe)
`when`(project.getService(LogMessage::class.java)).thenReturn(logMessage)

return project
}

/**
* Generates a mock runtime information service, useful for testing. If you need
* to create your own. You'll likely will build first an information service and
* then inject it into a mock project, something like this:
*
* ```kt
* val myInfoService = mockRuntimeInformationService(userId = "hey")
* val myProject = mockProject(runtimeInformationService = myInfoService)
* ```
*
* @param userId
* @param osName
* @param arch
* @param jvmVendor
* @param jvmVersion
* @param buildVersion
* @param applicationName
* @return A new mocked RuntimeInformationService
*/
internal fun mockRuntimeInformationService(
userId: String = "123456",
osName: String = "Winux OSX",
arch: String = "x128",
jvmVendor: String = "Obelisk",
jvmVersion: String = "42",
buildVersion: String = "2024.2",
applicationName: String = "Cool IDE"
): RuntimeInformationService = mock<RuntimeInformationService>().also { service ->
`when`(service.get()).thenReturn(
RuntimeInformation(
userId = userId,
osName = osName,
arch = arch,
jvmVendor = jvmVendor,
jvmVersion = jvmVersion,
buildVersion = buildVersion,
applicationName = applicationName
))
}

/**
* Generates a mock log message service.
* You'll likely will build first a log message service and
* then inject it into a mock project, something like this:
*
* ```kt
* val myLogMessage = mockLogMessage()
* val myProject = mockProject(logMessage = myLogMessage)
* ```
*
* @return A new mocked LogMessage
*/
internal fun mockLogMessage(): LogMessage = mock<LogMessage>().also { logMessage ->
`when`(logMessage.message(any())).then { message ->
LogMessageBuilder(Gson(), message.arguments[0].toString())
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.mongodb.jbplugin.observability

import com.google.gson.Gson
import com.mongodb.jbplugin.mockProject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

Expand All @@ -9,15 +10,15 @@ class LogMessageTest {

@Test
fun `should serialize a log message to json`() {
val message = LogMessage().message("My Message").build()
val message = LogMessage(mockProject()).message("My Message").build()
val parsedMessage = gson.fromJson<Map<String, Any>>(message, Map::class.java)

assertEquals("My Message", parsedMessage["message"])
}

@Test
fun `should serialize a log message to json with additional fields`() {
val message = LogMessage()
val message = LogMessage(mockProject())
.message("My Message")
.put("jetbrainsId", "someId")
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import org.junit.jupiter.api.Test
internal class TelemetryEventTest {
@Test
fun `PluginActivated is mapped correctly`() {
val pluginActivated = TelemetryEvent.PluginActivated("myUserId")
assertEquals("myUserId", pluginActivated.userId)
val pluginActivated = TelemetryEvent.PluginActivated
assertEquals(mapOf<TelemetryProperty, Any>(), pluginActivated.properties)
assertEquals("plugin-activated", pluginActivated.name)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.mongodb.jbplugin.observability

import com.mongodb.jbplugin.mockProject
import com.mongodb.jbplugin.mockRuntimeInformationService
import com.segment.analytics.Analytics
import org.junit.jupiter.api.Test
import org.mockito.kotlin.argThat
Expand All @@ -9,16 +11,19 @@ import org.mockito.kotlin.verify
internal class TelemetryServiceTest {
@Test
fun `sends an identify event when a PluginActivated event is sent`() {
val service = TelemetryService().apply {
val mockRuntimeInfo = mockRuntimeInformationService(userId = "654321")
val service = TelemetryService(mockProject(
runtimeInformationService = mockRuntimeInfo
)).apply {
analytics = mock<Analytics>()
}

service.sendEvent(TelemetryEvent.PluginActivated("myUserId"))
service.sendEvent(TelemetryEvent.PluginActivated)

verify(service.analytics).enqueue(
argThat {
build().let {
it.userId() == "myUserId" &&
it.userId() == "654321" &&
it.type().name == "identify"
}
}
Expand Down
Loading

0 comments on commit d8b2205

Please sign in to comment.