diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index f5226359c55..28c28100f97 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -77,10 +77,12 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDoc import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.resources.message +import software.aws.toolkits.telemetry.Telemetry import java.io.IOException import java.io.OutputStreamWriter import java.io.PipedInputStream @@ -526,20 +528,36 @@ private class AmazonQServerInstance(private val project: Project, private val cs * may fail to start in that case. The caller should handle potential runtime initialization failures. */ private fun getNodeRuntimePath(nodePath: Path): Path { + val resolveNodeMetric = { isBundled: Boolean, success: Boolean -> + Telemetry.languageserver.setup.use { + it.id("q") + it.metadata("languageServerSetupStage", "resolveNode") + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledNode", isBundled) + it.success(success) + } + } + if (Files.exists(nodePath) && Files.isExecutable(nodePath)) { + resolveNodeMetric(true, true) return nodePath } + // use alternative node runtime if it is not found LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " } // attempt to use user provided node runtime path val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() if (!nodeRuntime.isNullOrEmpty()) { LOG.info { "Using node from $nodeRuntime " } + + resolveNodeMetric(false, true) return Path.of(nodeRuntime) } else { val localNode = locateNodeCommand() if (localNode != null) { LOG.info { "Using node from ${localNode.toAbsolutePath()}" } + + resolveNodeMetric(false, true) return localNode } notifyInfo( @@ -557,6 +575,8 @@ private class AmazonQServerInstance(private val project: Project, private val cs ) { _, notification -> notification.expire() } ) ) + + resolveNodeMetric(false, false) return nodePath } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt index 97f2db11fed..dc8d25235dc 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt @@ -17,7 +17,10 @@ import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.saveFileFromUrl +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.resources.AwsCoreBundle +import software.aws.toolkits.telemetry.LanguageServerSetupStage +import software.aws.toolkits.telemetry.Telemetry import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -106,12 +109,12 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, } suspend fun tryDownloadLspArtifacts(project: Project, targetVersion: Version, target: VersionTarget): Path? { - val temporaryDownloadPath = Files.createTempDirectory("lsp-dl") - val downloadPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString()) + val destinationPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString()) while (currentAttempt.get() < maxDownloadAttempts) { currentAttempt.incrementAndGet() logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" } + val temporaryDownloadPath = Files.createTempDirectory("lsp-dl") try { return withBackgroundProgress( @@ -119,20 +122,20 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"), cancellable = true ) { - if (downloadLspArtifacts(temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) { - moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath) + if (downloadLspArtifacts(project, temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) { + moveFilesFromSourceToDestination(temporaryDownloadPath, destinationPath) target.contents .mapNotNull { it.filename } - .forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) } - logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" } + .forEach { filename -> extractZipFile(destinationPath.resolve(filename), destinationPath) } + logger.info { "Successfully downloaded and moved LSP artifacts to $destinationPath" } val thirdPartyLicenses = targetVersion.thirdPartyLicenses logger.info { - "Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $downloadPath. " + + "Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $destinationPath. " + if (thirdPartyLicenses == null) "" else "Attribution notice can be found at $thirdPartyLicenses" } - return@withBackgroundProgress downloadPath + return@withBackgroundProgress destinationPath } return@withBackgroundProgress null @@ -146,7 +149,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, else -> { logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" } } } temporaryDownloadPath.toFile().deleteRecursively() - downloadPath.toFile().deleteRecursively() + destinationPath.toFile().deleteRecursively() } } logger.error { "Failed to download LSP artifacts after $maxDownloadAttempts attempts" } @@ -154,7 +157,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, } @VisibleForTesting - internal fun downloadLspArtifacts(downloadPath: Path, target: VersionTarget?): Boolean { + internal fun downloadLspArtifacts(project: Project, downloadPath: Path, target: VersionTarget?): Boolean { if (target == null || target.contents.isNullOrEmpty()) { logger.warn { "No target contents available for download" } return false @@ -171,7 +174,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, logger.warn { "No hash available for ${content.filename}" } return@forEach } - downloadAndValidateFile(content.url, filePath, contentHash) + downloadAndValidateFile(project, content.url, filePath, contentHash) } validateDownloadedFiles(downloadPath, target.contents) } catch (e: Exception) { @@ -182,18 +185,46 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, return true } - private fun downloadAndValidateFile(url: String, filePath: Path, expectedHash: String) { + private fun downloadAndValidateFile(project: Project, url: String, filePath: Path, expectedHash: String) { + val recordDownload = { runnable: () -> Unit -> + Telemetry.languageserver.setup.use { telemetry -> + telemetry.id("q") + telemetry.languageServerSetupStage(LanguageServerSetupStage.GetServer) + telemetry.metadata("credentialStartUrl", getStartUrl(project)) + telemetry.success(true) + + try { + runnable() + } catch (t: Throwable) { + telemetry.success(false) + telemetry.recordException(t) + } + } + } + try { if (!filePath.exists()) { logger.info { "Downloading file: ${filePath.fileName}" } - saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator) + recordDownload { saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator) } } if (!validateFileHash(filePath, expectedHash)) { logger.warn { "Hash mismatch for ${filePath.fileName}, re-downloading" } filePath.deleteIfExists() - saveFileFromUrl(url, filePath) - if (!validateFileHash(filePath, expectedHash)) { - throw LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH) + recordDownload { saveFileFromUrl(url, filePath) } + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Validate) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.success(true) + + if (!validateFileHash(filePath, expectedHash)) { + it.success(false) + + val exception = LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH) + it.recordException(exception) + throw exception + } } } } catch (e: Exception) { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt index b0a4d8e7e9a..d7970612854 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt @@ -19,6 +19,10 @@ import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.AwsPlugin import software.aws.toolkits.jetbrains.AwsToolkit +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl +import software.aws.toolkits.telemetry.LanguageServerSetupStage +import software.aws.toolkits.telemetry.MetricResult +import software.aws.toolkits.telemetry.Telemetry import java.nio.file.Path @Service @@ -57,42 +61,82 @@ class ArtifactManager @NonInjectable internal constructor(private val manifestFe return mutex.withLock { coroutineScope { async { - try { - val manifest = manifestFetcher.fetch() ?: throw LspException( - "Language Support is not available, as manifest is missing.", - LspException.ErrorCode.MANIFEST_FETCH_FAILED - ) - val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest) - - artifactHelper.removeDelistedVersions(lspVersions.deListedVersions) - - if (lspVersions.inRangeVersions.isEmpty()) { - // No versions are found which are in the given range. Fallback to local lsp artifacts. - val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE) - if (localLspArtifacts.isNotEmpty()) { - return@async localLspArtifacts.first().first + Telemetry.languageserver.setup.use { all -> + all.id("q") + all.languageServerSetupStage(LanguageServerSetupStage.All) + all.metadata("credentialStartUrl", getStartUrl(project)) + all.result(MetricResult.Succeeded) + + try { + val lspVersions = Telemetry.languageserver.setup.use { telemetry -> + telemetry.id("q") + telemetry.languageServerSetupStage(LanguageServerSetupStage.GetManifest) + telemetry.metadata("credentialStartUrl", getStartUrl(project)) + + val exception = LspException( + "Language Support is not available, as manifest is missing.", + LspException.ErrorCode.MANIFEST_FETCH_FAILED + ) + telemetry.success(true) + val manifest = manifestFetcher.fetch() ?: run { + telemetry.recordException(exception) + telemetry.success(false) + throw exception + } + + getLSPVersionsFromManifestWithSpecifiedRange(manifest) } - throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) - } - val targetVersion = lspVersions.inRangeVersions.first() + artifactHelper.removeDelistedVersions(lspVersions.deListedVersions) + + if (lspVersions.inRangeVersions.isEmpty()) { + // No versions are found which are in the given range. Fallback to local lsp artifacts. + val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE) + if (localLspArtifacts.isNotEmpty()) { + return@async localLspArtifacts.first().first + } + throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) + } - // If there is an LSP Manifest with the same version - val target = getTargetFromLspManifest(targetVersion) - // Get Local LSP files and check if we can re-use existing LSP Artifacts - val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) { - artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first - } else { - artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target) - ?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED) + val targetVersion = lspVersions.inRangeVersions.first() + + // If there is an LSP Manifest with the same version + val target = getTargetFromLspManifest(targetVersion) + // Get Local LSP files and check if we can re-use existing LSP Artifacts + val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) { + artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first + } else { + artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target) + ?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED) + } + + artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE) + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Launch) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledArtifact", false) + it.success(true) + } + return@async artifactPath + } catch (e: Exception) { + logger.warn(e) { "Failed to resolve assets from Flare CDN" } + val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled") + logger.info { "Falling back to bundled assets at $path" } + + all.recordException(e) + all.result(MetricResult.Failed) + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Launch) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledArtifact", true) + it.success(false) + } + return@async path } - artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE) - return@async artifactPath - } catch (e: Exception) { - logger.warn(e) { "Failed to resolve assets from Flare CDN" } - val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled") - logger.info { "Falling back to bundled assets at $path" } - return@async path } } }.also { diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt index 367e2a31a97..72f3961fb58 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt @@ -199,7 +199,7 @@ class ArtifactHelperTest { val version = Version(serverVersion = "1.0.0") val spyArtifactHelper = spyk(artifactHelper) - every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns false + every { spyArtifactHelper.downloadLspArtifacts(mockProject, any(), any()) } returns false assertThat(runBlocking { artifactHelper.tryDownloadLspArtifacts(mockProject, version, VersionTarget(contents = contents)) }).isEqualTo(null) } @@ -210,7 +210,7 @@ class ArtifactHelperTest { val target = VersionTarget(contents = contents) val spyArtifactHelper = spyk(artifactHelper) - every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns true + every { spyArtifactHelper.downloadLspArtifacts(mockProject, any(), any()) } returns true mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt") every { moveFilesFromSourceToDestination(any(), any()) } just Runs every { extractZipFile(any(), any()) } just Runs diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 623663d85db..22cf1231e9b 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -201,6 +201,9 @@ abstract class AbstractBaseSpan>(internal override fun recordException(exception: Throwable): SpanType { delegate.recordException(exception) + + setAttribute("reason", exception::class.java.canonicalName) + setAttribute("reasonDesc", exception.message) return this as SpanType }