Skip to content

Commit bc95823

Browse files
authored
fix(amazonq): add metrics for flare/node resolution (#5786)
We need some visibility on how prevalent failures are
1 parent 060a132 commit bc95823

File tree

5 files changed

+148
-50
lines changed

5 files changed

+148
-50
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,12 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDoc
7777
import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
7878
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
7979
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
80+
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
8081
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
8182
import software.aws.toolkits.jetbrains.settings.LspSettings
8283
import software.aws.toolkits.jetbrains.utils.notifyInfo
8384
import software.aws.toolkits.resources.message
85+
import software.aws.toolkits.telemetry.Telemetry
8486
import java.io.IOException
8587
import java.io.OutputStreamWriter
8688
import java.io.PipedInputStream
@@ -526,20 +528,36 @@ private class AmazonQServerInstance(private val project: Project, private val cs
526528
* may fail to start in that case. The caller should handle potential runtime initialization failures.
527529
*/
528530
private fun getNodeRuntimePath(nodePath: Path): Path {
531+
val resolveNodeMetric = { isBundled: Boolean, success: Boolean ->
532+
Telemetry.languageserver.setup.use {
533+
it.id("q")
534+
it.metadata("languageServerSetupStage", "resolveNode")
535+
it.metadata("credentialStartUrl", getStartUrl(project))
536+
it.setAttribute("isBundledNode", isBundled)
537+
it.success(success)
538+
}
539+
}
540+
529541
if (Files.exists(nodePath) && Files.isExecutable(nodePath)) {
542+
resolveNodeMetric(true, true)
530543
return nodePath
531544
}
545+
532546
// use alternative node runtime if it is not found
533547
LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " }
534548
// attempt to use user provided node runtime path
535549
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
536550
if (!nodeRuntime.isNullOrEmpty()) {
537551
LOG.info { "Using node from $nodeRuntime " }
552+
553+
resolveNodeMetric(false, true)
538554
return Path.of(nodeRuntime)
539555
} else {
540556
val localNode = locateNodeCommand()
541557
if (localNode != null) {
542558
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }
559+
560+
resolveNodeMetric(false, true)
543561
return localNode
544562
}
545563
notifyInfo(
@@ -557,6 +575,8 @@ private class AmazonQServerInstance(private val project: Project, private val cs
557575
) { _, notification -> notification.expire() }
558576
)
559577
)
578+
579+
resolveNodeMetric(false, false)
560580
return nodePath
561581
}
562582
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import software.aws.toolkits.core.utils.getLogger
1717
import software.aws.toolkits.core.utils.info
1818
import software.aws.toolkits.core.utils.warn
1919
import software.aws.toolkits.jetbrains.core.saveFileFromUrl
20+
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
2021
import software.aws.toolkits.resources.AwsCoreBundle
22+
import software.aws.toolkits.telemetry.LanguageServerSetupStage
23+
import software.aws.toolkits.telemetry.Telemetry
2124
import java.nio.file.Files
2225
import java.nio.file.Path
2326
import java.nio.file.Paths
@@ -106,33 +109,33 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
106109
}
107110

108111
suspend fun tryDownloadLspArtifacts(project: Project, targetVersion: Version, target: VersionTarget): Path? {
109-
val temporaryDownloadPath = Files.createTempDirectory("lsp-dl")
110-
val downloadPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString())
112+
val destinationPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString())
111113

112114
while (currentAttempt.get() < maxDownloadAttempts) {
113115
currentAttempt.incrementAndGet()
114116
logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" }
117+
val temporaryDownloadPath = Files.createTempDirectory("lsp-dl")
115118

116119
try {
117120
return withBackgroundProgress(
118121
project,
119122
AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"),
120123
cancellable = true
121124
) {
122-
if (downloadLspArtifacts(temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) {
123-
moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath)
125+
if (downloadLspArtifacts(project, temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) {
126+
moveFilesFromSourceToDestination(temporaryDownloadPath, destinationPath)
124127
target.contents
125128
.mapNotNull { it.filename }
126-
.forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) }
127-
logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" }
129+
.forEach { filename -> extractZipFile(destinationPath.resolve(filename), destinationPath) }
130+
logger.info { "Successfully downloaded and moved LSP artifacts to $destinationPath" }
128131

129132
val thirdPartyLicenses = targetVersion.thirdPartyLicenses
130133
logger.info {
131-
"Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $downloadPath. " +
134+
"Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $destinationPath. " +
132135
if (thirdPartyLicenses == null) "" else "Attribution notice can be found at $thirdPartyLicenses"
133136
}
134137

135-
return@withBackgroundProgress downloadPath
138+
return@withBackgroundProgress destinationPath
136139
}
137140

138141
return@withBackgroundProgress null
@@ -146,15 +149,15 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
146149
else -> { logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" } }
147150
}
148151
temporaryDownloadPath.toFile().deleteRecursively()
149-
downloadPath.toFile().deleteRecursively()
152+
destinationPath.toFile().deleteRecursively()
150153
}
151154
}
152155
logger.error { "Failed to download LSP artifacts after $maxDownloadAttempts attempts" }
153156
return null
154157
}
155158

