/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectTelemetry/ProjectLoadTelemetryReporter.cs
183 строки
9 KB
David Barbet
Refactor LSP server logging to remove singleton lsp instance (#84001)
16 июн 2026, 05:04
Не верифицирован
16 июн 2026, 05:04
82e81e5
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Immutable; using System.Composition; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; using Microsoft.Extensions.Logging; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; [ExportCSharpVisualBasicLspServiceFactory(typeof(ProjectLoadTelemetryReporter)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class ProjectLoadTelemetryReporterFactory(ServerConfiguration serverConfiguration) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { return new ProjectLoadTelemetryReporter(lspServices.GetRequiredService<IClientLanguageServerManager>(), lspServices.GetRequiredService<ILoggerFactory>(), serverConfiguration); } } internal sealed class ProjectLoadTelemetryReporter(IClientLanguageServerManager clientLanguageServerManager, ILoggerFactory loggerFactory, ServerConfiguration serverConfiguration) : ILspService { private static readonly string s_hashedSessionId = VsTfmAndFileExtHashingAlgorithm.HashInput(Guid.NewGuid().ToString()); private readonly ILogger _logger = loggerFactory.CreateLogger<ProjectLoadTelemetryReporter>(); public sealed record TelemetryInfo { public ImmutableArray<CommandLineReference> MetadataReferences { get; init; } public OutputKind OutputKind { get; init; } public bool IsSdkStyle { get; init; } public bool HasSolutionFile { get; init; } public bool IsFileBasedProgram { get; init; } public bool HasFileBasedAppDirectives { get; init; } public bool IsMiscellaneousFile { get; init; } } /// <summary> /// This is designed to report project telemetry in an extremely similar way to O# /// so that we are able to compare data accurately. /// See https://github.com/OmniSharp/omnisharp-roslyn/blob/b2e64c6006beed49460f063117793f42ab2a8a5c/src/OmniSharp.MSBuild/ProjectLoadListener.cs#L36 /// </summary> public async Task ReportProjectLoadTelemetryAsync(Dictionary<ProjectFileInfo, TelemetryInfo> projectFileInfos, ProjectToLoad projectToLoad, CancellationToken cancellationToken) { try { if (serverConfiguration.TelemetryLevel is null or "off") { return; } if (!projectFileInfos.Any()) { return; } // Arbitrarily pick the first. This is an existing problem with the telemetry event where we report multiple target frameworks // but only the data from one of the sets of possible outputkinds / references / content / etc. var firstInfo = projectFileInfos.First(); var projectFileInfo = firstInfo.Key; var telemetryInfo = firstInfo.Value; // Matches O# behavior to not report this event if no references found. if (!telemetryInfo.MetadataReferences.Any()) { return; } var projectId = await GetProjectIdAsync(projectToLoad, projectFileInfo); var targetFrameworks = GetTargetFrameworks(projectFileInfos.Keys); var projectCapabilities = projectFileInfo.ProjectCapabilities; var hashedReferences = GetHashedReferences(telemetryInfo.MetadataReferences); var fileCounts = GetUniqueHashedFileExtensionsAndCounts(projectFileInfo); var isSdkStyleProject = telemetryInfo.IsSdkStyle; var projectEvent = new ProjectLoadTelemetryEvent( ProjectId: projectId, SessionId: s_hashedSessionId, OutputKind: (int)telemetryInfo.OutputKind, ProjectCapabilities: projectCapabilities, TargetFrameworks: targetFrameworks, References: hashedReferences, FileExtensions: fileCounts.Keys, FileCounts: fileCounts.Values, SdkStyleProject: isSdkStyleProject, HasSolutionFile: telemetryInfo.HasSolutionFile, IsFileBasedProgram: telemetryInfo.IsFileBasedProgram, HasFileBasedAppDirectives: telemetryInfo.HasFileBasedAppDirectives, IsMiscellaneousFile: telemetryInfo.IsMiscellaneousFile); await ReportEventAsync(projectEvent, cancellationToken); } catch (Exception ex) { // Don't fail project loading because we failed to report telemetry. Just log a warning and move on. _logger.LogWarning($"Failed to get project telemetry data: {ex.ToString()}"); } } private async Task ReportEventAsync(ProjectLoadTelemetryEvent telemetryEvent, CancellationToken cancellationToken) { await clientLanguageServerManager.SendNotificationAsync("workspace/projectConfigurationTelemetry", telemetryEvent, cancellationToken); } private static ImmutableDictionary<string, int> GetUniqueHashedFileExtensionsAndCounts(ProjectFileInfo projectFileInfo) { // Similar to O#, we report the content files + any non-generated source files. var contentFiles = projectFileInfo.ContentFilePaths; var sourceFiles = projectFileInfo.Documents .Concat(projectFileInfo.AdditionalDocuments) .Concat(projectFileInfo.AnalyzerConfigDocuments) .SelectAsArray(d => !d.IsGenerated, d => d.FilePath); var allFiles = contentFiles.Concat(sourceFiles); var fileCounts = new Dictionary<string, int>(); foreach (var file in allFiles) { var fileExtension = Path.GetExtension(file); fileCounts[fileExtension] = fileCounts.GetOrAdd(fileExtension, 0) + 1; } return fileCounts.ToImmutableDictionary(kvp => VsTfmAndFileExtHashingAlgorithm.HashInput(kvp.Key), kvp => kvp.Value); } private static ImmutableArray<string> GetHashedReferences(ImmutableArray<CommandLineReference> metadataReferences) { return metadataReferences.SelectAsArray(GetHashedReferenceName); static string GetHashedReferenceName(CommandLineReference reference) { var lowerCaseName = Path.GetFileNameWithoutExtension(reference.Reference).ToLower(); return VsReferenceHashingAlgorithm.HashInput(lowerCaseName); } } /// <summary> /// This reads the solution file project id or hashes the contents+path /// Matches O# implementation - https://github.com/OmniSharp/omnisharp-roslyn/blob/master/src/OmniSharp.MSBuild/ProjectLoadListener.cs#L88 /// </summary> private static async Task<string> GetProjectIdAsync(ProjectToLoad projectToLoad, ProjectFileInfo projectFileInfo) { if (projectToLoad.ProjectGuid is not null) { // The projectId is formatted as {GUID}. // In order to match with O#, we need just the guid. var projectGuid = projectToLoad.ProjectGuid.Replace("{", string.Empty).Replace("}", string.Empty); // No need to actually hash the project guid. return projectGuid; } var projectPath = projectToLoad.Path; // The projectToLoad.Path might be a virtual document (misc file), attempt to fall back to the actual file path from the loaded project info. if (!File.Exists(projectPath) && projectFileInfo.FilePath is not null) { projectPath = projectFileInfo.FilePath; } if (!File.Exists(projectPath)) { // We have no file path at all and so cannot compute a contents hash, just return a sentinel value. return "MISSING"; } var content = await File.ReadAllTextAsync(projectPath); // This should exactly match O# to ensure we get the same hashes. return VsReferenceHashingAlgorithm.HashInput($"Filename: {Path.GetFileName(projectToLoad.Path)}\n{content}"); } private static ImmutableArray<string> GetTargetFrameworks(IEnumerable<ProjectFileInfo> projectFileInfos) { return [.. projectFileInfos.Select(p => GetTargetFramework(p)?.ToLower()).WhereNotNull()]; string? GetTargetFramework(ProjectFileInfo projectFileInfo) => projectFileInfo.TargetFramework ?? projectFileInfo.TargetFrameworkVersion; } }