156159
@VisibleForTesting
157-
internal fun downloadLspArtifacts(downloadPath: Path, target: VersionTarget?): Boolean {
160+
internal fun downloadLspArtifacts(project: Project, downloadPath: Path, target: VersionTarget?): Boolean {
158161
if (target == null || target.contents.isNullOrEmpty()) {
159162
logger.warn { "No target contents available for download" }
160163
return false
@@ -171,7 +174,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
171174
logger.warn { "No hash available for ${content.filename}" }
172175
return@forEach
173176
}
174-
downloadAndValidateFile(content.url, filePath, contentHash)
177+
downloadAndValidateFile(project, content.url, filePath, contentHash)
175178
}
176179
validateDownloadedFiles(downloadPath, target.contents)
177180
} catch (e: Exception) {
@@ -182,18 +185,46 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
182185
return true
183186
}
184187

185-
private fun downloadAndValidateFile(url: String, filePath: Path, expectedHash: String) {
188+
private fun downloadAndValidateFile(project: Project, url: String, filePath: Path, expectedHash: String) {
189+
val recordDownload = { runnable: () -> Unit ->
190+
Telemetry.languageserver.setup.use { telemetry ->
191+
telemetry.id("q")
192+
telemetry.languageServerSetupStage(LanguageServerSetupStage.GetServer)
193+
telemetry.metadata("credentialStartUrl", getStartUrl(project))
194+
telemetry.success(true)
195+
196+
try {
197+
runnable()
198+
} catch (t: Throwable) {
199+
telemetry.success(false)
200+
telemetry.recordException(t)
201+
}
202+
}
203+
}
204+
186205
try {
187206
if (!filePath.exists()) {
188207
logger.info { "Downloading file: ${filePath.fileName}" }
189-
saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator)
208+
recordDownload { saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator) }
190209
}
191210
if (!validateFileHash(filePath, expectedHash)) {
192211
logger.warn { "Hash mismatch for ${filePath.fileName}, re-downloading" }
193212
filePath.deleteIfExists()
194-
saveFileFromUrl(url, filePath)
195-
if (!validateFileHash(filePath, expectedHash)) {
196-
throw LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH)
213+
recordDownload { saveFileFromUrl(url, filePath) }
214+
215+
Telemetry.languageserver.setup.use {
216+
it.id("q")
217+
it.languageServerSetupStage(LanguageServerSetupStage.Validate)
218+
it.metadata("credentialStartUrl", getStartUrl(project))
219+
it.success(true)
220+
221+
if (!validateFileHash(filePath, expectedHash)) {
222+
it.success(false)
223+
224+
val exception = LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH)
225+
it.recordException(exception)
226+
throw exception
227+
}
197228
}
198229
}
199230
} catch (e: Exception) {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import software.aws.toolkits.core.utils.info
1919
import software.aws.toolkits.core.utils.warn
2020
import software.aws.toolkits.jetbrains.AwsPlugin
2121
import software.aws.toolkits.jetbrains.AwsToolkit
22+
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
23+
import software.aws.toolkits.telemetry.LanguageServerSetupStage
24+
import software.aws.toolkits.telemetry.MetricResult
25+
import software.aws.toolkits.telemetry.Telemetry
2226
import java.nio.file.Path
2327

2428
@Service
@@ -57,42 +61,82 @@ class ArtifactManager @NonInjectable internal constructor(private val manifestFe
5761
return mutex.withLock {
5862
coroutineScope {
5963
async {
60-
try {
61-
val manifest = manifestFetcher.fetch() ?: throw LspException(
62-
"Language Support is not available, as manifest is missing.",
63-
LspException.ErrorCode.MANIFEST_FETCH_FAILED
64-
)
65-
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)
66-
67-
artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
68-
69-
if (lspVersions.inRangeVersions.isEmpty()) {
70-
// No versions are found which are in the given range. Fallback to local lsp artifacts.
71-
val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE)
72-
if (localLspArtifacts.isNotEmpty()) {
73-
return@async localLspArtifacts.first().first
64+
Telemetry.languageserver.setup.use { all ->
65+
all.id("q")
66+
all.languageServerSetupStage(LanguageServerSetupStage.All)
67+
all.metadata("credentialStartUrl", getStartUrl(project))
68+
all.result(MetricResult.Succeeded)
69+
70+
try {
71+
val lspVersions = Telemetry.languageserver.setup.use { telemetry ->
72+
telemetry.id("q")
73+
telemetry.languageServerSetupStage(LanguageServerSetupStage.GetManifest)
74+
telemetry.metadata("credentialStartUrl", getStartUrl(project))
75+
76+
val exception = LspException(
77+
"Language Support is not available, as manifest is missing.",
78+
LspException.ErrorCode.MANIFEST_FETCH_FAILED
79+
)
80+
telemetry.success(true)
81+
val manifest = manifestFetcher.fetch() ?: run {
82+
telemetry.recordException(exception)
83+
telemetry.success(false)
84+
throw exception
85+
}
86+
87+
getLSPVersionsFromManifestWithSpecifiedRange(manifest)
7488
}
75-
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
76-
}
7789

78-
val targetVersion = lspVersions.inRangeVersions.first()
90+
artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
91+
92+
if (lspVersions.inRangeVersions.isEmpty()) {
93+
// No versions are found which are in the given range. Fallback to local lsp artifacts.
94+
val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE)
95+
if (localLspArtifacts.isNotEmpty()) {
96+
return@async localLspArtifacts.first().first
97+
}
98+
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
99+
}
79100

80-
// If there is an LSP Manifest with the same version
81-
val target = getTargetFromLspManifest(targetVersion)
82-
// Get Local LSP files and check if we can re-use existing LSP Artifacts
83-
val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) {
84-
artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first
85-
} else {
86-
artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target)
87-
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
101+
val targetVersion = lspVersions.inRangeVersions.first()
102+
103+
// If there is an LSP Manifest with the same version
104+
val target = getTargetFromLspManifest(targetVersion)
105+
// Get Local LSP files and check if we can re-use existing LSP Artifacts
106+
val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) {
107+
artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first
108+
} else {
109+
artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target)
110+
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
111+
}
112+
113+
artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE)
114+
115+
Telemetry.languageserver.setup.use {
116+
it.id("q")
117+
it.languageServerSetupStage(LanguageServerSetupStage.Launch)
118+
it.metadata("credentialStartUrl", getStartUrl(project))
119+
it.setAttribute("isBundledArtifact", false)
120+
it.success(true)
121+
}
122+
return@async artifactPath
123+
} catch (e: Exception) {
124+
logger.warn(e) { "Failed to resolve assets from Flare CDN" }
125+
val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled")
126+
logger.info { "Falling back to bundled assets at $path" }
127+
128+
all.recordException(e)
129+
all.result(MetricResult.Failed)
130+
131+
Telemetry.languageserver.setup.use {
132+
it.id("q")
133+
it.languageServerSetupStage(LanguageServerSetupStage.Launch)
134+
it.metadata("credentialStartUrl", getStartUrl(project))
135+
it.setAttribute("isBundledArtifact", true)
136+
it.success(false)
137+
}
138+
return@async path
88139
}
89-
artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE)
90-
return@async artifactPath
91-
} catch (e: Exception) {
92-
logger.warn(e) { "Failed to resolve assets from Flare CDN" }
93-
val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled")
94-
logger.info { "Falling back to bundled assets at $path" }
95-
return@async path
96140
}
97141
}
98142
}.also {

plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class ArtifactHelperTest {
199199
val version = Version(serverVersion = "1.0.0")
200200

201201
val spyArtifactHelper = spyk(artifactHelper)
202-
every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns false
202+
every { spyArtifactHelper.downloadLspArtifacts(mockProject, any(), any()) } returns false
203203

204204
assertThat(runBlocking { artifactHelper.tryDownloadLspArtifacts(mockProject, version, VersionTarget(contents = contents)) }).isEqualTo(null)
205205
}
@@ -210,7 +210,7 @@ class ArtifactHelperTest {
210210
val target = VersionTarget(contents = contents)
211211
val spyArtifactHelper = spyk(artifactHelper)
212212

213-
every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns true
213+
every { spyArtifactHelper.downloadLspArtifacts(mockProject, any(), any()) } returns true
214214
mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt")
215215
every { moveFilesFromSourceToDestination(any(), any()) } just Runs
216216
every { extractZipFile(any(), any()) } just Runs

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ abstract class AbstractBaseSpan<SpanType : AbstractBaseSpan<SpanType>>(internal
201201

202202
override fun recordException(exception: Throwable): SpanType {
203203
delegate.recordException(exception)
204+
205+
setAttribute("reason", exception::class.java.canonicalName)
206+
setAttribute("reasonDesc", exception.message)
204207
return this as SpanType
205208
}
206209

0 commit comments

Comments
 (0)