diff --git a/servers/Azure.Mcp.Server/changelog-entries/1773958582020.yaml b/servers/Azure.Mcp.Server/changelog-entries/1773958582020.yaml new file mode 100644 index 0000000000..edc459277a --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/1773958582020.yaml @@ -0,0 +1,4 @@ +pr: 2115 +changes: + - section: "Features Added" + description: "Azure Monitor Instrumentation in the tool now supports guided onboarding for .NET, Node.js, and Python with framework-aware recommendations and code/package actions. Automated guidance currently includes .NET (ASP.NET Core, ASP.NET classic including MVC/WebForms, Worker Service, plus console/library migration paths), Node.js (Express, Fastify, NestJS, Next.js, LangChain.js, Postgres, MongoDB, Redis, MySQL, Winston, Bunyan, and console apps), and Python (Django, Flask, FastAPI, Falcon, Starlette, GenAI, and generic console/script apps). .NET also supports brownfield migration guidance for Application Insights SDK to 3.x and enhancement flows for projects already on Application Insights 3.x or Azure Monitor Distro." \ No newline at end of file diff --git a/servers/Azure.Mcp.Server/docs/azmcp-commands.md b/servers/Azure.Mcp.Server/docs/azmcp-commands.md index 0455eeeda7..6a1cd06130 100644 --- a/servers/Azure.Mcp.Server/docs/azmcp-commands.md +++ b/servers/Azure.Mcp.Server/docs/azmcp-commands.md @@ -2509,11 +2509,16 @@ azmcp monitor instrumentation orchestrator_next --session-id \ # Send brownfield analysis findings JSON to continue migration flow azmcp monitor instrumentation send_brownfield_analysis --session-id \ --findings-json + +# Submit enhancement selection when orchestrator_start returns enhancement_available +azmcp monitor instrumentation send_enhanced_selection --session-id \ + --enhancement-keys ``` **Notes:** - `orchestrator_start` and `orchestrator_next` mirror the orchestration flow used by Azure Monitor onboarding. - `send_brownfield_analysis` expects a JSON payload matching the `analysisTemplate` returned by `orchestrator_start` when status is `analysis_needed`. +- `send_enhanced_selection` expects one or more enhancement keys from `enhancementOptions` returned by `orchestrator_start` when status is `enhancement_available`. ### Azure Managed Lustre Operations diff --git a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md index a19be87ac4..51ba454186 100644 --- a/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md +++ b/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md @@ -660,6 +660,24 @@ This file contains prompts used for end-to-end testing to ensure each tool is in |:----------|:----------| | monitor_activitylog_list | List the activity logs of the last month for | | monitor_healthmodels_entity_get | Show me the health status of entity using the health model | +| monitor_instrumentation_get_learning_resource | Get the onboarding learning resource at path | +| monitor_instrumentation_get_learning_resource | Show me the content of the Azure Monitor onboarding learning resource at path | +| monitor_instrumentation_get_learning_resource | Get the content of the Azure Monitor learning resource file at path | +| monitor_instrumentation_get_learning_resource | List all available Azure Monitor onboarding learning resources | +| monitor_instrumentation_get_learning_resource | Show me all learning resource paths for Azure Monitor instrumentation | +| monitor_instrumentation_get_learning_resource | What learning resources are available for Azure Monitor instrumentation onboarding? | +| monitor_instrumentation_orchestrator_next | After completing the previous Azure Monitor instrumentation step, get the next action for session with completion note | +| monitor_instrumentation_orchestrator_next | Get the next onboarding action using session after I completed | +| monitor_instrumentation_orchestrator_next | I finished the previous instrumentation step; return the next step for session with note | +| monitor_instrumentation_orchestrator_start | Start Azure Monitor instrumentation orchestration for workspace | +| monitor_instrumentation_orchestrator_start | Analyze workspace and return the first Azure Monitor instrumentation step | +| monitor_instrumentation_orchestrator_start | Begin guided Azure Monitor onboarding for project at and give me step one | +| monitor_instrumentation_send_brownfield_analysis | Send brownfield code analysis findings JSON to Azure Monitor instrumentation session after analysis was requested | +| monitor_instrumentation_send_brownfield_analysis | Continue migration orchestration by submitting analysis payload to session | +| monitor_instrumentation_send_brownfield_analysis | Send completed brownfield telemetry analysis for onboarding session | +| monitor_instrumentation_send_enhanced_selection | Submit enhancement selection keys for Azure Monitor instrumentation session after enhancement options are presented | +| monitor_instrumentation_send_enhanced_selection | Continue instrumentation enhancement flow by sending selected keys to session | +| monitor_instrumentation_send_enhanced_selection | Send chosen enhancement option key list for onboarding session | | monitor_metrics_definitions | Get metric definitions for from the namespace | | monitor_metrics_definitions | Show me all available metrics and their definitions for storage account | | monitor_metrics_definitions | What metric definitions are available for the Application Insights resource | @@ -683,21 +701,6 @@ This file contains prompts used for end-to-end testing to ensure each tool is in | monitor_workspace_list | Show me my Log Analytics workspaces | | monitor_workspace_list | Show me the Log Analytics workspaces in my subscription | | monitor_workspace_log_query | Show me the logs for the past hour in the Log Analytics workspace | -| monitor_instrumentation_get_learning_resource | Get the onboarding learning resource at path | -| monitor_instrumentation_get_learning_resource | Show me the content of the Azure Monitor onboarding learning resource at path | -| monitor_instrumentation_get_learning_resource | Get the content of the Azure Monitor learning resource file at path | -| monitor_instrumentation_get_learning_resource | List all available Azure Monitor onboarding learning resources | -| monitor_instrumentation_get_learning_resource | Show me all learning resource paths for Azure Monitor instrumentation | -| monitor_instrumentation_get_learning_resource | What learning resources are available for Azure Monitor instrumentation onboarding? | -| monitor_instrumentation_orchestrator_next | After completing the previous Azure Monitor instrumentation step, get the next action for session with completion note | -| monitor_instrumentation_orchestrator_next | Get the next onboarding action using session after I completed | -| monitor_instrumentation_orchestrator_next | I finished the previous instrumentation step; return the next step for session with note | -| monitor_instrumentation_orchestrator_start | Start Azure Monitor instrumentation orchestration for workspace | -| monitor_instrumentation_orchestrator_start | Analyze workspace and return the first Azure Monitor instrumentation step | -| monitor_instrumentation_orchestrator_start | Begin guided Azure Monitor onboarding for project at and give me step one | -| monitor_instrumentation_send_brownfield_analysis | Send brownfield code analysis findings JSON to Azure Monitor instrumentation session after analysis was requested | -| monitor_instrumentation_send_brownfield_analysis | Continue migration orchestration by submitting analysis payload to session | -| monitor_instrumentation_send_brownfield_analysis | Send completed brownfield telemetry analysis for onboarding session | ## Azure Native ISV diff --git a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json index b5a7469e93..6652e88a04 100644 --- a/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json +++ b/servers/Azure.Mcp.Server/src/Resources/consolidated-tools.json @@ -3538,12 +3538,12 @@ "description": "Running this operation multiple times with the same arguments may have additional effects or produce different results." }, "openWorld": { - "value": true, - "description": "This tool may interact with an unpredictable or dynamic set of external entities." + "value": false, + "description": "This tool's domain of interaction is closed and well-defined, limited to a specific set of entities." }, "readOnly": { - "value": true, - "description": "This tool only performs read operations without modifying any state or data." + "value": false, + "description": "This tool may modify its environment by creating, updating, or deleting data." }, "secret": { "value": false, @@ -3557,7 +3557,8 @@ "mappedToolList": [ "monitor_instrumentation_orchestrator_start", "monitor_instrumentation_orchestrator_next", - "monitor_instrumentation_send_brownfield_analysis" + "monitor_instrumentation_send_brownfield_analysis", + "monitor_instrumentation_send_enhanced_selection" ] } , diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs index b2ae26927b..df1f486950 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorNextCommand.cs @@ -37,8 +37,8 @@ 3. Now call this tool to get the next action { Destructive = false, Idempotent = false, - OpenWorld = true, - ReadOnly = true, + OpenWorld = false, + ReadOnly = false, LocalRequired = true, Secret = false }; diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs index 2817967422..f7a6c40c11 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/OrchestratorStartCommand.cs @@ -29,8 +29,8 @@ public sealed class OrchestratorStartCommand(ILogger l { Destructive = false, Idempotent = false, - OpenWorld = true, - ReadOnly = true, + OpenWorld = false, + ReadOnly = false, LocalRequired = true, Secret = false }; diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs index 3d49ecc3b8..15fa06bc8a 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendBrownfieldAnalysisCommand.cs @@ -33,8 +33,8 @@ You must have scanned the workspace source files and filled in the analysis temp { Destructive = false, Idempotent = false, - OpenWorld = true, - ReadOnly = true, + OpenWorld = false, + ReadOnly = false, LocalRequired = true, Secret = false }; @@ -81,7 +81,8 @@ public override Task ExecuteAsync(CommandContext context, Parse findings.Processors, findings.ClientUsage, findings.Sampling, - findings.TelemetryPipeline); + findings.TelemetryPipeline, + findings.Logging); context.Response.Status = HttpStatusCode.OK; context.Response.Results = ResponseResult.Create(result, MonitorInstrumentationJsonContext.Default.String); diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancedSelectionCommand.cs b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancedSelectionCommand.cs new file mode 100644 index 0000000000..5b3e61cb16 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Commands/Instrumentation/SendEnhancedSelectionCommand.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net; +using Azure.Mcp.Tools.Monitor.Options; +using Azure.Mcp.Tools.Monitor.Tools; +using Microsoft.Extensions.Logging; +using Microsoft.Mcp.Core.Commands; +using Microsoft.Mcp.Core.Extensions; +using Microsoft.Mcp.Core.Models.Command; +using Microsoft.Mcp.Core.Models.Option; + +namespace Azure.Mcp.Tools.Monitor.Commands; + +public sealed class SendEnhancedSelectionCommand(ILogger logger) + : BaseCommand +{ + private readonly ILogger _logger = logger; + + public override string Id => "8fd4eb5f-14d1-450f-982c-82d761f0f7d6"; + + public override string Name => "send_enhanced_selection"; + + public override string Description => @"Submit the user's enhancement selection after orchestrator_start returned status 'enhancement_available'. +Present the enhancement options to the user first, then call this tool with their chosen option key(s). +Multiple enhancements can be selected by passing a comma-separated list (e.g. 'redis,processors'). +After this call succeeds, continue with orchestrator_next as usual."; + + public override string Title => "Send Enhancement Selection"; + + public override ToolMetadata Metadata => new() + { + Destructive = false, + Idempotent = false, + OpenWorld = false, + ReadOnly = false, + LocalRequired = true, + Secret = false + }; + + protected override void RegisterOptions(Command command) + { + command.Options.Add(MonitorInstrumentationOptionDefinitions.SessionId); + command.Options.Add(MonitorInstrumentationOptionDefinitions.EnhancementKeys); + } + + protected override SendEnhancedSelectionOptions BindOptions(ParseResult parseResult) + { + return new SendEnhancedSelectionOptions + { + SessionId = parseResult.CommandResult.GetValueOrDefault(MonitorInstrumentationOptionDefinitions.SessionId), + EnhancementKeys = parseResult.CommandResult.GetValueOrDefault(MonitorInstrumentationOptionDefinitions.EnhancementKeys) + }; + } + + public override Task ExecuteAsync(CommandContext context, ParseResult parseResult, CancellationToken cancellationToken) + { + if (!Validate(parseResult.CommandResult, context.Response).IsValid) + { + return Task.FromResult(context.Response); + } + + var options = BindOptions(parseResult); + + try + { + var tool = context.GetService(); + var result = tool.Send(options.SessionId!, options.EnhancementKeys!); + + context.Response.Status = HttpStatusCode.OK; + context.Response.Results = ResponseResult.Create(result, MonitorInstrumentationJsonContext.Default.String); + context.Response.Message = string.Empty; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in {Operation}. SessionId: {SessionId}", Name, options.SessionId); + HandleException(context, ex); + } + + return Task.FromResult(context.Response); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/DotNetInstrumentationDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/DotNetInstrumentationDetector.cs index 00cd296620..24e28d84f8 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/DotNetInstrumentationDetector.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/DotNetInstrumentationDetector.cs @@ -23,6 +23,10 @@ public InstrumentationResult Detect(string workspacePath) var configEvidence = CheckConfigFiles(workspacePath); evidence.AddRange(configEvidence); + // Check packages.config for classic .NET Framework projects + var packagesConfigEvidence = CheckPackagesConfig(workspacePath); + evidence.AddRange(packagesConfigEvidence); + if (evidence.Count == 0) { return new InstrumentationResult(InstrumentationState.Greenfield, null); @@ -31,6 +35,7 @@ public InstrumentationResult Detect(string workspacePath) // Determine instrumentation type from evidence var instrumentationType = DetermineInstrumentationType(evidence); var version = ExtractVersion(evidence); + var isTargetVersion = IsAlreadyOnTargetVersion(instrumentationType, version); return new InstrumentationResult( InstrumentationState.Brownfield, @@ -38,11 +43,48 @@ public InstrumentationResult Detect(string workspacePath) { Type = instrumentationType, Version = version, + IsTargetVersion = isTargetVersion, Evidence = evidence } ); } + private List CheckPackagesConfig(string workspacePath) + { + var evidence = new List(); + var packagesConfigs = Directory.GetFiles(workspacePath, "packages.config", SearchOption.AllDirectories); + + foreach (var configFile in packagesConfigs) + { + try + { + var doc = XDocument.Load(configFile); + var packages = doc.Descendants("package"); + + foreach (var pkg in packages) + { + var id = pkg.Attribute("id")?.Value ?? string.Empty; + var version = pkg.Attribute("version")?.Value ?? "unknown"; + + if (PackageDetection.AiSdkPackages.Any(p => id.Equals(p, StringComparison.OrdinalIgnoreCase))) + { + evidence.Add(new Evidence + { + File = configFile, + Indicator = $"PackageReference: {id} {version}" + }); + } + } + } + catch + { + // Skip files we can't parse + } + } + + return evidence; + } + private List AnalyzeProjectReferences(string csprojPath) { var evidence = new List(); @@ -174,4 +216,41 @@ private InstrumentationType DetermineInstrumentationType(List evidence } return null; } + + /// + /// Determines if the detected SDK is already on the target version: + /// - ApplicationInsightsSdk: 3.x is target (2.x needs migration) + /// - AzureMonitorDistro: any version is target (already on the recommended path) + /// + private static bool IsAlreadyOnTargetVersion(InstrumentationType type, string? version) + { + if (type == InstrumentationType.AzureMonitorDistro) + { + return true; + } + + if (type != InstrumentationType.ApplicationInsightsSdk || string.IsNullOrWhiteSpace(version)) + { + return false; + } + + // Handle wildcard versions like "3.*" + if (version.StartsWith("3.", StringComparison.Ordinal) || version.StartsWith("3-", StringComparison.Ordinal)) + { + return true; + } + + // Try to parse as a real version + // Strip leading 'v' if present, handle pre-release suffix + var versionToParse = version.TrimStart('v'); + var dashIndex = versionToParse.IndexOf('-'); + var versionCore = dashIndex > 0 ? versionToParse[..dashIndex] : versionToParse; + + if (Version.TryParse(versionCore, out var parsed)) + { + return parsed.Major >= 3; + } + + return false; + } } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsAppTypeDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsAppTypeDetector.cs new file mode 100644 index 0000000000..b5a6c351e3 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsAppTypeDetector.cs @@ -0,0 +1,164 @@ +using System.Text.Json; +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Detectors; + +public class NodeJsAppTypeDetector : IAppTypeDetector +{ + public Language SupportedLanguage => Language.NodeJs; + + public List DetectProjects(string workspacePath) + { + var projects = new List(); + + var packageJsonPath = Path.Combine(workspacePath, "package.json"); + if (!File.Exists(packageJsonPath)) + { + return projects; + } + + try + { + var packageJson = JsonDocument.Parse(File.ReadAllText(packageJsonPath)); + var root = packageJson.RootElement; + + // Get project name + var projectName = root.TryGetProperty("name", out var nameElement) + ? nameElement.GetString() ?? "nodejs-app" + : "nodejs-app"; + + // Detect app type based on dependencies + var appType = DetectAppType(root); + + // Common entry points for Node.js apps + var entryPoint = DetectEntryPoint(workspacePath, root); + + projects.Add(new ProjectInfo + { + ProjectFile = packageJsonPath, + EntryPoint = entryPoint, + AppType = appType + }); + } + catch (JsonException) + { + // If package.json is malformed, return empty list + return projects; + } + + return projects; + } + + private AppType DetectAppType(JsonElement root) + { + var dependencies = new List(); + + // Collect all dependencies + if (root.TryGetProperty("dependencies", out var depsElement)) + { + foreach (var dep in depsElement.EnumerateObject()) + { + dependencies.Add(dep.Name); + } + } + + if (root.TryGetProperty("devDependencies", out var devDepsElement)) + { + foreach (var dep in devDepsElement.EnumerateObject()) + { + dependencies.Add(dep.Name); + } + } + + // Check for framework-specific packages + // AI/ML frameworks first (they may also include web frameworks as dependencies) + if (dependencies.Contains("langchain") || dependencies.Contains("@langchain/core")) + { + return AppType.LangchainJs; + } + + if (dependencies.Contains("@nestjs/core") || dependencies.Contains("@nestjs/common")) + { + return AppType.NestJs; + } + + if (dependencies.Contains("next")) + { + return AppType.NextJs; + } + + if (dependencies.Contains("fastify")) + { + return AppType.Fastify; + } + + if (dependencies.Contains("express")) + { + return AppType.Express; + } + + // Database integrations (when no web framework is detected) + if (dependencies.Contains("pg") || dependencies.Contains("pg-pool") || dependencies.Contains("postgres")) + { + return AppType.PostgresNodeJs; + } + + if (dependencies.Contains("mongodb") || dependencies.Contains("mongoose")) + { + return AppType.MongoDBNodeJs; + } + + if (dependencies.Contains("redis") || dependencies.Contains("ioredis")) + { + return AppType.RedisNodeJs; + } + + if (dependencies.Contains("mysql2") || dependencies.Contains("mysql")) + { + return AppType.MySQLNodeJs; + } + + // Logging integrations (when no web framework or database is detected) + if (dependencies.Contains("winston")) + { + return AppType.WinstonNodeJs; + } + + if (dependencies.Contains("bunyan")) + { + return AppType.BunyanNodeJs; + } + + // Console-based apps (basic Node.js with no specific framework) + // This is a fallback for any Node.js app that uses console for logging + // or has minimal dependencies without a specific framework + return AppType.ConsoleNodeJs; + } + + private string DetectEntryPoint(string workspacePath, JsonElement root) + { + // Check package.json "main" field + if (root.TryGetProperty("main", out var mainElement)) + { + var mainFile = mainElement.GetString(); + if (!string.IsNullOrEmpty(mainFile)) + { + return Path.Combine(workspacePath, mainFile); + } + } + + // Common entry point files + var commonEntryPoints = new[] { "index.js", "server.js", "app.js", "src/index.js", "src/server.js", "src/app.js" }; + + foreach (var entryPoint in commonEntryPoints) + { + var fullPath = Path.Combine(workspacePath, entryPoint); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + return Path.Combine(workspacePath, "index.js"); // Default fallback + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsInstrumentationDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsInstrumentationDetector.cs new file mode 100644 index 0000000000..fbb2cd1c12 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsInstrumentationDetector.cs @@ -0,0 +1,117 @@ +using System.Text.Json; +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Detectors; + +public class NodeJsInstrumentationDetector : IInstrumentationDetector +{ + public Language SupportedLanguage => Language.NodeJs; + + private static readonly string[] AzureMonitorPackages = { + "@azure/monitor-opentelemetry", + "@azure/monitor-opentelemetry-exporter" + }; + + private static readonly string[] OpenTelemetryPackages = { + "@opentelemetry/api", + "@opentelemetry/sdk-node", + "@opentelemetry/auto-instrumentations-node" + }; + + private static readonly string[] ApplicationInsightsPackages = { + "applicationinsights" + }; + + public InstrumentationResult Detect(string workspacePath) + { + + var packageJsonPath = Path.Combine(workspacePath, "package.json"); + if (!File.Exists(packageJsonPath)) + { + return new InstrumentationResult( + InstrumentationState.Greenfield, + null + ); + } + + try + { + var packageJson = JsonDocument.Parse(File.ReadAllText(packageJsonPath)); + var root = packageJson.RootElement; + + var dependencies = new List(); + + // Collect all dependencies + if (root.TryGetProperty("dependencies", out var depsElement)) + { + foreach (var dep in depsElement.EnumerateObject()) + { + dependencies.Add(dep.Name); + } + } + + if (root.TryGetProperty("devDependencies", out var devDepsElement)) + { + foreach (var dep in devDepsElement.EnumerateObject()) + { + dependencies.Add(dep.Name); + } + } + + // Check for Azure Monitor packages + var azureMonitorFound = dependencies.Any(d => AzureMonitorPackages.Contains(d)); + if (azureMonitorFound) + { + return new InstrumentationResult( + InstrumentationState.Brownfield, + new ExistingInstrumentation + { + Type = InstrumentationType.AzureMonitorDistro, + Evidence = [new Evidence { File = packageJsonPath, Indicator = "Azure Monitor package found in dependencies" }] + } + ); + } + + // Check for OpenTelemetry packages + var openTelemetryFound = dependencies.Any(d => OpenTelemetryPackages.Contains(d)); + if (openTelemetryFound) + { + return new InstrumentationResult( + InstrumentationState.Brownfield, + new ExistingInstrumentation + { + Type = InstrumentationType.OpenTelemetry, + Evidence = [new Evidence { File = packageJsonPath, Indicator = "OpenTelemetry package found in dependencies" }] + } + ); + } + + // Check for Application Insights SDK + var appInsightsFound = dependencies.Any(d => ApplicationInsightsPackages.Contains(d)); + if (appInsightsFound) + { + return new InstrumentationResult( + InstrumentationState.Brownfield, + new ExistingInstrumentation + { + Type = InstrumentationType.ApplicationInsightsSdk, + Evidence = [new Evidence { File = packageJsonPath, Indicator = "Application Insights SDK found in dependencies" }] + } + ); + } + + // No instrumentation found + return new InstrumentationResult( + InstrumentationState.Greenfield, + null + ); + } + catch (JsonException) + { + return new InstrumentationResult( + InstrumentationState.Greenfield, + null + ); + } + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsLanguageDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsLanguageDetector.cs new file mode 100644 index 0000000000..8be19230bb --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/NodeJsLanguageDetector.cs @@ -0,0 +1,23 @@ +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Detectors; + +public class NodeJsLanguageDetector : ILanguageDetector +{ + public bool CanHandle(string workspacePath) + { + // Check for package.json which indicates a Node.js project + var packageJsonPath = Path.Combine(workspacePath, "package.json"); + return File.Exists(packageJsonPath); + } + + public Language Detect(string workspacePath) + { + if (CanHandle(workspacePath)) + { + return Language.NodeJs; + } + + return Language.Unknown; + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonAppTypeDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonAppTypeDetector.cs new file mode 100644 index 0000000000..56a745ca9b --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonAppTypeDetector.cs @@ -0,0 +1,184 @@ +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Detectors; + +/// +/// Detects Python application type based on framework dependencies. +/// Uses PythonInstrumentationRegistry to identify known frameworks. +/// +public class PythonAppTypeDetector : IAppTypeDetector +{ + public Language SupportedLanguage => Language.Python; + + public List DetectProjects(string workspacePath) + { + var projects = new List(); + + // Look for Python dependency files + var requirementsPath = Path.Combine(workspacePath, "requirements.txt"); + var pyprojectPath = Path.Combine(workspacePath, "pyproject.toml"); + + string? projectFile = null; + var dependencies = new List(); + + // Parse requirements.txt + if (File.Exists(requirementsPath)) + { + var content = PythonInstrumentationDetector.TryReadFile(requirementsPath); + if (content != null) + { + projectFile = requirementsPath; + dependencies.AddRange(PythonInstrumentationDetector.ParseRequirementsTxt(content)); + } + } + + // Parse pyproject.toml + if (File.Exists(pyprojectPath)) + { + var content = PythonInstrumentationDetector.TryReadFile(pyprojectPath); + if (content != null) + { + projectFile ??= pyprojectPath; + dependencies.AddRange(PythonInstrumentationDetector.ParsePyprojectToml(content)); + } + } + + // No Python dependency files found + if (projectFile == null) + { + return projects; + } + + // Normalize package names + dependencies = dependencies + .Select(PythonInstrumentationRegistry.NormalizePackageName) + .Distinct() + .ToList(); + + // Detect app type based on dependencies + var appType = DetectAppType(dependencies); + + // Detect entry point + var entryPoint = DetectEntryPoint(workspacePath, appType); + + projects.Add(new ProjectInfo + { + ProjectFile = projectFile, + EntryPoint = entryPoint, + AppType = appType, + Dependencies = dependencies + }); + + return projects; + } + + /// + /// Detect Python app type by checking for framework packages. + /// Priority order: Django > FastAPI > Flask > GenAI > others + /// (Django and FastAPI are more specific, Flask is more general) + /// + private AppType DetectAppType(List dependencies) + { + // Get known frameworks from registry + var frameworks = PythonInstrumentationRegistry.GetByCategory("framework") + .Select(f => PythonInstrumentationRegistry.NormalizePackageName(f.LibraryName)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Get GenAI libraries from registry + var genaiLibraries = PythonInstrumentationRegistry.GetByCategory("genai") + .Select(f => PythonInstrumentationRegistry.NormalizePackageName(f.LibraryName)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Check in priority order (most specific first) + // Django - full-featured framework + if (dependencies.Contains("django")) + { + return AppType.Django; + } + + // FastAPI - modern async framework (uses Starlette internally) + if (dependencies.Contains("fastapi")) + { + return AppType.FastAPI; + } + + // Flask - lightweight framework + if (dependencies.Contains("flask")) + { + return AppType.Flask; + } + + // Starlette - async framework (FastAPI is built on this) + if (dependencies.Contains("starlette")) + { + return AppType.Starlette; + } + + // Falcon - REST API framework + if (dependencies.Contains("falcon")) + { + return AppType.Falcon; + } + + // GenAI - Check for OpenAI, Azure OpenAI, Anthropic, LangChain, etc. + var foundGenAI = dependencies.FirstOrDefault(d => genaiLibraries.Contains(d)); + if (foundGenAI != null) + { + return AppType.GenAI; + } + + // Check for any other known framework from registry + var foundFramework = dependencies.FirstOrDefault(d => frameworks.Contains(d)); + if (foundFramework != null) + { + // Unrecognized framework — treat as Console since we have no specific generator. + // The generic Python instrumentation setup still applies. + return AppType.Console; + } + + // Console/generic Python app — even with no dependencies, Python projects + // should get the generic Console generator rather than "unsupported" + return AppType.Console; + } + + /// + /// Detect the entry point file for a Python application. + /// + private string? DetectEntryPoint(string workspacePath, AppType appType) + { + // Django uses manage.py as the main entry point + if (appType == AppType.Django) + { + var managePy = Path.Combine(workspacePath, "manage.py"); + if (File.Exists(managePy)) + { + return managePy; + } + } + + // Common entry point files for Python web apps + var commonEntryPoints = new[] + { + "app.py", // Flask/FastAPI convention + "main.py", // Common convention + "server.py", // Server apps + "wsgi.py", // WSGI apps + "asgi.py", // ASGI apps (FastAPI) + "src/app.py", + "src/main.py", + "application.py" + }; + + foreach (var entryPoint in commonEntryPoints) + { + var fullPath = Path.Combine(workspacePath, entryPoint); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + // Default fallback + return null; + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonInstrumentationDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonInstrumentationDetector.cs new file mode 100644 index 0000000000..6aebb01245 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonInstrumentationDetector.cs @@ -0,0 +1,302 @@ +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Detectors; + +/// +/// Detects Python application instrumentation status. +/// +/// Library lists are loaded from Resources/instrumentations/python-instrumentations.json +/// which can be updated by running the update script. +/// +/// Sources: +/// - OpenTelemetry Python Contrib: https://github.com/open-telemetry/opentelemetry-python-contrib +/// - Azure Monitor Distro: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry +/// +public class PythonInstrumentationDetector : IInstrumentationDetector +{ + public Language SupportedLanguage => Language.Python; + + public InstrumentationResult Detect(string workspacePath) + { + var dependencies = new List(); + string? evidenceFile = null; + + // Check for requirements.txt + var requirementsPath = Path.Combine(workspacePath, "requirements.txt"); + if (File.Exists(requirementsPath)) + { + var content = TryReadFile(requirementsPath); + if (content != null) + { + dependencies.AddRange(ParseRequirementsTxt(content)); + evidenceFile ??= requirementsPath; + } + } + + // Check for pyproject.toml + var pyprojectPath = Path.Combine(workspacePath, "pyproject.toml"); + if (File.Exists(pyprojectPath)) + { + var content = TryReadFile(pyprojectPath); + if (content != null) + { + dependencies.AddRange(ParsePyprojectToml(content)); + evidenceFile ??= pyprojectPath; + } + } + + // No dependency files found = Greenfield + if (evidenceFile == null) + { + return new InstrumentationResult( + InstrumentationState.Greenfield, + null + ); + } + + // Normalize and deduplicate package names + dependencies = dependencies + .Select(PythonInstrumentationRegistry.NormalizePackageName) + .Distinct() + .ToList(); + + // TODO: Re-enable Brownfield detection scenarios + // // Check for Azure Monitor packages (highest priority) + // var azureMonitorFound = dependencies.FirstOrDefault(d => + // PythonInstrumentationRegistry.AzureMonitorPackages.Contains(d)); + // if (azureMonitorFound != null) + // { + // return new InstrumentationResult( + // InstrumentationState.Brownfield, + // new ExistingInstrumentation + // { + // Type = InstrumentationType.AzureMonitorDistro, + // Evidence = [new Evidence { File = evidenceFile, Indicator = $"Azure Monitor package '{azureMonitorFound}' found in dependencies" }] + // } + // ); + // } + + // // Check for OpenTelemetry instrumentation packages (opentelemetry-instrumentation-*) + // var otelInstrumentationPackage = dependencies.FirstOrDefault(d => + // d.StartsWith("opentelemetry-instrumentation-") && d != "opentelemetry-instrumentation"); + // if (otelInstrumentationPackage != null) + // { + // return new InstrumentationResult( + // InstrumentationState.Brownfield, + // new ExistingInstrumentation + // { + // Type = InstrumentationType.OpenTelemetry, + // Evidence = [new Evidence { File = evidenceFile, Indicator = $"OpenTelemetry instrumentation package '{otelInstrumentationPackage}' found in dependencies" }] + // } + // ); + // } + + // // Check for OpenTelemetry core packages + // var openTelemetryFound = dependencies.FirstOrDefault(d => + // PythonInstrumentationRegistry.OpenTelemetryCorePackages.Contains(d)); + // if (openTelemetryFound != null) + // { + // return new InstrumentationResult( + // InstrumentationState.Brownfield, + // new ExistingInstrumentation + // { + // Type = InstrumentationType.OpenTelemetry, + // Evidence = [new Evidence { File = evidenceFile, Indicator = $"OpenTelemetry package '{openTelemetryFound}' found in dependencies" }] + // } + // ); + // } + + // // Check for Application Insights SDK + // var appInsightsFound = dependencies.FirstOrDefault(d => + // PythonInstrumentationRegistry.ApplicationInsightsPackages.Contains(d)); + // if (appInsightsFound != null) + // { + // return new InstrumentationResult( + // InstrumentationState.Brownfield, + // new ExistingInstrumentation + // { + // Type = InstrumentationType.ApplicationInsightsSdk, + // Evidence = [new Evidence { File = evidenceFile, Indicator = $"Application Insights SDK '{appInsightsFound}' found in dependencies" }] + // } + // ); + // } + + // No instrumentation found (Brownfield checks disabled) + return new InstrumentationResult( + InstrumentationState.Greenfield, + null + ); + } + + /// + /// Parse requirements.txt format. + /// Each line is typically: package-name==1.0.0 or package-name>=1.0 + /// + public static List ParseRequirementsTxt(string content) + { + var packages = new List(); + var lines = content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + // Skip empty lines + if (string.IsNullOrEmpty(trimmed)) + { + continue; + } + + // Skip comments + if (trimmed.StartsWith("#")) + { + continue; + } + + // Skip flags like -r, -e, --index-url, etc. + if (trimmed.StartsWith("-")) + { + continue; + } + + // Extract package name by finding the first special character + // Package names end at: ==, >=, <=, ~=, !=, <, >, [, ;, @, or space + var packageName = ExtractPackageName(trimmed); + if (!string.IsNullOrEmpty(packageName)) + { + packages.Add(packageName); + } + } + + return packages; + } + + /// + /// Extract package name from a requirements line. + /// Stops at version specifiers (==, >=, etc.) or extras ([). + /// + public static string ExtractPackageName(string line) + { + var endIndex = line.Length; + + // Find the earliest occurrence of any delimiter + char[] delimiters = { '=', '<', '>', '!', '~', '[', ';', '@', ' ' }; + foreach (var delimiter in delimiters) + { + var index = line.IndexOf(delimiter); + if (index > 0 && index < endIndex) + { + endIndex = index; + } + } + + return line.Substring(0, endIndex).Trim(); + } + + /// + /// Parse pyproject.toml format (PEP 621 and Poetry). + /// Uses simple line-by-line parsing instead of complex regex. + /// + public static List ParsePyprojectToml(string content) + { + var packages = new List(); + var lines = content.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var inPoetryDependencies = false; + + foreach (var line in lines) + { + var trimmed = line.Trim(); + + // Check for section headers + if (trimmed.StartsWith("[")) + { + inPoetryDependencies = trimmed == "[tool.poetry.dependencies]"; + continue; + } + + // Check for inline dependencies array: dependencies = ["pkg1", "pkg2"] + if (trimmed.StartsWith("dependencies") && trimmed.Contains("=") && trimmed.Contains("[")) + { + packages.AddRange(ExtractPackagesFromArray(trimmed)); + continue; + } + + // Poetry format: package-name = "version" or package-name = {version = "x"} + if (inPoetryDependencies && trimmed.Contains("=")) + { + var packageName = trimmed.Split('=')[0].Trim(); + // Skip python version and empty names + if (!string.IsNullOrEmpty(packageName) && packageName.ToLowerInvariant() != "python") + { + packages.Add(packageName); + } + } + } + + return packages; + } + + /// + /// Extract package names from an inline array like: dependencies = ["flask>=2.0", "requests"] + /// + private static List ExtractPackagesFromArray(string line) + { + var packages = new List(); + + // Find the array content between [ and ] + var startIndex = line.IndexOf('['); + var endIndex = line.LastIndexOf(']'); + + if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex) + { + return packages; + } + + var arrayContent = line.Substring(startIndex + 1, endIndex - startIndex - 1); + + // Split by comma and extract each package + var items = arrayContent.Split(','); + foreach (var item in items) + { + var trimmed = item.Trim().Trim('"', '\''); + if (!string.IsNullOrEmpty(trimmed)) + { + // Extract just the package name (before any version specifier) + var packageName = ExtractPackageName(trimmed); + if (!string.IsNullOrEmpty(packageName)) + { + packages.Add(packageName); + } + } + } + + return packages; + } + + /// + /// Safely read file content with exception handling for locked files, + /// permission issues, or encoding problems. + /// + internal static string? TryReadFile(string filePath) + { + try + { + return File.ReadAllText(filePath); + } + catch (IOException) + { + // File is locked or inaccessible + return null; + } + catch (UnauthorizedAccessException) + { + // Insufficient permissions + return null; + } + catch (Exception) + { + // Handle other potential exceptions + return null; + } + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonLanguageDetector.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonLanguageDetector.cs new file mode 100644 index 0000000000..80eaad4c0c --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Detectors/PythonLanguageDetector.cs @@ -0,0 +1,57 @@ +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Detectors; + +public class PythonLanguageDetector : ILanguageDetector +{ + public bool CanHandle(string workspacePath) + { + // Check for pyproject.toml which indicates a Python project + var pyprojectTomlPath = Path.Combine(workspacePath, "pyproject.toml"); + if (File.Exists(pyprojectTomlPath)) + { + return true; + } + + // Check for setup.py which indicates a Python project + var setupPyPath = Path.Combine(workspacePath, "setup.py"); + if (File.Exists(setupPyPath)) + { + return true; + } + + // Check for setup.cfg which indicates a Python project + var setupCfgPath = Path.Combine(workspacePath, "setup.cfg"); + if (File.Exists(setupCfgPath)) + { + return true; + } + + try + { + // Abstract out pattern for checking single .py/ .js files later + return Directory.EnumerateFiles(workspacePath, "*.py", SearchOption.AllDirectories).Any(); + } + catch (UnauthorizedAccessException) + { + // Log or handle the exception as needed + return false; + } + catch (DirectoryNotFoundException) + { + // Log or handle the exception as needed + return false; + } + + } + + public Language Detect(string workspacePath) + { + if (CanHandle(workspacePath)) + { + return Language.Python; + } + + return Language.Unknown; + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetClassicBrownfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetClassicBrownfieldGenerator.cs new file mode 100644 index 0000000000..f038d1f0dc --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetClassicBrownfieldGenerator.cs @@ -0,0 +1,158 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for classic ASP.NET brownfield projects migrating from Application Insights 2.x to 3.x. +/// Supports ASP.NET MVC, WebForms, and generic ASP.NET app types. +/// Uses Package Manager Console (nuget-vs) for package operations and ConfigureOpenTelemetryBuilder for non-DI extensibility. +/// +public class AspNetClassicBrownfieldGenerator : BrownfieldGeneratorBase +{ + // TargetAppType is used by base for default FindProject — we override FindProject instead + protected override AppType TargetAppType => AppType.AspNetMvc; + protected override string PackageName => Packages.ApplicationInsightsWeb; + protected override string PackageVersion => Packages.ApplicationInsightsWeb3x; + protected override string MigrationCodeResource => LearningResources.MigrationAspNetClassic2xTo3xCode; + protected override string MigrationNoCodeChangeResource => LearningResources.MigrationAspNetClassic2xTo3xCode; // No no-code-change path for classic — always has config changes + protected override string EntryPointMethodName => "TelemetryConfiguration.CreateDefault"; + protected override string PackageManagerType => Packages.PackageManagerNuGetVS; + protected override string DefaultEntryPoint => "Global.asax.cs"; + + protected override ProjectInfo FindProject(Analysis analysis) + => analysis.Projects.First(p => + p.AppType is AppType.AspNetClassic or AppType.AspNetMvc or AppType.AspNetWebForms); + + public override bool CanHandle(Analysis analysis) + { + var classicProjectCount = analysis.Projects + .Count(p => p.AppType is AppType.AspNetClassic + or AppType.AspNetMvc + or AppType.AspNetWebForms); + + return analysis.Language == Language.DotNet + && classicProjectCount == 1 + && analysis.State == InstrumentationState.Brownfield + && analysis.ExistingInstrumentation?.Type == InstrumentationType.ApplicationInsightsSdk + && analysis.ExistingInstrumentation?.IsTargetVersion != true + && analysis.BrownfieldFindings is not null; + } + + protected override List BuildLearnResources(BrownfieldFindings? findings) + { + var resources = new List { MigrationCodeResource }; + + // Always include non-DI extensibility doc for classic ASP.NET + resources.Add(LearningResources.ApiConfigureOpenTelemetryBuilder); + + if (findings?.Initializers is { Found: true, Implementations.Count: > 0 } + || findings?.Processors is { Found: true, Implementations.Count: > 0 }) + { + resources.Add(LearningResources.ApiActivityProcessors); + resources.Add(LearningResources.ApiLogProcessors); + // No ConfigureOpenTelemetryProvider — classic uses ConfigureOpenTelemetryBuilder (already added above) + } + + if (HasBreakingClientUsage(findings?.ClientUsage)) + resources.Add(LearningResources.ApiTelemetryClient); + + // Surface AAD migration doc when an initializer or processor related to + // credential / AAD / token authentication is detected. + if (HasAadRelatedCustomizations(findings)) + resources.Add(LearningResources.MigrationAadAuthentication); + + // No Sampling.md — classic ASP.NET sampling is via / in applicationinsights.config, + // already covered in the migration doc. Custom OTel samplers not supported with 3.x shim. + + return resources; + } + + protected override bool HasFrameworkSpecificCodeChanges(ServiceOptionsFindings opts) + { + // Classic ASP.NET always has code changes — applicationinsights.config must be rewritten + return true; + } + + protected override string AddServiceOptionsActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency) + { + var dep = AddSharedServiceOptionsActions(builder, opts, entryPoint, EntryPointMethodName, lastDependency); + + // Classic ASP.NET: rewrite applicationinsights.config + var actionId = "rewrite-appinsights-config"; + builder.AddManualStepAction( + actionId, + "Rewrite applicationinsights.config to 3.x format", + "Rewrite applicationinsights.config to the 3.x format: " + + "remove the entire section, section, " + + " section, and section. " + + "Replace with . " + + "Add 3.x elements: , , " + + ", , " + + ", , " + + ". " + + "See the migration guide for the full 3.x template.", + dependsOn: dep); + dep = actionId; + + // Update Web.config HTTP modules + actionId = "update-webconfig-modules"; + builder.AddManualStepAction( + actionId, + "Update Web.config HTTP modules", + "In Web.config: " + + "remove TelemetryCorrelationHttpModule entries (from both and ). " + + "Verify ApplicationInsightsWebTracking (ApplicationInsightsHttpModule) is present. " + + "Verify TelemetryHttpModule (OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule) is present — " + + "this should have been added by the package upgrade.", + dependsOn: dep); + dep = actionId; + + // Remove satellite packages + actionId = "remove-satellite-packages"; + builder.AddManualStepAction( + actionId, + "Remove satellite packages via Package Manager Console", + "ASK THE USER to remove these satellite packages in this exact order via Package Manager Console " + + "(dependents must be removed before their dependencies): " + + "1. Uninstall-Package Microsoft.ApplicationInsights.WindowsServer; " + + "2. Uninstall-Package Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel; " + + "3. Uninstall-Package Microsoft.ApplicationInsights.DependencyCollector; " + + "4. Uninstall-Package Microsoft.ApplicationInsights.PerfCounterCollector; " + + "5. Uninstall-Package Microsoft.ApplicationInsights.Agent.Intercept; " + + "6. Uninstall-Package Microsoft.AspNet.TelemetryCorrelation. " + + "Only uninstall packages that exist in packages.config — skip any that are not present.", + dependsOn: dep); + dep = actionId; + + // Fix TelemetryConfiguration.Active usage + actionId = "fix-telemetry-config-active"; + builder.AddManualStepAction( + actionId, + "Replace TelemetryConfiguration.Active with CreateDefault()", + $"In {entryPoint} and any other files, replace TelemetryConfiguration.Active with " + + "TelemetryConfiguration.CreateDefault(). Note: CreateDefault() returns a static singleton in 3.x. " + + "Also replace any config.InstrumentationKey assignments with config.ConnectionString.", + dependsOn: dep); + dep = actionId; + + return dep; + } + + protected override string AddRemovedMethodActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency) + { + // Classic ASP.NET doesn't have UseApplicationInsights() — go straight to shared methods + return AddSharedRemovedMethodActions(builder, opts, entryPoint, EntryPointMethodName, lastDependency); + } + + protected override void AddConnectionStringAction(OnboardingSpecBuilder builder, BrownfieldFindings? findings, string lastDependency) + { + // Classic ASP.NET uses in applicationinsights.config, not appsettings.json. + // This is already handled by the "rewrite-appinsights-config" step — no additional action needed. + // The user can also set APPLICATIONINSIGHTS_CONNECTION_STRING as env var. + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetClassicGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetClassicGreenfieldGenerator.cs new file mode 100644 index 0000000000..b8735adbab --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetClassicGreenfieldGenerator.cs @@ -0,0 +1,49 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for classic ASP.NET greenfield projects (.NET Framework). +/// Supports ASP.NET MVC, WebForms, and generic ASP.NET app types. +/// Uses Microsoft.ApplicationInsights.Web — zero code change onboarding via NuGet. +/// +public class AspNetClassicGreenfieldGenerator : IGenerator +{ + private readonly GeneratorConfig _config; + + public AspNetClassicGreenfieldGenerator() + { + _config = GeneratorConfigLoader.LoadConfig("aspnet-classic-greenfield"); + } + + public bool CanHandle(Analysis analysis) + { + var classicProjectCount = analysis.Projects + .Count(p => p.AppType is AppType.AspNetClassic + or AppType.AspNetMvc + or AppType.AspNetWebForms); + + return analysis.Language == Language.DotNet + && classicProjectCount == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => + p.AppType is AppType.AspNetClassic or AppType.AspNetMvc or AppType.AspNetWebForms); + var projectFile = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "Global.asax.cs"; + var projectDir = Path.GetDirectoryName(projectFile) ?? ""; + + return new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + _config.Decision.Solution, + _config.Decision.Rationale) + .AddActionsFromConfig(_config, projectFile, entryPoint, projectDir) + .Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetCoreBrownfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetCoreBrownfieldGenerator.cs index 84138ec7a0..79569268d0 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetCoreBrownfieldGenerator.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/AspNetCoreBrownfieldGenerator.cs @@ -5,223 +5,47 @@ namespace Azure.Mcp.Tools.Monitor.Generators; /// /// Generator for ASP.NET Core brownfield projects migrating from Application Insights SDK 2.x to 3.x. -/// Routes brownfield findings to existing learn resources and produces targeted migration actions. /// -public class AspNetCoreBrownfieldGenerator : IGenerator +public class AspNetCoreBrownfieldGenerator : BrownfieldGeneratorBase { - public bool CanHandle(Analysis analysis) + protected override AppType TargetAppType => AppType.AspNetCore; + protected override string PackageName => Packages.ApplicationInsightsAspNetCore; + protected override string PackageVersion => Packages.ApplicationInsightsAspNetCore3x; + protected override string MigrationCodeResource => LearningResources.MigrationAppInsights2xTo3xCode; + protected override string MigrationNoCodeChangeResource => LearningResources.MigrationAppInsights2xTo3xNoCodeChange; + protected override string EntryPointMethodName => "AddApplicationInsightsTelemetry"; + + public override bool CanHandle(Analysis analysis) { - var aspNetCoreProjects = analysis.Projects - .Where(p => p.AppType == AppType.AspNetCore) - .ToList(); + var aspNetCoreProjectCount = analysis.Projects + .Count(p => p.AppType == AppType.AspNetCore); - // BrownfieldFindings must be populated (by SendBrownfieldAnalysisTool) - // so we don't match during the initial WorkspaceAnalyzer scan. return analysis.Language == Language.DotNet - && aspNetCoreProjects.Count == 1 + && aspNetCoreProjectCount == 1 && analysis.State == InstrumentationState.Brownfield && analysis.ExistingInstrumentation?.Type == InstrumentationType.ApplicationInsightsSdk + && analysis.ExistingInstrumentation?.IsTargetVersion != true && analysis.BrownfieldFindings is not null; } - public OnboardingSpec Generate(Analysis analysis) + protected override bool HasFrameworkSpecificCodeChanges(ServiceOptionsFindings opts) { - var findings = analysis.BrownfieldFindings; - var project = analysis.Projects.First(p => p.AppType == AppType.AspNetCore); - var projectFile = project.ProjectFile; - var entryPoint = project.EntryPoint ?? "Program.cs"; - - var builder = new OnboardingSpecBuilder(analysis) - .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction); - - // Determine if any code changes are needed - var needsCodeChange = HasCodeChanges(findings); - - if (!needsCodeChange) - { - // No-code-change path: just bump the package version - return builder - .WithDecision( - Intents.Migrate, - Approaches.ApplicationInsights3x, - "Existing Application Insights SDK detected with no removed properties or custom code. Package upgrade only.") - .AddReviewEducationAction( - "review-migration", - "Review the no-code-change migration guide", - [LearningResources.MigrationAppInsights2xTo3xNoCodeChange]) - .AddPackageAction( - "upgrade-appinsights", - "Upgrade Microsoft.ApplicationInsights.AspNetCore to 3.x", - Packages.PackageManagerNuGet, - Packages.ApplicationInsightsAspNetCore, - Packages.ApplicationInsightsAspNetCore3x, - projectFile, - "review-migration") - .Build(); - } - - // Code-change path: build targeted migration plan based on findings - builder.WithDecision( - Intents.Migrate, - Approaches.ApplicationInsights3x, - "Existing Application Insights SDK detected with properties/patterns that require code changes for migration."); - - // Collect education resources based on what was found - var learnResources = new List { LearningResources.MigrationAppInsights2xTo3xCode }; - - if (findings?.Sampling != null && findings.Sampling.HasCustomSampling) - { - learnResources.Add(LearningResources.ApiSampling); - } - - if (findings?.Initializers is { Found: true, Implementations.Count: > 0 } - || findings?.Processors is { Found: true, Implementations.Count: > 0 }) - { - learnResources.Add(LearningResources.ApiActivityProcessors); - learnResources.Add(LearningResources.ApiLogProcessors); - learnResources.Add(LearningResources.ApiConfigureOpenTelemetryProvider); - } - - builder.AddReviewEducationAction( - "review-migration", - "Review the migration guide and relevant API references", - learnResources); - - // Package upgrade - builder.AddPackageAction( - "upgrade-appinsights", - "Upgrade Microsoft.ApplicationInsights.AspNetCore to 3.x", - Packages.PackageManagerNuGet, - Packages.ApplicationInsightsAspNetCore, - Packages.ApplicationInsightsAspNetCore3x, - projectFile, - "review-migration"); - - var lastDependency = "upgrade-appinsights"; - - // Route service options findings - if (findings?.ServiceOptions != null) - { - lastDependency = AddServiceOptionsActions(builder, findings.ServiceOptions, entryPoint, lastDependency); - } - - // Route removed extension method findings - if (findings?.ServiceOptions != null) - { - lastDependency = AddRemovedMethodActions(builder, findings.ServiceOptions, entryPoint, lastDependency); - } - - // Route telemetry initializer findings - if (findings?.Initializers is { Found: true, Implementations.Count: > 0 }) - { - lastDependency = AddInitializerActions(builder, findings.Initializers, lastDependency); - } - - // Route telemetry processor findings - if (findings?.Processors is { Found: true, Implementations.Count: > 0 }) - { - lastDependency = AddProcessorActions(builder, findings.Processors, lastDependency); - } - - // TelemetryClient still works in 3.x — no migration actions needed for direct usage - - // Route custom sampling findings - if (findings?.Sampling is { HasCustomSampling: true }) - { - lastDependency = AddSamplingActions(builder, findings.Sampling, lastDependency); - } - - // Connection string config (use AppInsights path, not AzureMonitor distro path) - // If InstrumentationKey was found, tell the user to replace it in config too - var hasIkeyInCode = findings?.ServiceOptions?.InstrumentationKey != null; - var configDescription = hasIkeyInCode - ? "Replace InstrumentationKey with ConnectionString in appsettings.json (remove the old ApplicationInsights.InstrumentationKey entry)" - : "Configure Azure Monitor connection string"; - - builder.AddConfigAction( - "add-connection-string", - configDescription, - Config.AppSettingsFileName, - Config.AppInsightsConnectionStringPath, - Config.ConnectionStringPlaceholder, - Config.ConnectionStringEnvVar, - lastDependency); - - return builder.Build(); - } - - private static bool HasCodeChanges(BrownfieldFindings? findings) - { - if (findings == null) - return false; - - var opts = findings.ServiceOptions; - if (opts != null) - { - // Check for removed properties - if (opts.InstrumentationKey != null) - return true; - if (opts.EnableAdaptiveSampling != null) - return true; - if (opts.DeveloperMode != null) - return true; - if (opts.EndpointAddress != null) - return true; - if (opts.EnableHeartbeat != null) - return true; - if (opts.EnableDebugLogger != null) - return true; - if (opts.RequestCollectionOptions != null) - return true; - if (opts.DependencyCollectionOptions != null) - return true; - - // Check for removed extension methods - if (opts.UseApplicationInsights == true) - return true; - if (opts.AddTelemetryProcessor == true) - return true; - if (opts.ConfigureTelemetryModule == true) - return true; - } - - if (findings.Initializers is { Found: true }) - return true; - if (findings.Processors is { Found: true }) + if (opts.RequestCollectionOptions != null) return true; - // TelemetryClient still works in 3.x — no code changes needed - if (findings.Sampling is { HasCustomSampling: true }) + if (opts.UseApplicationInsights == true) return true; - return false; } - private static string AddServiceOptionsActions( + protected override string AddServiceOptionsActions( OnboardingSpecBuilder builder, ServiceOptionsFindings opts, string entryPoint, string lastDependency) { - var dep = lastDependency; - - // Instrumentation key → connection string migration - if (opts.InstrumentationKey != null) - { - var actionId = "migrate-ikey"; - builder.AddManualStepAction( - actionId, - "Replace InstrumentationKey with ConnectionString", - $"In {entryPoint}, inside the AddApplicationInsightsTelemetry options block, " + - $"remove the line `options.InstrumentationKey = \"{opts.InstrumentationKey}\";` and replace it with " + - "`options.ConnectionString = \"InstrumentationKey=...;IngestionEndpoint=...\";` " + - "(use your actual connection string). " + - "Alternatively, remove the InstrumentationKey line entirely and set the " + - "APPLICATIONINSIGHTS_CONNECTION_STRING environment variable instead.", - dependsOn: dep); - dep = actionId; - } + var dep = AddSharedServiceOptionsActions(builder, opts, entryPoint, EntryPointMethodName, lastDependency); - // Removed properties — generate delete actions + // ASP.NET Core removed properties var removedProperties = new List<(string name, object? value)> { ("EnableAdaptiveSampling", opts.EnableAdaptiveSampling), @@ -240,7 +64,7 @@ private static string AddServiceOptionsActions( builder.AddManualStepAction( actionId, "Remove deprecated ApplicationInsightsServiceOptions properties", - $"In {entryPoint}, remove these properties from the AddApplicationInsightsTelemetry options block — they are removed in 3.x: {string.Join(", ", removedFound)}", + $"In {entryPoint}, remove these properties from the {EntryPointMethodName} options block — they are removed in 3.x: {string.Join(", ", removedFound)}", dependsOn: dep); dep = actionId; } @@ -248,7 +72,7 @@ private static string AddServiceOptionsActions( return dep; } - private static string AddRemovedMethodActions( + protected override string AddRemovedMethodActions( OnboardingSpecBuilder builder, ServiceOptionsFindings opts, string entryPoint, @@ -256,6 +80,7 @@ private static string AddRemovedMethodActions( { var dep = lastDependency; + // ASP.NET Core-only: UseApplicationInsights() if (opts.UseApplicationInsights == true) { var actionId = "remove-use-appinsights"; @@ -267,117 +92,9 @@ private static string AddRemovedMethodActions( dep = actionId; } - if (opts.AddTelemetryProcessor == true) - { - var actionId = "remove-add-processor-ext"; - builder.AddManualStepAction( - actionId, - "Remove AddApplicationInsightsTelemetryProcessor() call", - $"In {entryPoint}, remove the call to AddApplicationInsightsTelemetryProcessor() — it is removed in 3.x. Convert to an OpenTelemetry processor instead.", - dependsOn: dep); - dep = actionId; - } - - if (opts.ConfigureTelemetryModule == true) - { - var actionId = "remove-configure-module"; - builder.AddManualStepAction( - actionId, - "Remove ConfigureTelemetryModule() call", - $"In {entryPoint}, remove the call to ConfigureTelemetryModule() — it is removed in 3.x.", - dependsOn: dep); - dep = actionId; - } + // Shared removed methods + dep = AddSharedRemovedMethodActions(builder, opts, entryPoint, EntryPointMethodName, dep); return dep; } - - private static string AddInitializerActions( - OnboardingSpecBuilder builder, - InitializerFindings initializers, - string lastDependency) - { - var dep = lastDependency; - - foreach (var init in initializers.Implementations) - { - var actionId = $"migrate-initializer-{init.ClassName.ToLowerInvariant()}"; - var purpose = !string.IsNullOrWhiteSpace(init.Purpose) ? $" ({init.Purpose})" : ""; - - // Find the matching DI registration for this initializer, if captured - var registration = initializers.Registrations - .FirstOrDefault(r => r.Contains(init.ClassName, StringComparison.OrdinalIgnoreCase)); - var removeRegistration = registration != null - ? $" Remove the old DI registration: `{registration}` — ITelemetryInitializer no longer exists in 3.x." - : " Also remove the old AddSingleton() DI registration — ITelemetryInitializer no longer exists in 3.x."; - - builder.AddManualStepAction( - actionId, - $"Convert ITelemetryInitializer '{init.ClassName}' to OpenTelemetry processor", - $"Convert {init.ClassName}{purpose} from ITelemetryInitializer to a BaseProcessor.OnStart implementation. " + - $"File: {init.File ?? "unknown"}. " + - "The initializer's Initialize(ITelemetry) method should become OnStart(Activity). " + - "If the initializer touched all telemetry (not just RequestTelemetry/DependencyTelemetry), also create a BaseProcessor.OnEnd for the log side — see LogProcessors.md. " + - "Register the new processor(s) via .AddProcessor() in the OpenTelemetry pipeline setup." + - removeRegistration, - dependsOn: dep); - dep = actionId; - } - - return dep; - } - - private static string AddProcessorActions( - OnboardingSpecBuilder builder, - ProcessorFindings processors, - string lastDependency) - { - var dep = lastDependency; - - foreach (var proc in processors.Implementations) - { - var actionId = $"migrate-processor-{proc.ClassName.ToLowerInvariant()}"; - var purpose = !string.IsNullOrWhiteSpace(proc.Purpose) ? $" ({proc.Purpose})" : ""; - - // Find the matching registration for this processor, if captured - var registration = processors.Registrations - .FirstOrDefault(r => r.Contains(proc.ClassName, StringComparison.OrdinalIgnoreCase)); - var removeRegistration = registration != null - ? $" Remove the old registration: `{registration}` — ITelemetryProcessor no longer exists in 3.x." - : " Also remove any old ITelemetryProcessor DI registration — ITelemetryProcessor no longer exists in 3.x."; - - builder.AddManualStepAction( - actionId, - $"Convert ITelemetryProcessor '{proc.ClassName}' to OpenTelemetry processor", - $"Convert {proc.ClassName}{purpose} from ITelemetryProcessor to a BaseProcessor.OnEnd implementation. " + - $"File: {proc.File ?? "unknown"}. " + - "The processor's Process(ITelemetry) method should become OnEnd(Activity). " + - "To drop telemetry, clear the Recorded flag: data.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded. " + - "If the processor also handled TraceTelemetry/EventTelemetry, use ILoggingBuilder.AddFilter for log filtering or create a BaseProcessor.OnEnd for log enrichment — see LogProcessors.md. " + - "Register the new processor(s) via .AddProcessor() in the OpenTelemetry pipeline setup." + - removeRegistration, - dependsOn: dep); - dep = actionId; - } - - return dep; - } - - private static string AddSamplingActions( - OnboardingSpecBuilder builder, - SamplingFindings sampling, - string lastDependency) - { - var details = !string.IsNullOrWhiteSpace(sampling.Details) ? $" Current config: {sampling.Details}" : ""; - var actionId = "migrate-sampling"; - builder.AddManualStepAction( - actionId, - "Migrate custom sampling configuration to OpenTelemetry", - $"Replace the existing {sampling.Type ?? "custom"} sampling configuration with OpenTelemetry sampling.{details} " + - $"File: {sampling.File ?? "unknown"}. " + - "Use TracesPerSecond or SamplingRatio in the new AddApplicationInsightsTelemetry options, " + - "or configure a custom OTel sampler via .SetSampler().", - dependsOn: lastDependency); - return actionId; - } } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/BrownfieldGeneratorBase.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/BrownfieldGeneratorBase.cs new file mode 100644 index 0000000000..9e0c9508bf --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/BrownfieldGeneratorBase.cs @@ -0,0 +1,689 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Shared base for Application Insights 2.x → 3.x brownfield generators. +/// Contains logic that is identical across ASP.NET Core and Worker Service: +/// TelemetryClient breaking methods, initializer/processor migration, sampling. +/// Subclasses provide package names, migration docs, and service-options handling. +/// +public abstract class BrownfieldGeneratorBase : IGenerator +{ + // ── Subclass contracts ────────────────────────────────────────────── + + public abstract bool CanHandle(Analysis analysis); + + /// App-type filter for project lookup. + protected abstract AppType TargetAppType { get; } + + /// Find the matching project. Override for multi-AppType matching (e.g. classic ASP.NET). + protected virtual ProjectInfo FindProject(Analysis analysis) + => analysis.Projects.First(p => p.AppType == TargetAppType); + + /// NuGet package name, e.g. "Microsoft.ApplicationInsights.AspNetCore". + protected abstract string PackageName { get; } + + /// Target version specifier, e.g. "3.*". + protected abstract string PackageVersion { get; } + + /// Learning resource URI for the code-change migration doc. + protected abstract string MigrationCodeResource { get; } + + /// Learning resource URI for the no-code-change migration doc. + protected abstract string MigrationNoCodeChangeResource { get; } + + /// Human-readable entry point method name (for action descriptions). + protected abstract string EntryPointMethodName { get; } + + /// Package manager type for install command. Default "nuget" (dotnet add package). Override to "nuget-vs" for classic ASP.NET. + protected virtual string PackageManagerType => Packages.PackageManagerNuGet; + + /// Default entry point file name. Override for classic ASP.NET (Global.asax.cs). + protected virtual string DefaultEntryPoint => "Program.cs"; + + /// Check for framework-specific removed properties / extension methods. + protected abstract bool HasFrameworkSpecificCodeChanges(ServiceOptionsFindings opts); + + /// Generate migration actions for framework-specific removed properties. + protected abstract string AddServiceOptionsActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency); + + /// Generate migration actions for framework-specific removed extension methods. + protected abstract string AddRemovedMethodActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency); + + // ── Shared implementation ─────────────────────────────────────────── + + public OnboardingSpec Generate(Analysis analysis) + { + var findings = analysis.BrownfieldFindings; + var project = FindProject(analysis); + var projectFile = project.ProjectFile; + var entryPoint = project.EntryPoint ?? DefaultEntryPoint; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction); + + var needsCodeChange = HasCodeChanges(findings); + + if (!needsCodeChange) + { + return builder + .WithDecision( + Intents.Migrate, + Approaches.ApplicationInsights3x, + "Existing Application Insights SDK detected with no removed properties or custom code. Package upgrade only.") + .AddReviewEducationAction( + "review-migration", + "Review the no-code-change migration guide", + [MigrationNoCodeChangeResource]) + .AddPackageAction( + "upgrade-appinsights", + $"Upgrade {PackageName} to 3.x", + PackageManagerType, + PackageName, + PackageVersion, + projectFile, + "review-migration") + .Build(); + } + + // Code-change path + builder.WithDecision( + Intents.Migrate, + Approaches.ApplicationInsights3x, + "Existing Application Insights SDK detected with properties/patterns that require code changes for migration."); + + var learnResources = BuildLearnResources(findings); + + builder.AddReviewEducationAction( + "review-migration", + "Review the migration guide and relevant API references", + learnResources); + + builder.AddPackageAction( + "upgrade-appinsights", + $"Upgrade {PackageName} to 3.x", + PackageManagerType, + PackageName, + PackageVersion, + projectFile, + "review-migration"); + + var lastDependency = "upgrade-appinsights"; + + if (findings?.ServiceOptions != null) + { + lastDependency = AddServiceOptionsActions(builder, findings.ServiceOptions, entryPoint, lastDependency); + lastDependency = AddRemovedMethodActions(builder, findings.ServiceOptions, entryPoint, lastDependency); + } + + if (findings?.Initializers is { Found: true, Implementations.Count: > 0 }) + lastDependency = AddInitializerActions(builder, findings.Initializers, lastDependency); + + if (findings?.Processors is { Found: true, Implementations.Count: > 0 }) + lastDependency = AddProcessorActions(builder, findings.Processors, lastDependency); + + if (HasBreakingClientUsage(findings?.ClientUsage)) + lastDependency = AddClientUsageActions(builder, findings!.ClientUsage!, lastDependency); + + if (findings?.Sampling is { HasCustomSampling: true }) + { + // Only generate a sampling action if it's truly custom sampling, + // not just EnableAdaptiveSampling (which is handled by remove-deprecated-options) + var isJustAdaptiveSamplingFlag = findings.Sampling.Type?.Contains("adaptive", StringComparison.OrdinalIgnoreCase) == true + && findings.ServiceOptions?.EnableAdaptiveSampling != null; + + if (!isJustAdaptiveSamplingFlag) + lastDependency = AddSamplingActions(builder, findings.Sampling, lastDependency); + } + + if (findings?.TelemetryPipeline is { Found: true }) + lastDependency = AddTelemetryPipelineActions(builder, findings.TelemetryPipeline, lastDependency); + + if (findings?.Logging is { Found: true }) + lastDependency = AddLoggingActions(builder, findings.Logging, lastDependency); + + // Connection string config — virtual so classic ASP.NET can override + AddConnectionStringAction(builder, findings, lastDependency); + + return builder.Build(); + } + + /// Add the connection string config step. Override for non-JSON config (e.g. classic ASP.NET uses applicationinsights.config). + protected virtual void AddConnectionStringAction(OnboardingSpecBuilder builder, BrownfieldFindings? findings, string lastDependency) + { + var hasIkeyInCode = findings?.ServiceOptions?.InstrumentationKey != null; + var configDescription = hasIkeyInCode + ? "Replace InstrumentationKey with ConnectionString in appsettings.json (remove the old ApplicationInsights.InstrumentationKey entry)" + : "Configure Azure Monitor connection string"; + + builder.AddConfigAction( + "add-connection-string", + configDescription, + Config.AppSettingsFileName, + Config.AppInsightsConnectionStringPath, + Config.ConnectionStringPlaceholder, + Config.ConnectionStringEnvVar, + lastDependency); + } + + // ── Shared helpers ────────────────────────────────────────────────── + + protected bool HasCodeChanges(BrownfieldFindings? findings) + { + if (findings == null) + return false; + + var opts = findings.ServiceOptions; + if (opts != null) + { + // Shared removed properties + if (opts.InstrumentationKey != null) + return true; + if (opts.EnableAdaptiveSampling != null) + return true; + if (opts.DeveloperMode != null) + return true; + if (opts.EndpointAddress != null) + return true; + if (opts.EnableHeartbeat != null) + return true; + if (opts.EnableDebugLogger != null) + return true; + if (opts.DependencyCollectionOptions != null) + return true; + + // Shared removed extension methods + if (opts.AddTelemetryProcessor == true) + return true; + if (opts.ConfigureTelemetryModule == true) + return true; + if (opts.UsesInstrumentationKeyOverload == true) + return true; + + // Framework-specific checks + if (HasFrameworkSpecificCodeChanges(opts)) + return true; + } + + if (findings.Initializers is { Found: true }) + return true; + if (findings.Processors is { Found: true }) + return true; + if (HasBreakingClientUsage(findings.ClientUsage)) + return true; + if (findings.Sampling is { HasCustomSampling: true }) + return true; + if (findings.TelemetryPipeline is { Found: true }) + return true; + if (findings.Logging is { Found: true }) + return true; + + return false; + } + + /// Build the list of learn resources based on findings. Override for framework-specific resource swaps. + protected virtual List BuildLearnResources(BrownfieldFindings? findings) + { + var resources = new List { MigrationCodeResource }; + + if (findings?.Sampling is { HasCustomSampling: true }) + resources.Add(LearningResources.ApiSampling); + + if (findings?.Initializers is { Found: true, Implementations.Count: > 0 } + || findings?.Processors is { Found: true, Implementations.Count: > 0 }) + { + resources.Add(LearningResources.ApiActivityProcessors); + resources.Add(LearningResources.ApiLogProcessors); + resources.Add(LearningResources.ApiConfigureOpenTelemetryProvider); + } + + if (HasBreakingClientUsage(findings?.ClientUsage)) + resources.Add(LearningResources.ApiTelemetryClient); + + // Surface AAD migration doc when an initializer or processor related to + // credential / AAD / token authentication is detected (e.g. TelemetryConfigurationEnricher + // that calls SetAzureTokenCredential). + if (HasAadRelatedCustomizations(findings)) + resources.Add(LearningResources.MigrationAadAuthentication); + + if (findings?.Logging is { Found: true }) + resources.Add(LearningResources.MigrationILoggerMigration); + + return resources; + } + + /// + /// Returns true if any initializer or processor appears related to AAD / credential / token + /// authentication based on its class name or stated purpose. + /// + protected static bool HasAadRelatedCustomizations(BrownfieldFindings? findings) + { + static bool IsAadRelated(string? text) + => text != null && ( + text.Contains("aad", StringComparison.OrdinalIgnoreCase) || + text.Contains("credential", StringComparison.OrdinalIgnoreCase) || + text.Contains("token", StringComparison.OrdinalIgnoreCase) || + text.Contains("SetAzureTokenCredential", StringComparison.OrdinalIgnoreCase) || + text.Contains("Entra", StringComparison.OrdinalIgnoreCase)); + + if (findings?.Initializers is { Found: true, Implementations.Count: > 0 }) + { + if (findings.Initializers.Implementations.Any(i => + IsAadRelated(i.ClassName) || IsAadRelated(i.Purpose))) + return true; + } + + if (findings?.Processors is { Found: true, Implementations.Count: > 0 }) + { + if (findings.Processors.Implementations.Any(p => + IsAadRelated(p.ClassName) || IsAadRelated(p.Purpose))) + return true; + } + + // Also check serviceOptions — Console apps may call SetAzureTokenCredential + // directly on TelemetryConfiguration without a wrapper class + if (IsAadRelated(findings?.ServiceOptions?.SetupPattern)) + return true; + + return false; + } + + // ── TelemetryClient ───────────────────────────────────────────────── + + /// + /// Methods on TelemetryClient that have removed overloads or are entirely removed in 3.x. + /// + protected static readonly HashSet BreakingClientMethods = new(StringComparer.OrdinalIgnoreCase) + { + "TrackPageView", + "TrackEvent", + "TrackException", + "TrackAvailability", + "TrackDependency", + "GetMetric", + }; + + protected static bool HasBreakingClientUsage(ClientUsageFindings? clientUsage) + { + if (clientUsage is not { DirectUsage: true, Usages.Count: > 0 }) + return false; + + return clientUsage.Usages + .SelectMany(u => u.Methods) + .Any(m => BreakingClientMethods.Contains(m)); + } + + protected static string AddClientUsageActions( + OnboardingSpecBuilder builder, + ClientUsageFindings clientUsage, + string lastDependency) + { + var dep = lastDependency; + + foreach (var usage in clientUsage.Usages) + { + var breakingMethods = usage.Methods + .Where(m => BreakingClientMethods.Contains(m)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (breakingMethods.Count == 0) + continue; + + var fileName = Path.GetFileNameWithoutExtension(usage.File ?? "unknown").ToLowerInvariant(); + var actionId = $"fix-telemetryclient-{fileName}"; + + var instructions = new List(); + foreach (var method in breakingMethods) + { + instructions.Add(method switch + { + "TrackPageView" => + "TrackPageView is removed in 3.x — replace with TrackEvent(name, properties) or TrackRequest.", + "TrackEvent" => + "TrackEvent 3-param overload (with IDictionary metrics) is removed — " + + "remove the metrics dictionary parameter. Track metrics separately via TrackMetric().", + "TrackException" => + "TrackException 3-param overload (with IDictionary metrics) is removed — " + + "remove the metrics dictionary parameter. Track metrics separately via TrackMetric().", + "TrackAvailability" => + "TrackAvailability 8-param overload (with trailing IDictionary metrics) is removed — " + + "remove the metrics dictionary parameter. Track metrics separately via TrackMetric().", + "TrackDependency" => + "The obsolete 5-param TrackDependency overload is removed — " + + "use the full overload with dependencyTypeName, target, data, startTime, duration, success.", + "GetMetric" => + "GetMetric overloads with MetricConfiguration / MetricAggregationScope parameters are removed — " + + "use the simplified GetMetric(metricId) or GetMetric(metricId, dim1, ...).", + _ => $"{method} has breaking changes in 3.x — review TelemetryClient.md." + }); + } + + builder.AddManualStepAction( + actionId, + $"Fix TelemetryClient breaking calls in {usage.File ?? "unknown"}", + $"File: {usage.File ?? "unknown"}. " + + $"The following TelemetryClient methods have breaking changes in 3.x: {string.Join(", ", breakingMethods)}. " + + string.Join(" ", instructions), + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + // ── Initializers ──────────────────────────────────────────────────── + + protected static string AddInitializerActions( + OnboardingSpecBuilder builder, + InitializerFindings initializers, + string lastDependency) + { + var dep = lastDependency; + + foreach (var init in initializers.Implementations) + { + // AAD-related entries get a different migration action — they're not real + // ITelemetryInitializer implementations but IConfigureOptions + // or direct SetAzureTokenCredential calls reported for AAD doc surfacing. + if (IsAadRelatedEntry(init)) + { + var aadActionId = $"migrate-aad-{init.ClassName.ToLowerInvariant()}"; + + var registration = initializers.Registrations + .FirstOrDefault(r => r.Contains(init.ClassName, StringComparison.OrdinalIgnoreCase)); + var removeRegistration = registration != null + ? $" Remove the DI registration: `{registration}`." + : ""; + + builder.AddManualStepAction( + aadActionId, + $"Migrate AAD authentication ({init.ClassName})", + $"File: {init.File ?? "unknown"}. {init.Purpose ?? "AAD authentication configuration"}. " + + "In 3.x, for DI scenarios use options.Credential on ApplicationInsightsServiceOptions. " + + "For non-DI (Console) scenarios, SetAzureTokenCredential(TokenCredential) is available — " + + "note the parameter type changed from object (2.x) to TokenCredential (3.x). " + + "See the AAD Authentication Migration guide for details." + + removeRegistration, + dependsOn: dep); + dep = aadActionId; + continue; + } + + var actionId = $"migrate-initializer-{init.ClassName.ToLowerInvariant()}"; + var purpose = !string.IsNullOrWhiteSpace(init.Purpose) ? $" ({init.Purpose})" : ""; + + var reg = initializers.Registrations + .FirstOrDefault(r => r.Contains(init.ClassName, StringComparison.OrdinalIgnoreCase)); + var removeReg = reg != null + ? $" Remove the old DI registration: `{reg}` — ITelemetryInitializer no longer exists in 3.x." + : " Also remove the old AddSingleton() DI registration — ITelemetryInitializer no longer exists in 3.x."; + + builder.AddManualStepAction( + actionId, + $"Convert ITelemetryInitializer '{init.ClassName}' to OpenTelemetry processor", + $"Convert {init.ClassName}{purpose} from ITelemetryInitializer to a BaseProcessor.OnStart implementation. " + + $"File: {init.File ?? "unknown"}. " + + "The initializer's Initialize(ITelemetry) method should become OnStart(Activity). " + + "If the initializer touched all telemetry (not just RequestTelemetry/DependencyTelemetry), also create a BaseProcessor.OnEnd for the log side — see LogProcessors.md. " + + "Register the new processor(s) via .AddProcessor() in the OpenTelemetry pipeline setup." + + removeReg, + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + /// + /// Returns true if an initializer entry is actually an AAD/credential related + /// configuration (IConfigureOptions or direct SetAzureTokenCredential call) + /// rather than a real ITelemetryInitializer. + /// + private static bool IsAadRelatedEntry(InitializerInfo init) + { + static bool Contains(string? text, string term) + => text?.Contains(term, StringComparison.OrdinalIgnoreCase) == true; + + return Contains(init.ClassName, "credential") || + Contains(init.ClassName, "aad") || + Contains(init.ClassName, "Entra") || + Contains(init.Purpose, "SetAzureTokenCredential") || + Contains(init.Purpose, "AAD") || + Contains(init.Purpose, "IConfigureOptions"); + } + + // ── Processors ────────────────────────────────────────────────────── + + protected static string AddProcessorActions( + OnboardingSpecBuilder builder, + ProcessorFindings processors, + string lastDependency) + { + var dep = lastDependency; + + foreach (var proc in processors.Implementations) + { + var actionId = $"migrate-processor-{proc.ClassName.ToLowerInvariant()}"; + var purpose = !string.IsNullOrWhiteSpace(proc.Purpose) ? $" ({proc.Purpose})" : ""; + + var registration = processors.Registrations + .FirstOrDefault(r => r.Contains(proc.ClassName, StringComparison.OrdinalIgnoreCase)); + var removeRegistration = registration != null + ? $" Remove the old registration: `{registration}` — ITelemetryProcessor no longer exists in 3.x." + : " Also remove any old ITelemetryProcessor DI registration — ITelemetryProcessor no longer exists in 3.x."; + + builder.AddManualStepAction( + actionId, + $"Convert ITelemetryProcessor '{proc.ClassName}' to OpenTelemetry processor", + $"Convert {proc.ClassName}{purpose} from ITelemetryProcessor to a BaseProcessor.OnEnd implementation. " + + $"File: {proc.File ?? "unknown"}. " + + "The processor's Process(ITelemetry) method should become OnEnd(Activity). " + + "To drop telemetry, clear the Recorded flag: data.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded. " + + "If the processor also handled TraceTelemetry/EventTelemetry, use ILoggingBuilder.AddFilter for log filtering or create a BaseProcessor.OnEnd for log enrichment — see LogProcessors.md. " + + "Register the new processor(s) via .AddProcessor() in the OpenTelemetry pipeline setup." + + removeRegistration, + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + // ── Sampling ──────────────────────────────────────────────────────── + + protected string AddSamplingActions( + OnboardingSpecBuilder builder, + SamplingFindings sampling, + string lastDependency) + { + var details = !string.IsNullOrWhiteSpace(sampling.Details) ? $" Current config: {sampling.Details}" : ""; + var actionId = "migrate-sampling"; + builder.AddManualStepAction( + actionId, + "Migrate custom sampling configuration to OpenTelemetry", + $"Replace the existing {sampling.Type ?? "custom"} sampling configuration with OpenTelemetry sampling.{details} " + + $"File: {sampling.File ?? "unknown"}. " + + $"Use TracesPerSecond or SamplingRatio in the new {EntryPointMethodName} options, " + + "or configure a custom OTel sampler via .SetSampler().", + dependsOn: lastDependency); + return actionId; + } + + // ── Shared service-options helpers ─────────────────────────────────── + + /// Generate actions for shared removed properties (IKey, adaptive sampling, etc.). + protected static string AddSharedServiceOptionsActions( + OnboardingSpecBuilder builder, + ServiceOptionsFindings opts, + string entryPoint, + string entryPointMethodName, + string lastDependency) + { + var dep = lastDependency; + + if (opts.InstrumentationKey != null) + { + var actionId = "migrate-ikey"; + builder.AddManualStepAction( + actionId, + "Replace InstrumentationKey with ConnectionString", + $"In {entryPoint}, inside the {entryPointMethodName} options block, " + + $"remove the line `options.InstrumentationKey = \"{opts.InstrumentationKey}\";` and replace it with " + + "`options.ConnectionString = \"InstrumentationKey=...;IngestionEndpoint=...\";` " + + "(use your actual connection string). " + + "Alternatively, remove the InstrumentationKey line entirely and set the " + + "APPLICATIONINSIGHTS_CONNECTION_STRING environment variable instead.", + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + /// Generate actions for shared removed extension methods. + protected static string AddSharedRemovedMethodActions( + OnboardingSpecBuilder builder, + ServiceOptionsFindings opts, + string entryPoint, + string entryPointMethodName, + string lastDependency) + { + var dep = lastDependency; + + if (opts.AddTelemetryProcessor == true) + { + var actionId = "remove-add-processor-ext"; + builder.AddManualStepAction( + actionId, + "Remove AddApplicationInsightsTelemetryProcessor() call", + $"In {entryPoint}, remove the call to AddApplicationInsightsTelemetryProcessor() — it is removed in 3.x. Convert to an OpenTelemetry processor instead.", + dependsOn: dep); + dep = actionId; + } + + if (opts.ConfigureTelemetryModule == true) + { + var actionId = "remove-configure-module"; + builder.AddManualStepAction( + actionId, + "Remove ConfigureTelemetryModule() call", + $"In {entryPoint}, remove the call to ConfigureTelemetryModule() — it is removed in 3.x.", + dependsOn: dep); + dep = actionId; + } + + if (opts.UsesInstrumentationKeyOverload == true) + { + var actionId = "remove-ikey-overload"; + builder.AddManualStepAction( + actionId, + "Replace instrumentation key string overload", + $"In {entryPoint}, the call to {entryPointMethodName}(\"ikey\") with a string argument is removed in 3.x. " + + $"Replace with the parameterless {entryPointMethodName}() and set ConnectionString via options or the " + + "APPLICATIONINSIGHTS_CONNECTION_STRING environment variable.", + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + // ── TelemetryChannel / TelemetrySinks ─────────────────────────────── + + protected static string AddTelemetryPipelineActions( + OnboardingSpecBuilder builder, + TelemetryPipelineFindings pipeline, + string lastDependency) + { + var dep = lastDependency; + + if (pipeline.HasCustomChannel) + { + var className = pipeline.ClassName ?? "custom ITelemetryChannel"; + var details = !string.IsNullOrWhiteSpace(pipeline.Details) ? $" Details: {pipeline.Details}" : ""; + var actionId = "remove-telemetry-channel"; + builder.AddManualStepAction( + actionId, + $"Remove custom TelemetryChannel usage ({className})", + $"The TelemetryChannel property on TelemetryConfiguration is removed in 3.x.{details} " + + $"File: {pipeline.File ?? "unknown"}. " + + "If using InMemoryChannel or ServerTelemetryChannel, remove the assignment — " + + "the 3.x SDK manages its own export pipeline internally via OpenTelemetry. " + + "If using a fully custom ITelemetryChannel, convert to an OpenTelemetry exporter instead.", + dependsOn: dep); + dep = actionId; + } + + if (pipeline.HasTelemetrySinks) + { + var actionId = "remove-telemetry-sinks"; + builder.AddManualStepAction( + actionId, + "Remove TelemetrySinks / DefaultTelemetrySink usage", + $"TelemetrySinks and DefaultTelemetrySink properties on TelemetryConfiguration are removed in 3.x. " + + $"File: {pipeline.File ?? "unknown"}. " + + "The 3.x SDK uses OpenTelemetry exporters internally. " + + "If you need to export to multiple destinations, configure additional OpenTelemetry exporters " + + "via ConfigureOpenTelemetryBuilder on TelemetryConfiguration or via the DI pipeline.", + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + // ── Logging ───────────────────────────────────────────────────────── + + protected static string AddLoggingActions( + OnboardingSpecBuilder builder, + LoggingFindings logging, + string lastDependency) + { + var dep = lastDependency; + + if (logging.HasExplicitLoggerProvider) + { + var actionId = "remove-explicit-logger-provider"; + builder.AddManualStepAction( + actionId, + "Remove explicit AddApplicationInsights() logger provider registration", + $"In {logging.File ?? "unknown"}, remove the call to AddApplicationInsights() on ILoggingBuilder " + + "(e.g. loggingBuilder.AddApplicationInsights() or services.AddLogging(b => b.AddApplicationInsights(...))). " + + "In 3.x, ILogger output is exported to Application Insights automatically — no explicit logger provider registration is needed. " + + "Also remove any ApplicationInsightsLoggerOptions configuration (e.g. TrackExceptionsAsExceptionTelemetry, IncludeScopes) — " + + "these options no longer exist.", + dependsOn: dep); + dep = actionId; + } + + if (logging.LogFilters.Count > 0) + { + var filterLines = string.Join("; ", logging.LogFilters); + var actionId = "remove-ai-log-filters"; + builder.AddManualStepAction( + actionId, + "Migrate AddFilter to OpenTelemetry", + $"In {logging.File ?? "unknown"}, replace the following log filter registrations: {filterLines}. " + + "ApplicationInsightsLoggerProvider no longer exists in 3.x — logging now flows through OpenTelemetryLoggerProvider. " + + "Replace AddFilter(...) with either: " + + "(1) AddFilter(category, level) to target the OTel provider specifically " + + "(requires 'using OpenTelemetry.Logs;'), or " + + "(2) AddFilter(category, level) for a provider-agnostic filter, or " + + "(3) configure log-level filtering in appsettings.json under the \"Logging:LogLevel\" section. " + + "Also replace 'using Microsoft.Extensions.Logging.ApplicationInsights;' with 'using OpenTelemetry.Logs;' if using option (1). " + + "For advanced log filtering (dropping specific log records), use a BaseProcessor " + + "registered via ConfigureOpenTelemetryLoggerProvider.", + dependsOn: dep); + dep = actionId; + } + + return dep; + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/BunyanNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/BunyanNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..a64540c8be --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/BunyanNodeJsGreenfieldGenerator.cs @@ -0,0 +1,79 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js Bunyan greenfield projects (no existing telemetry) +/// +public class BunyanNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var bunyanProjects = analysis.Projects + .Where(p => p.AppType == AppType.BunyanNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && bunyanProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.BunyanNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Node.js application with Bunyan logging. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests and can be configured to collect Bunyan logs.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleBunyanSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration with Bunyan log collection +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + instrumentationOptions: { + bunyan: { enabled: true } + } +});", + "// At the very top of the file, before any other imports", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ConsoleBrownfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ConsoleBrownfieldGenerator.cs new file mode 100644 index 0000000000..4e593a25ca --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ConsoleBrownfieldGenerator.cs @@ -0,0 +1,181 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Console app (and class library) brownfield projects migrating from +/// Application Insights 2.x to 3.x. Handles the non-DI pattern where TelemetryConfiguration +/// and TelemetryClient are created manually (not via AddApplicationInsightsTelemetry). +/// +public class ConsoleBrownfieldGenerator : BrownfieldGeneratorBase +{ + protected override AppType TargetAppType => AppType.Console; + protected override string PackageName => Packages.ApplicationInsightsCore; + protected override string PackageVersion => Packages.ApplicationInsightsCore3x; + protected override string MigrationCodeResource => LearningResources.MigrationConsole2xTo3xCode; + protected override string MigrationNoCodeChangeResource => LearningResources.MigrationConsole2xTo3xCode; // Always has code changes + protected override string EntryPointMethodName => "TelemetryConfiguration.CreateDefault"; + + protected override ProjectInfo FindProject(Analysis analysis) + => analysis.Projects.First(p => + p.AppType is AppType.Console or AppType.Library); + + public override bool CanHandle(Analysis analysis) + { + var consoleOrLibraryCount = analysis.Projects + .Count(p => p.AppType is AppType.Console or AppType.Library); + + // Don't match if this is a Console app using WorkerService package + // (those are handled by WorkerServiceBrownfieldGenerator) + var hasWorkerServicePackage = analysis.ExistingInstrumentation?.Evidence + .Any(e => e.Indicator.Contains("Microsoft.ApplicationInsights.WorkerService", StringComparison.OrdinalIgnoreCase)) == true; + + return analysis.Language == Language.DotNet + && consoleOrLibraryCount >= 1 + && !hasWorkerServicePackage + && analysis.State == InstrumentationState.Brownfield + && analysis.ExistingInstrumentation?.Type == InstrumentationType.ApplicationInsightsSdk + && analysis.ExistingInstrumentation?.IsTargetVersion != true + && analysis.BrownfieldFindings is not null; + } + + protected override List BuildLearnResources(BrownfieldFindings? findings) + { + var resources = new List { MigrationCodeResource }; + + // Always include non-DI extensibility doc + resources.Add(LearningResources.ApiConfigureOpenTelemetryBuilder); + + if (findings?.Initializers is { Found: true, Implementations.Count: > 0 } + || findings?.Processors is { Found: true, Implementations.Count: > 0 }) + { + resources.Add(LearningResources.ApiActivityProcessors); + resources.Add(LearningResources.ApiLogProcessors); + } + + if (HasBreakingClientUsage(findings?.ClientUsage)) + resources.Add(LearningResources.ApiTelemetryClient); + + if (HasAadRelatedCustomizations(findings)) + resources.Add(LearningResources.MigrationAadAuthentication); + + if (findings?.Logging is { Found: true }) + resources.Add(LearningResources.MigrationILoggerMigration); + + return resources; + } + + protected override bool HasFrameworkSpecificCodeChanges(ServiceOptionsFindings opts) + { + // Console apps always have code changes — TelemetryConfiguration setup must be updated + return true; + } + + protected override string AddServiceOptionsActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency) + { + var dep = AddSharedServiceOptionsActions(builder, opts, entryPoint, EntryPointMethodName, lastDependency); + + // Replace TelemetryConfiguration.Active with CreateDefault() + var actionId = "fix-telemetry-config-active"; + builder.AddManualStepAction( + actionId, + "Replace TelemetryConfiguration.Active with CreateDefault()", + $"In {entryPoint} and any other files, replace TelemetryConfiguration.Active with " + + "TelemetryConfiguration.CreateDefault(). Note: CreateDefault() returns a static singleton in 3.x. " + + "Also replace any config.InstrumentationKey assignments with config.ConnectionString. " + + "ConnectionString is required in 3.x — the SDK will throw if it is not set.", + dependsOn: dep); + dep = actionId; + + // Remove manual module initialization + actionId = "remove-manual-modules"; + builder.AddManualStepAction( + actionId, + "Remove manual TelemetryModule initialization", + $"In {entryPoint} and any telemetry setup files, remove manual initialization of " + + "DependencyTrackingTelemetryModule, PerformanceCollectorModule, and any other TelemetryModule " + + "instances (new ...Module() + .Initialize(config)). " + + "In 3.x, dependency tracking, performance counters, and request tracking are automatic — " + + "no manual module setup is needed. " + + "Also remove built-in initializers added manually: " + + "HttpDependenciesParsingTelemetryInitializer, OperationCorrelationTelemetryInitializer — " + + "correlation and HTTP parsing are automatic in 3.x.", + dependsOn: dep); + dep = actionId; + + // Remove config.TelemetryInitializers.Add() calls + actionId = "remove-config-initializers-add"; + builder.AddManualStepAction( + actionId, + "Migrate config.TelemetryInitializers.Add() calls", + $"In {entryPoint} and any telemetry setup files, the TelemetryInitializers collection on " + + "TelemetryConfiguration is removed in 3.x. " + + "For custom initializers added via config.TelemetryInitializers.Add(new MyInit()), " + + "convert them to BaseProcessor and register via: " + + "config.ConfigureOpenTelemetryBuilder(builder => builder.WithTracing(t => t.AddProcessor())). " + + "Important: add 'using OpenTelemetry;' — the WithTracing/WithLogging/ConfigureResource extension methods " + + "are in the root OpenTelemetry namespace (not OpenTelemetry.Trace). " + + "See the ActivityProcessors and TelemetryConfigurationBuilder API references.", + dependsOn: dep); + dep = actionId; + + // Remove DependencyCollector package + actionId = "remove-dependency-collector"; + builder.AddManualStepAction( + actionId, + "Remove Microsoft.ApplicationInsights.DependencyCollector package", + "Remove the Microsoft.ApplicationInsights.DependencyCollector NuGet package — " + + "dependency tracking is automatic in 3.x and this package is no longer needed. " + + "Run: dotnet remove package Microsoft.ApplicationInsights.DependencyCollector", + dependsOn: dep); + dep = actionId; + + return dep; + } + + protected override string AddRemovedMethodActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency) + { + // Console apps don't have UseApplicationInsights() or ConfigureTelemetryModule + // but may have TelemetryProcessorChainBuilder usage + var dep = lastDependency; + + if (opts.AddTelemetryProcessor == true) + { + var actionId = "remove-processor-chain-builder"; + builder.AddManualStepAction( + actionId, + "Remove TelemetryProcessorChainBuilder usage", + $"In {entryPoint}, remove any TelemetryProcessorChainBuilder or " + + "config.TelemetryProcessors usage — these are removed in 3.x. " + + "Convert to OpenTelemetry processors registered via " + + "config.ConfigureOpenTelemetryBuilder(builder => builder.WithTracing(t => t.AddProcessor())).", + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + protected override void AddConnectionStringAction(OnboardingSpecBuilder builder, BrownfieldFindings? findings, string lastDependency) + { + // Console apps set ConnectionString directly on TelemetryConfiguration, not via appsettings.json. + // The fix-telemetry-config-active step already covers this. + // Only add a reminder if they didn't have InstrumentationKey in code (so it wasn't already covered) + if (findings?.ServiceOptions?.InstrumentationKey == null) + { + builder.AddManualStepAction( + "add-connection-string", + "Set ConnectionString on TelemetryConfiguration", + "Ensure config.ConnectionString is set before creating TelemetryClient. " + + "ConnectionString is required in 3.x. Use the format: " + + "\"InstrumentationKey=...;IngestionEndpoint=...\" " + + "or set the APPLICATIONINSIGHTS_CONNECTION_STRING environment variable.", + dependsOn: lastDependency); + } + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ConsoleNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ConsoleNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..363e2cb3f1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ConsoleNodeJsGreenfieldGenerator.cs @@ -0,0 +1,74 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js Console greenfield projects (no existing telemetry) +/// +public class ConsoleNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var consoleProjects = analysis.Projects + .Where(p => p.AppType == AppType.ConsoleNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && consoleProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.ConsoleNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "applicationinsights", + "Node.js application using console logging. The applicationinsights package is required for automatic console log collection, as @azure/monitor-opentelemetry only supports Bunyan and Winston.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleConsoleNodeJsSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Application Insights package", + "npm", + "applicationinsights", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Application Insights at application startup", + entryPoint, + @"const appInsights = require('applicationinsights'); + +// Initialize Application Insights with console log collection +appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) + .setAutoCollectConsole(true, true) + .start();", + "// At the very top of the file, before any other imports", + "applicationinsights", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/DotNetEnhancementGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/DotNetEnhancementGenerator.cs new file mode 100644 index 0000000000..37f32b0561 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/DotNetEnhancementGenerator.cs @@ -0,0 +1,172 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for apps already on Application Insights 3.x or Azure Monitor Distro. +/// Confirms no migration needed and produces enhancement actions based on user selection. +/// +public class DotNetEnhancementGenerator : IGenerator +{ + /// + /// Supported enhancement options — each maps to a learn resource, package, and setup instruction. + /// + public static readonly Dictionary SupportedEnhancements = new(StringComparer.OrdinalIgnoreCase) + { + ["entityframework"] = new("Entity Framework Core Instrumentation", + Packages.EntityFrameworkInstrumentation, + [LearningResources.ApiEntityFrameworkInstrumentation, LearningResources.ApiConfigureOpenTelemetryProvider], + "Add .AddEntityFrameworkCoreInstrumentation() to the tracing pipeline."), + + ["redis"] = new("Redis Instrumentation (StackExchange.Redis)", + Packages.RedisInstrumentation, + [LearningResources.ApiRedisInstrumentation, LearningResources.ApiConfigureOpenTelemetryProvider], + "Add .AddRedisInstrumentation() to the tracing pipeline. Ensure IConnectionMultiplexer is registered in DI."), + + ["sqlclient"] = new("SQL Client Instrumentation", + Packages.SqlClientInstrumentation, + [LearningResources.ApiSqlClientInstrumentation, LearningResources.ApiConfigureOpenTelemetryProvider], + "Add .AddSqlClientInstrumentation() to the tracing pipeline."), + + ["http"] = new("HTTP Client/Server Enrichment", + Packages.HttpInstrumentation, + [LearningResources.ApiHttpInstrumentation, LearningResources.ApiConfigureOpenTelemetryProvider], + "Customize HTTP client/server instrumentation with enrichment callbacks, filters, and RecordException."), + + ["otlp"] = new("OTLP Exporter", + Packages.OtlpExporter, + [LearningResources.ApiOtlpExporter, LearningResources.ApiConfigureOpenTelemetryProvider], + "Add .AddOtlpExporter() to each signal pipeline (traces, metrics, logs) for dual export."), + + ["console"] = new("Console Exporter (dev only)", + Packages.ConsoleExporter, + [LearningResources.ApiConsoleExporter, LearningResources.ApiConfigureOpenTelemetryProvider], + "Add .AddConsoleExporter() to each signal pipeline for local debugging output."), + + ["sampling"] = new("Sampling Configuration", + null, + [LearningResources.ApiSampling], + "Configure TracesPerSecond or SamplingRatio in service options, or use the OTel pipeline."), + + ["processors"] = new("Custom Processors", + null, + [LearningResources.ApiActivityProcessors, LearningResources.ApiLogProcessors, LearningResources.ApiConfigureOpenTelemetryProvider], + "Create BaseProcessor for trace enrichment/filtering and BaseProcessor for log enrichment."), + }; + + public bool CanHandle(Analysis analysis) + { + if (analysis.Language != Language.DotNet) + return false; + + var hasDotNetProject = analysis.Projects.Any(p => + p.AppType == AppType.AspNetCore || p.AppType == AppType.Worker); + if (!hasDotNetProject) + return false; + + if (analysis.State != InstrumentationState.Brownfield) + return false; + + // Match: AI SDK 3.x (IsTargetVersion) or Azure Monitor Distro + var instr = analysis.ExistingInstrumentation; + if (instr == null) + return false; + + return (instr.Type == InstrumentationType.ApplicationInsightsSdk && instr.IsTargetVersion) + || instr.Type == InstrumentationType.AzureMonitorDistro; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => + p.AppType == AppType.AspNetCore || p.AppType == AppType.Worker); + + var sdkLabel = analysis.ExistingInstrumentation?.Type == InstrumentationType.AzureMonitorDistro + ? "Azure Monitor Distro" + : $"Application Insights 3.x ({analysis.ExistingInstrumentation?.Version})"; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Enhance, + Approaches.ApplicationInsights3x, + $"Already on {sdkLabel}. No migration needed. Enhancement options available."); + + return builder.Build(); + } + + /// + /// Generate actions for one or more enhancement selections. + /// Called by the SendEnhancedSelectionTool after user picks. + /// + public static OnboardingSpec GenerateForSelections( + Analysis analysis, + List<(string key, EnhancementOption option)> selections) + { + var project = analysis.Projects.First(p => + p.AppType == AppType.AspNetCore || p.AppType == AppType.Worker); + var entryPoint = project.EntryPoint ?? "Program.cs"; + + var displayNames = string.Join(" + ", selections.Select(s => s.option.DisplayName)); + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Enhance, + Approaches.ApplicationInsights3x, + $"Adding {displayNames}."); + + // Collect all learn resources (deduplicated) + var allResources = selections + .SelectMany(s => s.option.LearnResources) + .Distinct() + .ToList(); + + builder.AddReviewEducationAction( + "review-enhancement", + $"Review the guides for: {displayNames}", + allResources); + + var lastDep = "review-enhancement"; + + // Package actions for each enhancement that needs one + foreach (var (key, option) in selections) + { + if (option.PackageName != null) + { + var actionId = $"add-package-{key}"; + builder.AddPackageAction( + actionId, + $"Install {option.PackageName}", + Packages.PackageManagerNuGet, + option.PackageName, + Packages.LatestStableVersion, + project.ProjectFile, + lastDep); + lastDep = actionId; + } + } + + // Setup instruction for each enhancement + foreach (var (key, option) in selections) + { + var actionId = $"configure-{key}"; + builder.AddManualStepAction( + actionId, + $"Configure {option.DisplayName}", + $"In {entryPoint}: {option.SetupInstruction} " + + $"Refer to the {option.DisplayName} guide for options and examples.", + dependsOn: lastDep); + lastDep = actionId; + } + + return builder.Build(); + } +} + +public record EnhancementOption( + string DisplayName, + string? PackageName, + string[] LearnResources, + string SetupInstruction); diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ExpressGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ExpressGreenfieldGenerator.cs new file mode 100644 index 0000000000..42ba3c1b32 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/ExpressGreenfieldGenerator.cs @@ -0,0 +1,77 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Express.js greenfield projects (no existing telemetry) +/// +public class ExpressGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Single Express project, greenfield + var expressProjects = analysis.Projects + .Where(p => p.AppType == AppType.Express) + .ToList(); + + return analysis.Language == Language.NodeJs + && expressProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.Express); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Express.js greenfield application. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, dependencies, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleExpressSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration - must be called before other requires +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// At the very top of the file, before other requires", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/FastifyGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/FastifyGreenfieldGenerator.cs new file mode 100644 index 0000000000..55684a66dd --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/FastifyGreenfieldGenerator.cs @@ -0,0 +1,77 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Fastify greenfield projects (no existing telemetry) +/// +public class FastifyGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Single Fastify project, greenfield + var fastifyProjects = analysis.Projects + .Where(p => p.AppType == AppType.Fastify) + .ToList(); + + return analysis.Language == Language.NodeJs + && fastifyProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.Fastify); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Fastify greenfield application. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, dependencies, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleFastifySetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration - must be called before other requires +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// At the very top of the file, before other requires", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/LangchainJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/LangchainJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..98edbcab4f --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/LangchainJsGreenfieldGenerator.cs @@ -0,0 +1,142 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for LangChain.js greenfield projects (no existing telemetry) +/// +public class LangchainJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Single LangChain.js project, greenfield + var langchainProjects = analysis.Projects + .Where(p => p.AppType == AppType.LangchainJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && langchainProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.LangchainJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + // Determine if the project uses ES modules or CommonJS + var isEsModule = IsEsModuleProject(packageJsonPath); + var tracingFileName = isEsModule ? "tracing.mjs" : "tracing.js"; + var tracingFile = Path.Combine(projectDir, tracingFileName); + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "LangChain.js greenfield application. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, LLM calls, and custom telemetry. A separate tracing file ensures proper initialization before LangChain imports.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ConceptsGenAiObservability, + LearningResources.ExampleLangchainJsSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "create-tracing-file", + $"Create {tracingFileName} file for OpenTelemetry initialization", + tracingFile, + GenerateTracingFileContent(isEsModule), + $"// Create new file at project root: {tracingFileName}", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddModifyCodeAction( + "import-tracing", + "Import tracing at the top of the entry point (must be first import)", + entryPoint, + GenerateTracingImport(isEsModule, tracingFileName), + "// Add as the very first import in the entry file", + "@azure/monitor-opentelemetry", + "create-tracing-file") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } + + private bool IsEsModuleProject(string packageJsonPath) + { + try + { + var content = File.ReadAllText(packageJsonPath); + using var doc = System.Text.Json.JsonDocument.Parse(content); + if (doc.RootElement.TryGetProperty("type", out var typeElement)) + { + return typeElement.GetString() == "module"; + } + } + catch + { + // Default to CommonJS if we can't parse + } + return false; + } + + private string GenerateTracingFileContent(bool isEsModule) + { + if (isEsModule) + { + return @"import { useAzureMonitor } from '@azure/monitor-opentelemetry'; + +// Enable Azure Monitor integration +// This must be called before any other imports to ensure proper instrumentation +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});"; + } + else + { + return @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +// This must be called before any other imports to ensure proper instrumentation +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});"; + } + } + + private string GenerateTracingImport(bool isEsModule, string tracingFileName) + { + if (isEsModule) + { + return $"import './{tracingFileName}';"; + } + else + { + return $"require('./{tracingFileName}');"; + } + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/MongoDBNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/MongoDBNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..9df902d077 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/MongoDBNodeJsGreenfieldGenerator.cs @@ -0,0 +1,77 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js MongoDB greenfield projects (no existing telemetry) +/// +public class MongoDBNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var mongoProjects = analysis.Projects + .Where(p => p.AppType == AppType.MongoDBNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && mongoProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.MongoDBNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Node.js application with MongoDB. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, MongoDB operations, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleMongoDBSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +// MongoDB operations will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// At the very top of the file, before any other imports", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/MySQLNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/MySQLNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..263aa133c7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/MySQLNodeJsGreenfieldGenerator.cs @@ -0,0 +1,77 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js MySQL greenfield projects (no existing telemetry) +/// +public class MySQLNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var mysqlProjects = analysis.Projects + .Where(p => p.AppType == AppType.MySQLNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && mysqlProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.MySQLNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Node.js application with MySQL. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, MySQL queries, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleMySQLSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +// MySQL queries will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// At the very top of the file, before any other imports", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/NestJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/NestJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..8642ebf79d --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/NestJsGreenfieldGenerator.cs @@ -0,0 +1,112 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for NestJS greenfield projects (no existing telemetry) +/// +public class NestJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Single NestJS project, greenfield + var nestjsProjects = analysis.Projects + .Where(p => p.AppType == AppType.NestJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && nestjsProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.NestJs); + var packageJsonPath = project.ProjectFile; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + // NestJS typically uses src/main.ts as entry point + var mainFile = DetectMainFile(projectDir); + var tracingFile = Path.Combine(projectDir, "src", "tracing.ts"); + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "NestJS greenfield application. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, dependencies, and custom telemetry. A separate tracing file is recommended for proper initialization.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleNestJsSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "create-tracing-file", + "Create tracing.ts file for OpenTelemetry initialization", + tracingFile, + @"import { useAzureMonitor } from '@azure/monitor-opentelemetry'; + +// Enable Azure Monitor integration +// This must be called before any other imports to ensure proper instrumentation +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// Create new file at src/tracing.ts", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddModifyCodeAction( + "import-tracing", + "Import tracing at the top of main.ts (must be first import)", + mainFile, + @"import './tracing';", + "// Add as the very first import in main.ts", + "@azure/monitor-opentelemetry", + "create-tracing-file") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } + + private string DetectMainFile(string projectDir) + { + // Check common NestJS entry points + var possiblePaths = new[] + { + Path.Combine(projectDir, "src", "main.ts"), + Path.Combine(projectDir, "src", "main.js"), + Path.Combine(projectDir, "main.ts"), + Path.Combine(projectDir, "main.js") + }; + + foreach (var path in possiblePaths) + { + if (File.Exists(path)) + { + return path; + } + } + + // Default to src/main.ts for NestJS + return Path.Combine(projectDir, "src", "main.ts"); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/NextJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/NextJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..89e32cc69b --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/NextJsGreenfieldGenerator.cs @@ -0,0 +1,116 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Next.js greenfield projects (no existing telemetry) +/// +public class NextJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Single Next.js project, greenfield + var nextjsProjects = analysis.Projects + .Where(p => p.AppType == AppType.NextJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && nextjsProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.NextJs); + var packageJsonPath = project.ProjectFile; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + // Next.js uses instrumentation.js/ts for OpenTelemetry setup + var instrumentationFile = Path.Combine(projectDir, "instrumentation.js"); + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Next.js greenfield application. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, dependencies, and custom telemetry via Next.js instrumentation hook.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleNextJsSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "create-instrumentation-file", + "Create instrumentation.js file for Next.js OpenTelemetry setup", + instrumentationFile, + @"import { useAzureMonitor } from '@azure/monitor-opentelemetry'; + +export function register() { + // Only initialize on server-side + if (process.env.NEXT_RUNTIME === 'nodejs') { + useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } + }); + } +}", + "// Create new file at project root", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddModifyCodeAction( + "enable-instrumentation", + "Enable instrumentation hook and externalize server-only packages in next.config.js", + Path.Combine(projectDir, "next.config.js"), + @"experimental: { + instrumentationHook: true, + serverComponentsExternalPackages: [ + '@azure/monitor-opentelemetry', + '@opentelemetry/sdk-node', + '@opentelemetry/api', + '@opentelemetry/instrumentation', + '@opentelemetry/exporter-logs-otlp-grpc', + '@opentelemetry/otlp-grpc-exporter-base', + '@grpc/grpc-js', + '@grpc/proto-loader', + ], +}, +webpack: (config, { isServer }) => { + if (isServer) { + config.externals = config.externals || []; + config.externals.push({ + '@azure/monitor-opentelemetry': 'commonjs @azure/monitor-opentelemetry', + '@opentelemetry/sdk-node': 'commonjs @opentelemetry/sdk-node', + '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', + '@opentelemetry/api': 'commonjs @opentelemetry/api', + '@grpc/grpc-js': 'commonjs @grpc/grpc-js', + }); + } + return config; +},", + "// Add to nextConfig object", + "next", + "create-instrumentation-file") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env.local"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/PostgresNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/PostgresNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..3a3013c830 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/PostgresNodeJsGreenfieldGenerator.cs @@ -0,0 +1,77 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js PostgreSQL greenfield projects (no existing telemetry) +/// +public class PostgresNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var postgresProjects = analysis.Projects + .Where(p => p.AppType == AppType.PostgresNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && postgresProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.PostgresNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Node.js application with PostgreSQL. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, PostgreSQL queries, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExamplePostgresSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +// PostgreSQL queries will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// At the very top of the file, before any other imports", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/PythonGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/PythonGreenfieldGenerator.cs new file mode 100644 index 0000000000..4733dabb41 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/PythonGreenfieldGenerator.cs @@ -0,0 +1,606 @@ +using Azure.Mcp.Tools.Monitor.Detectors; +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Python greenfield projects (no existing telemetry). +/// Supports Django, Flask, FastAPI, GenAI apps, and other Python frameworks. +/// +/// Based on Azure Monitor OpenTelemetry Distro for Python: +/// https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry +/// +public class PythonGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Python project, greenfield, with at least one recognized framework + if (analysis.Language != Language.Python) + return false; + + if (analysis.State != InstrumentationState.Greenfield) + return false; + + // Check if we have at least one project with a known app type + var knownAppTypes = new[] { AppType.Django, AppType.Flask, AppType.FastAPI, AppType.Falcon, AppType.Starlette, AppType.Console, AppType.GenAI }; + return analysis.Projects.Any(p => knownAppTypes.Contains(p.AppType)); + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType != AppType.Unknown); + var projectFile = project.ProjectFile; + var entryPoint = project.EntryPoint; + var projectDir = Path.GetDirectoryName(projectFile) ?? ""; + var appType = project.AppType; + var dependencies = project.Dependencies; + + // Get description based on app type + var description = appType switch + { + AppType.GenAI => "GenAI greenfield application. Azure Monitor OpenTelemetry Distro with GenAI instrumentations for tracing LLM calls, token usage, and model interactions.", + AppType.Console => "Generic Python console/script application. Azure Monitor OpenTelemetry Distro provides basic telemetry. Add library-specific instrumentations for HTTP clients, databases, etc.", + _ => $"{appType} greenfield application. Azure Monitor OpenTelemetry Distro provides automatic instrumentation for HTTP requests, database calls, and custom telemetry." + }; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + description); + + // Step 1: Review educational materials + builder.AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + GetEducationResources(appType)); + + // Step 2: Add azure-monitor-opentelemetry package + builder.AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "pip", + "azure-monitor-opentelemetry", + ">=1.8.3", + projectFile, + "review-education"); + + // Step 2b: Add GenAI instrumentation packages if needed + var lastPackageAction = "add-monitor-package"; + if (appType == AppType.GenAI) + { + var genaiPackages = GetGenAIInstrumentationPackages(dependencies); + for (int i = 0; i < genaiPackages.Count; i++) + { + var pkg = genaiPackages[i]; + var actionId = $"add-genai-package-{i + 1}"; + builder.AddPackageAction( + actionId, + $"Add {pkg.DisplayName} instrumentation package", + "pip", + pkg.InstrumentationPackage, + "latest", + projectFile, + lastPackageAction); + lastPackageAction = actionId; + } + } + + // Step 2c: Add Console app instrumentation packages if needed + if (appType == AppType.Console) + { + var consolePackages = GetConsoleInstrumentationPackages(dependencies); + for (int i = 0; i < consolePackages.Count; i++) + { + var pkg = consolePackages[i]; + var actionId = $"add-instrumentation-{i + 1}"; + builder.AddPackageAction( + actionId, + $"Add {pkg.DisplayName} instrumentation package", + "pip", + pkg.InstrumentationPackage, + "latest", + projectFile, + lastPackageAction); + lastPackageAction = actionId; + } + } + + // Step 3: Add instrumentation code to entry point + if (entryPoint != null) + { + builder.AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + GetInstrumentationCode(appType, dependencies), + GetInsertLocation(appType), + "azure.monitor.opentelemetry", + lastPackageAction); + } + else + { + // If no entry point found, add a manual step + builder.AddManualStepAction( + "configure-opentelemetry", + "Add Azure Monitor initialization to your application entry point", + GetManualInstructions(appType), + [ + "https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python" + ], + lastPackageAction); + } + + // Step 4: Configure connection string with full .env.example content + builder.AddConfigAction( + "add-env-config", + "Create .env.example with Azure Monitor configuration", + Path.Combine(projectDir, ".env.example"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + GetEnvFileContent(appType, dependencies), + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + // Step 5: Add PowerShell script for setting environment variables + builder.AddConfigAction( + "add-powershell-script", + "Create PowerShell script for setting environment variables", + Path.Combine(projectDir, "set-env.ps1"), + "script", + GetPowerShellScriptContent(appType, dependencies), + null); + + return builder.Build(); + } + + /// + /// Get education resources based on framework type. + /// + private static List GetEducationResources(AppType appType) + { + var resources = new List + { + LearningResources.ConceptsOpenTelemetryPipelinePython, + LearningResources.ConceptsAzureMonitorPython + }; + + // Add framework-specific example + var frameworkExample = appType switch + { + AppType.Django => LearningResources.ExampleDjangoSetup, + AppType.Flask => LearningResources.ExampleFlaskSetup, + AppType.FastAPI => LearningResources.ExampleFastApiSetup, + AppType.Console => LearningResources.ExampleConsolePythonSetup, + AppType.GenAI => LearningResources.ExampleGenAiSetup, + _ => LearningResources.ExampleGenericPythonSetup + }; + resources.Add(frameworkExample); + + return resources; + } + + /// + /// Get GenAI instrumentation packages based on detected dependencies. + /// Returns the instrumentation packages for GenAI libraries found in the project. + /// + private static List GetGenAIInstrumentationPackages(List dependencies) + { + var genaiInstrumentations = PythonInstrumentationRegistry.GetByCategory("genai"); + var result = new List(); + + foreach (var dep in dependencies) + { + var normalized = PythonInstrumentationRegistry.NormalizePackageName(dep); + var instrumentation = genaiInstrumentations + .FirstOrDefault(g => PythonInstrumentationRegistry.NormalizePackageName(g.LibraryName) == normalized); + + if (instrumentation != null && !string.IsNullOrEmpty(instrumentation.InstrumentationPackage)) + { + // Avoid duplicates (e.g., langchain-core and langchain-community use same instrumentation) + if (!result.Any(r => r.InstrumentationPackage == instrumentation.InstrumentationPackage)) + { + result.Add(instrumentation); + } + } + } + + return result; + } + + /// + /// Get Console app instrumentation packages based on detected dependencies. + /// Returns instrumentation packages for HTTP clients, databases, and other libraries. + /// Always includes logging instrumentation since it's a built-in module. + /// + private static List GetConsoleInstrumentationPackages(List dependencies) + { + var result = new List(); + var normalized = dependencies.Select(PythonInstrumentationRegistry.NormalizePackageName).ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Always add logging instrumentation for console apps (logging is built-in, so not in requirements.txt) + var loggingInstrumentation = new InstrumentationInfo + { + LibraryName = "logging", + DisplayName = "Logging", + ModuleName = "logging", + InstrumentationPackage = "opentelemetry-instrumentation-logging", + InDistro = false, + Category = "other" + }; + result.Add(loggingInstrumentation); + + // Check for HTTP libraries + var httpLibraries = new[] { "requests", "httpx", "urllib3", "urllib", "aiohttp" }; + foreach (var lib in httpLibraries) + { + if (normalized.Contains(lib)) + { + var instrumentation = PythonInstrumentationRegistry.GetInstrumentation(lib); + if (instrumentation != null && !string.IsNullOrEmpty(instrumentation.InstrumentationPackage)) + { + if (!result.Any(r => r.InstrumentationPackage == instrumentation.InstrumentationPackage)) + { + result.Add(instrumentation); + } + } + } + } + + // Check for database libraries + var dbLibraries = new[] { "psycopg2", "psycopg2-binary", "pymongo", "redis", "pymysql", "mysql-connector-python", "sqlalchemy" }; + foreach (var lib in dbLibraries) + { + if (normalized.Contains(lib)) + { + var instrumentation = PythonInstrumentationRegistry.GetInstrumentation(lib); + if (instrumentation != null && !string.IsNullOrEmpty(instrumentation.InstrumentationPackage)) + { + if (!result.Any(r => r.InstrumentationPackage == instrumentation.InstrumentationPackage)) + { + result.Add(instrumentation); + } + } + } + } + + // Check for async libraries + if (normalized.Contains("asyncio") || normalized.Contains("aiohttp")) + { + var asyncioInstrumentation = PythonInstrumentationRegistry.GetInstrumentation("asyncio"); + if (asyncioInstrumentation != null && !string.IsNullOrEmpty(asyncioInstrumentation.InstrumentationPackage)) + { + if (!result.Any(r => r.InstrumentationPackage == asyncioInstrumentation.InstrumentationPackage)) + { + result.Add(asyncioInstrumentation); + } + } + } + + return result; + } + + /// + /// Get the instrumentation code snippet for the specific framework. + /// + private static string GetInstrumentationCode(AppType appType, List dependencies) + { + // Basic instrumentation code that works for all frameworks + // The distro auto-detects and instruments Django, Flask, FastAPI, etc. + const string basicCode = @"from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure Monitor OpenTelemetry - must be called before importing app frameworks +configure_azure_monitor() +"; + + return appType switch + { + AppType.Django => @"from azure.monitor.opentelemetry import configure_azure_monitor + configure_azure_monitor() + +", + AppType.Flask => @"from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure Monitor OpenTelemetry - call before creating Flask app +configure_azure_monitor() + +# Your Flask app code follows +# from flask import Flask +# app = Flask(__name__) +", + AppType.FastAPI => @"from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure Monitor OpenTelemetry - call before creating FastAPI app +configure_azure_monitor() + +# Your FastAPI app code follows +# from fastapi import FastAPI +# app = FastAPI() +", + AppType.Console => @"from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +", + AppType.GenAI => GetGenAIInstrumentationCode(dependencies), + _ => basicCode + }; + } + + /// + /// Generate GenAI instrumentation code including library-specific instrumentors. + /// + private static string GetGenAIInstrumentationCode(List dependencies) + { + var code = @"import logging + +logging.basicConfig(level=logging.INFO) + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +"; + + // Add instrumentor calls for detected GenAI libraries + var genaiPackages = GetGenAIInstrumentationPackages(dependencies); + if (genaiPackages.Any()) + { + foreach (var pkg in genaiPackages) + { + var instrumentorClass = GetInstrumentorClassName(pkg.LibraryName); + if (!string.IsNullOrEmpty(instrumentorClass)) + { + var moduleName = pkg.LibraryName.ToLower().Replace("-", "").Replace("_", ""); + code += $"from opentelemetry.instrumentation.{moduleName} import {instrumentorClass}\n"; + code += $"{instrumentorClass}().instrument()\n\n"; + } + } + } + + code += "logger = logging.getLogger(__name__)\n"; + return code; + } + + /// + /// Get the instrumentor class name for a GenAI library. + /// + private static string GetInstrumentorClassName(string libraryName) + { + return libraryName.ToLower() switch + { + "openai" => "OpenAIInstrumentor", + "anthropic" => "AnthropicInstrumentor", + "langchain" => "LangchainInstrumentor", + "langchain-core" => "LangchainInstrumentor", + "langchain-community" => "LangchainInstrumentor", + "google-cloud-aiplatform" => "VertexAIInstrumentor", + "google-genai" => "GoogleGenAIInstrumentor", + "openai-agents" => "OpenAIAgentsInstrumentor", + "weaviate-client" => "WeaviateInstrumentor", + _ => "" + }; + } + + /// + /// Get the location hint for where to insert the instrumentation code. + /// + private static string GetInsertLocation(AppType appType) + { + return appType switch + { + AppType.Django => "os.environ.setdefault('DJANGO_SETTINGS_MODULE'", + AppType.Flask => "At the top of the file, before Flask import and app creation", + AppType.FastAPI => "At the top of the file, before FastAPI import and app creation", + AppType.Console => "logger = logging.getLogger(__name__)", + AppType.GenAI => "At the very top of the file, before importing GenAI libraries (OpenAI, LangChain, Anthropic, etc.)", + _ => "At the very top of the file, before other imports" + }; + } + + /// + /// Get manual instructions when entry point cannot be determined. + /// + private static string GetManualInstructions(AppType appType) + { + var baseInstructions = @"Add the following code at the very top of your application entry point: + +```python +from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure Monitor - must be called before importing frameworks +configure_azure_monitor() +``` + +"; + + var frameworkSpecific = appType switch + { + AppType.Django => @"For Django applications (manage.py): +- Add this inside the main() function +- Must be AFTER os.environ.setdefault('DJANGO_SETTINGS_MODULE', ...) line +- Must be BEFORE Django imports (execute_from_command_line) + +Alternatively, for wsgi.py/asgi.py: +- Add at the very top of the file before importing Django +- Must be BEFORE get_wsgi_application() or get_asgi_application()", + + AppType.Flask => @"For Flask applications: +- Add this to the top of your main application file (e.g., `app.py`) +- Must be before you create the Flask app instance", + + AppType.FastAPI => @"For FastAPI applications: +- Add this to the top of your main application file (e.g., `main.py`) +- Must be before you create the FastAPI app instance", + + AppType.GenAI => @"For GenAI applications (OpenAI, LangChain, Anthropic, etc.): +- Add this to the top of your main application file (e.g., `app.py`) +- Must be before you import any GenAI libraries (openai, langchain, anthropic) +- This enables tracing for LLM calls, token usage, and model interactions", + + AppType.Console => @"For Console/Script applications: +- Add this to the top of your main script file (e.g., `app.py`, `main.py`) +- For library-specific tracing (requests, httpx, psycopg2), add manual instrumentations +- Use OpenTelemetry APIs to create custom spans for your business logic", + + _ => @"Add this to your application's entry point file before any other imports." + }; + + return baseInstructions + frameworkSpecific; + } + + /// + /// Get the full .env.example file content matching config_generator.py logic. + /// + private static string GetEnvFileContent(AppType appType, List dependencies) + { + var baseContent = @"# Azure Monitor OpenTelemetry Configuration + +# Required: Application Insights connection string +# Get this from Azure Portal -> Application Insights -> Overview +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://your-region.in.applicationinsights.azure.com/;LiveEndpoint=https://your-region.livediagnostics.monitor.azure.com/ +"; + + // Add GenAI-specific API keys + if (appType == AppType.GenAI) + { + var genaiSection = GetGenAIApiKeySection(dependencies); + if (!string.IsNullOrEmpty(genaiSection)) + { + baseContent += "\n" + genaiSection; + } + } + + baseContent += @" +# Optional: Service name (defaults to application name) +# OTEL_SERVICE_NAME=my-application + +# Optional: Resource attributes +# OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0 + +# Optional: Sampling rate (0.0 to 1.0, default is 1.0) +# OTEL_TRACES_SAMPLER=traceidratio +# OTEL_TRACES_SAMPLER_ARG=0.1 + +# Optional: Enable/disable specific instrumentations +# OTEL_PYTHON_DISABLED_INSTRUMENTATIONS= + +# Optional: Log level for OpenTelemetry +# OTEL_LOG_LEVEL=info +"; + + return baseContent; + } + + /// + /// Get the PowerShell script content for setting environment variables. + /// + private static string GetPowerShellScriptContent(AppType appType, List dependencies) + { + var baseScript = @"# Set Azure Monitor OpenTelemetry environment variables +# Copy this file to set-env.local.ps1 and update with your values + +$env:APPLICATIONINSIGHTS_CONNECTION_STRING = ""InstrumentationKey=YOUR-KEY;IngestionEndpoint=https://YOUR-REGION.in.applicationinsights.azure.com/"" +"; + + // Add GenAI-specific API keys + if (appType == AppType.GenAI) + { + var genaiSection = GetGenAIApiKeyScriptSection(dependencies); + if (!string.IsNullOrEmpty(genaiSection)) + { + baseScript += "\n" + genaiSection; + } + } + + baseScript += @" +# Optional settings +# $env:OTEL_SERVICE_NAME = ""my-application"" +# $env:OTEL_RESOURCE_ATTRIBUTES = ""deployment.environment=production"" + +Write-Host ""Environment variables set for current session"" -ForegroundColor Green +Write-Host ""Run your application now: python app.py"" -ForegroundColor Cyan +"; + + return baseScript; + } + + /// + /// Get GenAI API key environment variable section for .env files. + /// + private static string GetGenAIApiKeySection(List dependencies) + { + var normalized = dependencies.Select(PythonInstrumentationRegistry.NormalizePackageName).ToList(); + var sections = new List(); + + if (normalized.Contains("openai") || normalized.Contains("openai-agents")) + { + sections.Add(@"# Required: OpenAI API Key +# Get this from https://platform.openai.com/api-keys +OPENAI_API_KEY=sk-your-openai-api-key-here"); + } + + if (normalized.Contains("anthropic")) + { + sections.Add(@"# Required: Anthropic API Key +# Get this from https://console.anthropic.com/ +ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here"); + } + + if (normalized.Contains("langchain") || normalized.Contains("langchain-core") || normalized.Contains("langchain-community")) + { + // LangChain often uses OpenAI, but could use other providers + if (!normalized.Contains("openai")) + { + sections.Add(@"# Required: LangChain LLM API Key (e.g., OpenAI) +# LangChain supports multiple providers - configure the one you're using +OPENAI_API_KEY=sk-your-api-key-here"); + } + } + + if (normalized.Contains("google-cloud-aiplatform") || normalized.Contains("google-genai")) + { + sections.Add(@"# Required: Google Cloud credentials +# Set up using: gcloud auth application-default login +# Or set GOOGLE_APPLICATION_CREDENTIALS=path/to/service-account.json"); + } + + return sections.Count > 0 ? "\n" + string.Join("\n\n", sections) : string.Empty; + } + + /// + /// Get GenAI API key environment variable section for PowerShell scripts. + /// + private static string GetGenAIApiKeyScriptSection(List dependencies) + { + var normalized = dependencies.Select(PythonInstrumentationRegistry.NormalizePackageName).ToList(); + var sections = new List(); + + if (normalized.Contains("openai") || normalized.Contains("openai-agents")) + { + sections.Add(@"# Required: OpenAI API Key +$env:OPENAI_API_KEY = ""sk-your-openai-api-key-here"""); + } + + if (normalized.Contains("anthropic")) + { + sections.Add(@"# Required: Anthropic API Key +$env:ANTHROPIC_API_KEY = ""sk-ant-your-anthropic-api-key-here"""); + } + + if (normalized.Contains("langchain") || normalized.Contains("langchain-core") || normalized.Contains("langchain-community")) + { + if (!normalized.Contains("openai")) + { + sections.Add(@"# Required: LangChain LLM API Key +$env:OPENAI_API_KEY = ""sk-your-api-key-here"""); + } + } + + if (normalized.Contains("google-cloud-aiplatform") || normalized.Contains("google-genai")) + { + sections.Add(@"# Required: Google Cloud credentials +# $env:GOOGLE_APPLICATION_CREDENTIALS = ""path/to/service-account.json"""); + } + + return sections.Count > 0 ? string.Join("\n", sections) : string.Empty; + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/RedisNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/RedisNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..a05fb39927 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/RedisNodeJsGreenfieldGenerator.cs @@ -0,0 +1,77 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js Redis greenfield projects (no existing telemetry) +/// +public class RedisNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var redisProjects = analysis.Projects + .Where(p => p.AppType == AppType.RedisNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && redisProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.RedisNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Node.js application with Redis. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests, Redis operations, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleRedisSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +// Redis operations will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +});", + "// At the very top of the file, before any other imports", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WinstonNodeJsGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WinstonNodeJsGreenfieldGenerator.cs new file mode 100644 index 0000000000..cdce29c59c --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WinstonNodeJsGreenfieldGenerator.cs @@ -0,0 +1,79 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Node.js Winston greenfield projects (no existing telemetry) +/// +public class WinstonNodeJsGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + var winstonProjects = analysis.Projects + .Where(p => p.AppType == AppType.WinstonNodeJs) + .ToList(); + + return analysis.Language == Language.NodeJs + && winstonProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.WinstonNodeJs); + var packageJsonPath = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "index.js"; + var projectDir = Path.GetDirectoryName(packageJsonPath) ?? ""; + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + "azure-monitor-opentelemetry", + "Node.js application with Winston logging. Azure Monitor OpenTelemetry provides automatic instrumentation for HTTP requests and can be configured to collect Winston logs.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineNodeJs, + LearningResources.ConceptsAzureMonitorNodeJs, + LearningResources.ExampleWinstonSetup + ]) + .AddPackageAction( + "add-monitor-package", + "Add Azure Monitor OpenTelemetry package", + "npm", + "@azure/monitor-opentelemetry", + "latest", + packageJsonPath, + "review-education") + .AddModifyCodeAction( + "configure-opentelemetry", + "Initialize Azure Monitor OpenTelemetry at application startup", + entryPoint, + @"const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration with Winston log collection +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + instrumentationOptions: { + winston: { enabled: true } + } +});", + "// At the very top of the file, before any other imports", + "@azure/monitor-opentelemetry", + "add-monitor-package") + .AddConfigAction( + "add-connection-string", + "Set Azure Monitor connection string in environment variables", + Path.Combine(projectDir, ".env"), + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "", + "APPLICATIONINSIGHTS_CONNECTION_STRING"); + + return builder.Build(); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WorkerServiceBrownfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WorkerServiceBrownfieldGenerator.cs new file mode 100644 index 0000000000..a9b87ed81e --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WorkerServiceBrownfieldGenerator.cs @@ -0,0 +1,105 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for Worker Service brownfield projects migrating from Application Insights SDK 2.x to 3.x. +/// +public class WorkerServiceBrownfieldGenerator : BrownfieldGeneratorBase +{ + protected override AppType TargetAppType => AppType.Worker; + protected override string PackageName => Packages.WorkerService; + protected override string PackageVersion => Packages.WorkerService3x; + protected override string MigrationCodeResource => LearningResources.MigrationWorkerService2xTo3xCode; + protected override string MigrationNoCodeChangeResource => LearningResources.MigrationWorkerService2xTo3xNoCodeChange; + protected override string EntryPointMethodName => "AddApplicationInsightsTelemetryWorkerService"; + + public override bool CanHandle(Analysis analysis) + { + // Match Worker SDK projects, or Console/Library projects that reference the WorkerService package + // (Console apps were commonly told to use AddApplicationInsightsTelemetryWorkerService in 2.x) + var isWorkerProject = analysis.Projects.Any(p => p.AppType == AppType.Worker); + var isConsoleWithWorkerServicePackage = !isWorkerProject + && analysis.Projects.Any(p => p.AppType is AppType.Console or AppType.Library) + && HasWorkerServicePackage(analysis); + + return analysis.Language == Language.DotNet + && (isWorkerProject || isConsoleWithWorkerServicePackage) + && analysis.State == InstrumentationState.Brownfield + && analysis.ExistingInstrumentation?.Type == InstrumentationType.ApplicationInsightsSdk + && analysis.ExistingInstrumentation?.IsTargetVersion != true + && analysis.BrownfieldFindings is not null; + } + + protected override ProjectInfo FindProject(Analysis analysis) + { + // Prefer Worker app type, fall back to Console/Library + return analysis.Projects.FirstOrDefault(p => p.AppType == AppType.Worker) + ?? analysis.Projects.First(p => p.AppType is AppType.Console or AppType.Library); + } + + private static bool HasWorkerServicePackage(Analysis analysis) + { + return analysis.ExistingInstrumentation?.Evidence + .Any(e => e.Indicator.Contains("Microsoft.ApplicationInsights.WorkerService", StringComparison.OrdinalIgnoreCase)) == true; + } + + protected override bool HasFrameworkSpecificCodeChanges(ServiceOptionsFindings opts) + { + // Worker Service-only removed properties + if (opts.EnableEventCounterCollectionModule != null) + return true; + if (opts.EnableAppServicesHeartbeatTelemetryModule != null) + return true; + if (opts.EnableAzureInstanceMetadataTelemetryModule != null) + return true; + if (opts.EnableDiagnosticsTelemetryModule != null) + return true; + return false; + } + + protected override string AddServiceOptionsActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency) + { + var dep = AddSharedServiceOptionsActions(builder, opts, entryPoint, EntryPointMethodName, lastDependency); + + // Worker Service removed properties (shared + Worker-specific) + var removedProperties = new List<(string name, object? value)> + { + ("EnableAdaptiveSampling", opts.EnableAdaptiveSampling), + ("DeveloperMode", opts.DeveloperMode), + ("EndpointAddress", opts.EndpointAddress), + ("EnableHeartbeat", opts.EnableHeartbeat), + ("EnableDebugLogger", opts.EnableDebugLogger), + ("DependencyCollectionOptions", opts.DependencyCollectionOptions), + ("EnableEventCounterCollectionModule", opts.EnableEventCounterCollectionModule), + ("EnableAppServicesHeartbeatTelemetryModule", opts.EnableAppServicesHeartbeatTelemetryModule), + ("EnableAzureInstanceMetadataTelemetryModule", opts.EnableAzureInstanceMetadataTelemetryModule), + ("EnableDiagnosticsTelemetryModule", opts.EnableDiagnosticsTelemetryModule), + }; + + var removedFound = removedProperties.Where(p => p.value != null).Select(p => p.name).ToList(); + if (removedFound.Count > 0) + { + var actionId = "remove-deprecated-options"; + builder.AddManualStepAction( + actionId, + "Remove deprecated ApplicationInsightsServiceOptions properties", + $"In {entryPoint}, remove these properties from the {EntryPointMethodName} options block — they are removed in 3.x: {string.Join(", ", removedFound)}", + dependsOn: dep); + dep = actionId; + } + + return dep; + } + + protected override string AddRemovedMethodActions( + OnboardingSpecBuilder builder, ServiceOptionsFindings opts, + string entryPoint, string lastDependency) + { + // Worker Service has no UseApplicationInsights() — go straight to shared methods + return AddSharedRemovedMethodActions(builder, opts, entryPoint, EntryPointMethodName, lastDependency); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WorkerServiceGreenfieldGenerator.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WorkerServiceGreenfieldGenerator.cs new file mode 100644 index 0000000000..78c4c51df9 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Generators/WorkerServiceGreenfieldGenerator.cs @@ -0,0 +1,87 @@ +using Azure.Mcp.Tools.Monitor.Models; +using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; + +namespace Azure.Mcp.Tools.Monitor.Generators; + +/// +/// Generator for .NET Worker Service greenfield projects (no existing telemetry). +/// Supports both Host.CreateDefaultBuilder and Host.CreateApplicationBuilder hosting patterns. +/// +public class WorkerServiceGreenfieldGenerator : IGenerator +{ + public bool CanHandle(Analysis analysis) + { + // Single Worker Service project, greenfield + var workerProjects = analysis.Projects + .Where(p => p.AppType == AppType.Worker) + .ToList(); + + return analysis.Language == Language.DotNet + && workerProjects.Count == 1 + && analysis.State == InstrumentationState.Greenfield; + } + + public OnboardingSpec Generate(Analysis analysis) + { + var project = analysis.Projects.First(p => p.AppType == AppType.Worker); + var projectFile = project.ProjectFile; + var entryPoint = project.EntryPoint ?? "Program.cs"; + var projectDir = Path.GetDirectoryName(projectFile) ?? ""; + + // Select appropriate code marker based on detected hosting pattern + var codeMarker = GetCodeMarkerForHostingPattern(project.HostingPattern); + + var builder = new OnboardingSpecBuilder(analysis) + .WithAgentPreExecuteInstruction(AgentPreExecuteInstruction) + .WithDecision( + Intents.Onboard, + Approaches.ApplicationInsights3x, + "Worker Service greenfield application. AddApplicationInsightsTelemetryWorkerService() provides automatic instrumentation for dependencies, performance counters, and custom telemetry.") + .AddReviewEducationAction( + "review-education", + "Review educational materials before implementation", + [ + LearningResources.ConceptsOpenTelemetryPipelineDotNet, + LearningResources.ApiAddOpenTelemetry, + LearningResources.ExampleWorkerServiceSetup + ]) + .AddPackageAction( + "add-worker-service-package", + "Add Application Insights Worker Service package", + Packages.PackageManagerNuGet, + Packages.WorkerService, + Packages.WorkerServiceVersion, + projectFile, + "review-education") + .AddModifyCodeAction( + "configure-telemetry", + "Add Application Insights telemetry to service configuration", + entryPoint, + CodePatterns.AddWorkerServiceSnippet, + codeMarker, + CodePatterns.WorkerServiceNamespace, + "add-worker-service-package") + .AddConfigAction( + "add-connection-string", + "Configure Application Insights connection string", + Path.Combine(projectDir, Config.AppSettingsFileName), + Config.AppInsightsConnectionStringPath, + Config.ConnectionStringPlaceholder, + Config.ConnectionStringEnvVar); + + return builder.Build(); + } + + /// + /// Returns the appropriate code insertion marker based on the detected hosting pattern. + /// + private static string GetCodeMarkerForHostingPattern(HostingPattern pattern) + { + return pattern switch + { + HostingPattern.GenericHost => CodePatterns.HostCreateDefaultBuilderMarker, + // For unknown patterns, default to GenericHost as Worker Services typically use that + _ => CodePatterns.HostCreateDefaultBuilderMarker + }; + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/AddApplicationInsightsTelemetryWorkerService.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/AddApplicationInsightsTelemetryWorkerService.md new file mode 100644 index 0000000000..31bc335489 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/AddApplicationInsightsTelemetryWorkerService.md @@ -0,0 +1,115 @@ +--- +title: AddApplicationInsightsTelemetryWorkerService +category: api-reference +applies-to: 3.x +source: NETCORE/src/Microsoft.ApplicationInsights.WorkerService/ApplicationInsightsExtensions.cs +--- + +# AddApplicationInsightsTelemetryWorkerService + +**Package:** `Microsoft.ApplicationInsights.WorkerService` (3.x) + +## Signatures + +```csharp +using Microsoft.Extensions.DependencyInjection; + +// 1. Parameterless — reads config from appsettings.json / env vars +public static IServiceCollection AddApplicationInsightsTelemetryWorkerService( + this IServiceCollection services); + +// 2. IConfiguration — binds "ApplicationInsights" section +public static IServiceCollection AddApplicationInsightsTelemetryWorkerService( + this IServiceCollection services, + IConfiguration configuration); + +// 3. Action delegate — configure options inline +public static IServiceCollection AddApplicationInsightsTelemetryWorkerService( + this IServiceCollection services, + Action options); + +// 4. Options instance — pass a pre-built options object +public static IServiceCollection AddApplicationInsightsTelemetryWorkerService( + this IServiceCollection services, + ApplicationInsightsServiceOptions options); +``` + +All overloads return `IServiceCollection`. Overloads 2–4 call overload 1 internally, then apply configuration. + +## ApplicationInsightsServiceOptions + +Namespace: `Microsoft.ApplicationInsights.WorkerService` + +| Property | Type | Default | Description | +|---|---|---|---| +| `ConnectionString` | `string` | `null` | Connection string for Application Insights. Can also be set via env var or config (see below). | +| `Credential` | `TokenCredential` | `null` | AAD credential for token-based authentication. When null, the instrumentation key from the connection string is used. | +| `ApplicationVersion` | `string` | Entry assembly version | Application version reported with telemetry. | +| `EnableQuickPulseMetricStream` | `bool` | `true` | Enables Live Metrics. | +| `EnablePerformanceCounterCollectionModule` | `bool` | `true` | Enables performance counter collection. | +| `EnableDependencyTrackingTelemetryModule` | `bool` | `true` | Enables HTTP and SQL dependency tracking. | +| `AddAutoCollectedMetricExtractor` | `bool` | `true` | Enables standard metric extraction. | +| `TracesPerSecond` | `double?` | `null` (effective: `5`) | Rate-limited sampling — targets this many traces per second. Must be ≥ 0. | +| `SamplingRatio` | `float?` | `null` | Fixed-rate sampling (0.0–1.0, where 1.0 = no sampling). When set, overrides `TracesPerSecond`. | +| `EnableTraceBasedLogsSampler` | `bool?` | `null` (effective: `true`) | When true, logs are sampled with their parent trace. Set `false` to collect all logs. | + +**Note:** Unlike the ASP.NET Core package, WorkerService does not include `EnableRequestTrackingTelemetryModule` or `EnableAuthenticationTrackingJavaScript`. + +## Minimal example + +```csharp +using Microsoft.Extensions.DependencyInjection; + +var builder = Host.CreateDefaultBuilder(args); +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(); +}); +var host = builder.Build(); +await host.RunAsync(); +``` + +Set the connection string via environment variable: +``` +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...;IngestionEndpoint=... +``` + +Or in `appsettings.json`: +```json +{ + "ApplicationInsights": { + "ConnectionString": "InstrumentationKey=...;IngestionEndpoint=..." + } +} +``` + +## Full example + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ApplicationInsights.WorkerService; + +var builder = Host.CreateDefaultBuilder(args); +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(options => + { + options.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + options.EnableQuickPulseMetricStream = true; + options.SamplingRatio = 0.5f; // Collect 50% of telemetry + }); +}); +var host = builder.Build(); +await host.RunAsync(); +``` + +## Behavior notes + +- Connection string resolution order: `ApplicationInsightsServiceOptions.ConnectionString` → env var `APPLICATIONINSIGHTS_CONNECTION_STRING` → config key `ApplicationInsights:ConnectionString`. +- `TracesPerSecond` is the default sampling mode (effective default `5`). Set `SamplingRatio` for fixed-rate sampling instead. +- Additional OTel sources/meters can be added: `services.AddOpenTelemetry().WithTracing(t => t.AddSource("MySource"))`. +## See also + +- UseAzureMonitor (distro)(see in UseAzureMonitor.md) +- UseAzureMonitorExporter(see in UseAzureMonitorExporter.md) +- Worker Service 2.x → 3.x Migration(see in workerservice-2x-to-3x-code-migration.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ApplicationInsightsWeb.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ApplicationInsightsWeb.md new file mode 100644 index 0000000000..0b3aca7601 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ApplicationInsightsWeb.md @@ -0,0 +1,103 @@ +--- +title: ApplicationInsightsWeb +category: api-reference +applies-to: 3.x +--- + +# Microsoft.ApplicationInsights.Web — API Reference + +## Package + +``` +Microsoft.ApplicationInsights.Web +``` + +Target: .NET Framework 4.6.2+ + +## ApplicationInsights.config elements + +| Element | Default | Description | +|---|---|---| +| `ConnectionString` | — | **Required.** Azure Monitor connection string. | +| `DisableTelemetry` | `false` | Disable all telemetry collection. | +| `ApplicationVersion` | — | Sets `service.version` resource attribute. | +| `TracesPerSecond` | `5.0` | Rate-limited sampling (traces per second). | +| `SamplingRatio` | — | Fixed-rate sampling (0.0–1.0). Overrides `TracesPerSecond` if set. | +| `EnableTraceBasedLogsSampler` | `true` | Logs follow parent trace sampling decision. | +| `StorageDirectory` | — | Directory for offline telemetry storage. | +| `DisableOfflineStorage` | `false` | Disable offline storage. | +| `EnableQuickPulseMetricStream` | `true` | Enable Live Metrics. | +| `EnablePerformanceCounterCollectionModule` | `true` | Collect performance counters. | +| `EnableDependencyTrackingTelemetryModule` | `true` | Track SQL/HTTP dependencies. | +| `EnableRequestTrackingTelemetryModule` | `true` | Track incoming HTTP requests. | +| `AddAutoCollectedMetricExtractor` | `true` | Extract standard metrics. | + +## Removed in 3.x (from 2.x) + +| Element/Section | Status | +|---|---| +| `` | **Removed** — use `` | +| `` | **Removed** — initializers are now internal Activity Processors | +| `` | **Removed** — modules are auto-configured internally | +| `` | **Removed** — use `ConfigureOpenTelemetryBuilder` for custom processors | +| `` | **Removed** — export pipeline managed by OpenTelemetry | + +## HTTP modules required in Web.config + +### IIS Integrated mode (`system.webServer/modules`) + +```xml + + +``` + +### IIS Classic mode (`system.web/httpModules`) + +```xml + +``` + +> These are auto-configured by the NuGet package install. + +## TelemetryClient usage + +`TelemetryClient` works the same as in ASP.NET Core 3.x. See TelemetryClient.md for breaking changes. + +Key difference: in classic ASP.NET, create via `new TelemetryClient(TelemetryConfiguration.CreateDefault())` or let `ApplicationInsightsHttpModule` initialize the configuration first. + +```csharp +var client = new TelemetryClient(TelemetryConfiguration.CreateDefault()); +client.TrackEvent("OrderCreated", new Dictionary { ["OrderId"] = orderId }); +client.TrackMetric("ProcessingTime", elapsed); +``` + +## ConfigureOpenTelemetryBuilder + +The primary extensibility point in 3.x for classic ASP.NET. Add custom processors, exporters, or resource attributes: + +```csharp +// In Global.asax.cs Application_Start() +var config = TelemetryConfiguration.CreateDefault(); +config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; +config.ConfigureOpenTelemetryBuilder(otel => +{ + otel.WithTracing(tracing => + { + tracing.AddProcessor(); + tracing.AddConsoleExporter(); // For debugging + }); + otel.ConfigureResource(r => r.AddService("MyWebApp")); +}); +``` + +## Notes + +- The NuGet package install handles all configuration — no code changes needed for basic setup. +- `TelemetryCorrelationHttpModule` is **not needed** in 3.x — OpenTelemetry handles correlation natively. +- Connection string can be set in `ApplicationInsights.config`, in code, or via `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable. +- **Non-DI:** Classic ASP.NET does not use `IServiceCollection`. Use `TelemetryConfiguration.ConfigureOpenTelemetryBuilder` for extensibility. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConfigureOpenTelemetryProvider.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConfigureOpenTelemetryProvider.md index 7fb7ff748b..4852ce8a0c 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConfigureOpenTelemetryProvider.md +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConfigureOpenTelemetryProvider.md @@ -111,6 +111,11 @@ Register instrumentation sources early in startup, then configure exporters separately: ```csharp +using OpenTelemetry; +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; + var builder = WebApplication.CreateBuilder(args); // Register sources (could be in a different file or method) @@ -130,12 +135,30 @@ var app = builder.Build(); app.Run(); ``` +## Example - setting cloud role name via ConfigureResource + +A common migration pattern: replace a custom `ITelemetryInitializer` that sets +cloud role name with `ConfigureResource` + `AddService`: + +```csharp +using OpenTelemetry.Resources; // Required for ResourceBuilder.AddService + +// Set cloud role name (replaces ITelemetryInitializer that set Context.Cloud.RoleName) +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.ConfigureResource(r => r.AddService( + serviceName: "MyService", + serviceInstanceId: Environment.MachineName))); +``` + ## Example - library author registering sources A library can register its instrumentation without depending on the app's startup code: ```csharp +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; + public static class MyLibraryExtensions { public static IServiceCollection AddMyLibrary(this IServiceCollection services) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConsoleExporter.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConsoleExporter.md new file mode 100644 index 0000000000..809b42ca11 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/ConsoleExporter.md @@ -0,0 +1,47 @@ +--- +title: ConsoleExporter +category: api-reference +applies-to: 1.x +--- + +# Console Exporter + +Writes traces, metrics, and logs to the console (stdout). Useful for **local development and debugging only** — not for production. + +## Package + +``` +OpenTelemetry.Exporter.Console +``` + +## Setup + +```csharp +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using OpenTelemetry.Logs; + +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddConsoleExporter()); + +builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => + metrics.AddConsoleExporter()); + +builder.Services.ConfigureOpenTelemetryLoggerProvider(logging => + logging.AddConsoleExporter()); +``` + +## Options + +| Option | Default | Description | +|---|---|---| +| `Targets` | `Console` | `ConsoleExporterOutputTargets.Console` or `Debug` (writes to `System.Diagnostics.Debug`). | + +## Notes + +- **Development only** — do not use in production. Adds significant overhead. +- Useful for verifying that spans, metrics, and log records are being generated correctly. +- Console exporter works alongside Azure Monitor — both receive the same telemetry. +- Each signal needs its own `AddConsoleExporter()` call. +- For production multi-destination export, use OTLP exporter instead. +- **Non-DI usage:** Use `config.ConfigureOpenTelemetryBuilder(otel => otel.WithTracing(t => t.AddConsoleExporter()))` on `TelemetryConfiguration`. See TelemetryClient.md. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/EntityFrameworkInstrumentation.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/EntityFrameworkInstrumentation.md new file mode 100644 index 0000000000..0a222ddd5d --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/EntityFrameworkInstrumentation.md @@ -0,0 +1,56 @@ +--- +title: EntityFrameworkInstrumentation +category: api-reference +applies-to: 1.x +--- + +# Entity Framework Core Instrumentation + +## Package + +``` +OpenTelemetry.Instrumentation.EntityFrameworkCore +``` + +## Setup + +```csharp +using OpenTelemetry.Trace; + +// EF Core is auto-instrumented via DiagnosticSource — no additional setup needed. +// To customize, use ConfigureOpenTelemetryTracerProvider: +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddEntityFrameworkCoreInstrumentation(options => + { + options.SetDbStatementForText = true; // Include SQL text (be careful with PII) + options.SetDbStatementForStoredProcedure = true; + })); +``` + +## Options + +| Option | Default | Description | +|---|---|---| +| `SetDbStatementForText` | `false` | Include raw SQL text in `db.statement` attribute. May contain PII. | +| `SetDbStatementForStoredProcedure` | `true` | Include stored procedure names in `db.statement`. | +| `EnrichWithIDbCommand` | `null` | `Action` callback to enrich spans with custom tags from the command. | +| `Filter` | `null` | `Func` — filter by provider name and command text. Return `false` to suppress. | + +## Semantic conventions + +EF Core spans use the [OpenTelemetry Database semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/): + +| Attribute | Example | +|---|---| +| `db.system` | `microsoft.sql_server`, `postgresql`, `sqlite` | +| `db.name` | `MyDatabase` | +| `db.statement` | `SELECT * FROM Orders WHERE Id = @p0` (if `SetDbStatementForText = true`) | +| `server.address` | `localhost` | +| `server.port` | `5432` | + +## Notes + +- EF Core instrumentation relies on `DiagnosticSource` events emitted by EF Core itself — no additional packages needed for basic span collection. +- The `OpenTelemetry.Instrumentation.EntityFrameworkCore` package is only needed to customize options (SQL text capture, enrichment, filtering). +- Works with all EF Core providers (SQL Server, PostgreSQL, SQLite, MySQL, etc.). +- **Non-DI usage:** If not using ASP.NET Core DI, use `config.ConfigureOpenTelemetryBuilder(otel => otel.WithTracing(t => t.AddEntityFrameworkCoreInstrumentation()))` on `TelemetryConfiguration`. See TelemetryClient.md. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/HttpInstrumentation.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/HttpInstrumentation.md new file mode 100644 index 0000000000..cdddfc03f2 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/HttpInstrumentation.md @@ -0,0 +1,109 @@ +--- +title: HttpInstrumentation +category: api-reference +applies-to: 1.x +--- + +# HTTP Instrumentation — Client & Server Enrichment + +HTTP client and server instrumentation is auto-configured by both Azure Monitor Distro and Application Insights 3.x. This doc covers **customization**: enrichment, filtering, and advanced options. + +## Packages + +| Scope | Package | Auto-included? | +|---|---|---| +| HTTP Client (`HttpClient`) | `OpenTelemetry.Instrumentation.Http` | Yes (by Distro & AI 3.x) | +| HTTP Server (ASP.NET Core) | `OpenTelemetry.Instrumentation.AspNetCore` | Yes (by Distro & AI 3.x) | + +You only need to install these packages explicitly if you want to **customize** the instrumentation options. + +## HTTP Client enrichment + +```csharp +using OpenTelemetry.Trace; + +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddHttpClientInstrumentation(options => + { + options.EnrichWithHttpRequestMessage = (activity, request) => + { + activity.SetTag("http.request.header.x-correlation-id", + request.Headers.TryGetValues("X-Correlation-Id", out var vals) ? vals.First() : null); + }; + options.EnrichWithHttpResponseMessage = (activity, response) => + { + activity.SetTag("http.response.content_length", response.Content.Headers.ContentLength); + }; + options.FilterHttpRequestMessage = (request) => + { + // Suppress health check calls + return request.RequestUri?.AbsolutePath != "/health"; + }; + options.RecordException = true; + })); +``` + +## HTTP Client options + +| Option | Default | Description | +|---|---|---| +| `EnrichWithHttpRequestMessage` | `null` | `Action` to add tags from the request. | +| `EnrichWithHttpResponseMessage` | `null` | `Action` to add tags from the response. | +| `EnrichWithException` | `null` | `Action` to enrich on failure. | +| `FilterHttpRequestMessage` | `null` | `Func` — return `false` to suppress span. | +| `RecordException` | `false` | Record exception events on the span. | + +## HTTP Server (ASP.NET Core) enrichment + +```csharp +using OpenTelemetry.Trace; + +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddAspNetCoreInstrumentation(options => + { + options.EnrichWithHttpRequest = (activity, request) => + { + activity.SetTag("http.request.header.user-agent", request.Headers["User-Agent"].ToString()); + }; + options.EnrichWithHttpResponse = (activity, response) => + { + activity.SetTag("http.response.custom_header", response.Headers["X-Custom"].ToString()); + }; + options.Filter = (httpContext) => + { + // Suppress /health and /metrics endpoints + return httpContext.Request.Path != "/health" + && httpContext.Request.Path != "/metrics"; + }; + options.RecordException = true; + })); +``` + +## HTTP Server options + +| Option | Default | Description | +|---|---|---| +| `EnrichWithHttpRequest` | `null` | `Action` to add tags from the request. | +| `EnrichWithHttpResponse` | `null` | `Action` to add tags from the response. | +| `EnrichWithException` | `null` | `Action` to enrich on failure. | +| `Filter` | `null` | `Func` — return `false` to suppress span. | +| `RecordException` | `false` | Record exception events on the span. | + +## Semantic conventions (auto-populated) + +| Attribute | Source | +|---|---| +| `http.request.method` | `GET`, `POST`, etc. | +| `url.full` | Full URL (client) | +| `url.path` | Path (server) | +| `http.response.status_code` | `200`, `404`, etc. | +| `server.address` | Target host (client) | +| `network.protocol.version` | `1.1`, `2` | +| `http.route` | Route template (server) | + +## Notes + +- Both client and server instrumentation are **already active** in AI 3.x setups. Adding the packages explicitly is only needed to access the `options` overload for enrichment/filtering. +- Enrichment callbacks run synchronously on the hot path — keep them lightweight. +- `Filter` on the server side is the recommended way to suppress health check / readiness probe spans (instead of a custom processor). +- **Non-DI usage:** Use `config.ConfigureOpenTelemetryBuilder(otel => otel.WithTracing(t => t.AddHttpClientInstrumentation(...)))` on `TelemetryConfiguration`. See TelemetryClient.md. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/LogProcessors.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/LogProcessors.md index 783a97180d..8490615044 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/LogProcessors.md +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/LogProcessors.md @@ -15,6 +15,8 @@ applies-to: 1.x ## Filtering — use `AddFilter` ```csharp +using OpenTelemetry.Logs; // Required for OpenTelemetryLoggerProvider + builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); builder.Logging.AddFilter("System.Net.Http", LogLevel.Warning); ``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/OtlpExporter.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/OtlpExporter.md new file mode 100644 index 0000000000..d3a54e1c36 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/OtlpExporter.md @@ -0,0 +1,88 @@ +--- +title: OtlpExporter +category: api-reference +applies-to: 1.x +--- + +# OTLP Exporter + +Export traces, metrics, and logs to any OpenTelemetry Protocol (OTLP) compatible backend (e.g. Jaeger, Grafana Tempo, Aspire Dashboard, custom collectors). + +## Package + +``` +OpenTelemetry.Exporter.OpenTelemetryProtocol +``` + +## Setup + +```csharp +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using OpenTelemetry.Logs; +using OpenTelemetry.Exporter; + +// Azure Monitor is already configured via AddApplicationInsightsTelemetry. +// Add OTLP as a secondary exporter: +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddOtlpExporter(options => + { + options.Endpoint = new Uri("http://localhost:4317"); + options.Protocol = OtlpExportProtocol.Grpc; + })); + +builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => + metrics.AddOtlpExporter(options => + { + options.Endpoint = new Uri("http://localhost:4317"); + })); + +builder.Services.ConfigureOpenTelemetryLoggerProvider(logging => + logging.AddOtlpExporter(options => + { + options.Endpoint = new Uri("http://localhost:4317"); + })); +``` + .WithTracing(tracing => tracing.AddOtlpExporter()) + .WithMetrics(metrics => metrics.AddOtlpExporter()); +``` + +### Environment variable configuration + +Instead of code, configure via environment variables: + +| Variable | Default | Description | +|---|---|---| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4317` | OTLP endpoint URL | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` | Protocol: `grpc` or `http/protobuf` | +| `OTEL_EXPORTER_OTLP_HEADERS` | — | Headers as `key=value` pairs, comma-separated | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | `10000` | Timeout in milliseconds | + +## Options + +| Option | Default | Description | +|---|---|---| +| `Endpoint` | `http://localhost:4317` | Backend URL. Use port `4317` for gRPC, `4318` for HTTP. | +| `Protocol` | `Grpc` | `OtlpExportProtocol.Grpc` or `OtlpExportProtocol.HttpProtobuf` | +| `Headers` | `null` | Custom headers (e.g. auth tokens). Format: `"key=value"` | +| `TimeoutMilliseconds` | `10000` | Export timeout. | +| `ExportProcessorType` | `Batch` | `Batch` or `Simple`. Use `Simple` for debugging only. | +| `BatchExportProcessorOptions` | default | Batch size, delay, queue size for batch processor. | + +## Common backends + +| Backend | Endpoint | Protocol | +|---|---|---| +| Aspire Dashboard | `http://localhost:4317` | gRPC | +| Jaeger | `http://localhost:4317` | gRPC | +| Grafana Tempo | `http://localhost:4317` | gRPC | +| Grafana Cloud | `https://otlp-gateway-*.grafana.net/otlp` | HTTP (`http/protobuf`) | +| Seq | `http://localhost:5341/ingest/otlp/v1/traces` | HTTP | + +## Notes + +- OTLP exporter works **alongside** Azure Monitor — data is sent to both destinations. +- For local development with Aspire Dashboard, use `http://localhost:4317` with gRPC. +- `AddOtlpExporter()` with no arguments uses environment variables or defaults. +- Each signal (traces, metrics, logs) needs its own `AddOtlpExporter()` call. +- **Non-DI usage:** Use `config.ConfigureOpenTelemetryBuilder(otel => otel.WithTracing(t => t.AddOtlpExporter(...)))` on `TelemetryConfiguration`. See TelemetryClient.md. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/RedisInstrumentation.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/RedisInstrumentation.md new file mode 100644 index 0000000000..41c0ef4fd7 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/RedisInstrumentation.md @@ -0,0 +1,63 @@ +--- +title: RedisInstrumentation +category: api-reference +applies-to: 1.x +--- + +# Redis Instrumentation (StackExchange.Redis) + +## Package + +``` +OpenTelemetry.Instrumentation.StackExchangeRedis +``` + +## Setup + +```csharp +using OpenTelemetry.Trace; +using OpenTelemetry.Instrumentation.StackExchangeRedis; + +// Register IConnectionMultiplexer in DI +builder.Services.AddSingleton( + ConnectionMultiplexer.Connect("localhost:6379")); + +// Add Redis instrumentation +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddRedisInstrumentation()); +``` + +## Options with customization + +```csharp +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddRedisInstrumentation(options => + { + options.SetVerboseDatabaseStatements = true; + })); +``` + +## Options + +| Option | Default | Description | +|---|---|---| +| `SetVerboseDatabaseStatements` | `false` | Include full Redis command in `db.statement` (e.g. `GET mykey`). | +| `EnrichActivityWithTimingEvents` | `true` | Add Redis timing events (enqueue, sent, response) as Activity events. | +| `Enrich` | `null` | `Action` callback to add custom tags. | +| `FlushInterval` | 1 second | How often to flush profiling sessions. | + +## Semantic conventions + +| Attribute | Example | +|---|---| +| `db.system` | `redis` | +| `db.statement` | `GET mykey` (if verbose) | +| `server.address` | `localhost` | +| `server.port` | `6379` | +| `db.redis.database_index` | `0` | + +## Notes + +- The Redis instrumentation hooks into `StackExchange.Redis` profiling. It requires either DI-registered `IConnectionMultiplexer` (auto-discovered) or passing the connection explicitly. +- When using DI registration, the instrumentation automatically discovers all `IConnectionMultiplexer` instances — no need to pass the connection manually. +- **Non-DI usage:** Use `config.ConfigureOpenTelemetryBuilder(otel => otel.WithTracing(t => t.AddRedisInstrumentation(connection)))` on `TelemetryConfiguration`. See TelemetryClient.md. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/SqlClientInstrumentation.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/SqlClientInstrumentation.md new file mode 100644 index 0000000000..feb1ba6e33 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/SqlClientInstrumentation.md @@ -0,0 +1,53 @@ +--- +title: SqlClientInstrumentation +category: api-reference +applies-to: 1.x +--- + +# SQL Client Instrumentation + +## Package + +``` +OpenTelemetry.Instrumentation.SqlClient +``` + +## Setup + +```csharp +using OpenTelemetry.Trace; + +builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + tracing.AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = true; // Include SQL text + options.RecordException = true; // Record exception details on spans + })); +``` + +## Options + +| Option | Default | Description | +|---|---|---| +| `SetDbStatementForText` | `false` | Include SQL command text in `db.statement`. May contain PII. | +| `SetDbStatementForStoredProcedure` | `true` | Include stored procedure name in `db.statement`. | +| `RecordException` | `false` | Record exception details as span events when SQL commands fail. | +| `EnableConnectionLevelAttributes` | `false` | Add `server.address` and `server.port` from the connection string. | +| `Enrich` | `null` | `Action` to enrich with custom tags. Event names: `OnCustom`. | +| `Filter` | `null` | `Func` — return `false` to suppress a span. | + +## Semantic conventions + +| Attribute | Example | +|---|---| +| `db.system` | `microsoft.sql_server` | +| `db.name` | `MyDatabase` | +| `db.statement` | `SELECT * FROM Users WHERE Id = @Id` | +| `server.address` | `myserver.database.windows.net` | + +## Notes + +- Works with both `System.Data.SqlClient` and `Microsoft.Data.SqlClient`. +- If using EF Core with SQL Server, you may not need this package separately — EF Core instrumentation captures the same spans. Use this when you have raw `SqlCommand` / `SqlConnection` calls alongside or instead of EF Core. +- `SetDbStatementForText = true` captures raw SQL which may contain sensitive data. Use with caution in production. +- **Non-DI usage:** Use `config.ConfigureOpenTelemetryBuilder(otel => otel.WithTracing(t => t.AddSqlClientInstrumentation()))` on `TelemetryConfiguration`. See TelemetryClient.md. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/TelemetryClient.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/TelemetryClient.md new file mode 100644 index 0000000000..9347c6fd1e --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/TelemetryClient.md @@ -0,0 +1,122 @@ +--- +title: TelemetryClient +category: api-reference +applies-to: 3.x +--- + +# TelemetryClient — Breaking Changes in 3.x + +`TelemetryClient` still works in 3.x but several constructors, properties, and method overloads were **removed or changed**. Code that uses removed overloads will not compile after upgrading. + +## Removed APIs + +| API | Status | Migration | +| --- | --- | --- | +| `new TelemetryClient()` (parameterless) | **Removed** | Use `TelemetryClient(TelemetryConfiguration)` via DI (constructor injection). | +| `client.InstrumentationKey` | **Removed** | Use `TelemetryConfiguration.ConnectionString`. | +| `TrackPageView(string)` | **Removed** | Use `TrackEvent(name, properties)` or `TrackRequest`. | +| `TrackPageView(PageViewTelemetry)` | **Removed** | Use `TrackEvent(name, properties)` or `TrackRequest`. | + +## Changed method signatures + +The `IDictionary metrics` parameter was **removed** from several Track methods. Code passing a metrics dictionary will not compile. + +| Method | 2.x signature | 3.x signature | Fix | +| --- | --- | --- | --- | +| `TrackEvent` | `(string, IDictionary, IDictionary)` | `(string, IDictionary)` | Remove metrics dict. Track metrics separately via `TrackMetric()`. | +| `TrackException` | `(Exception, IDictionary, IDictionary)` | `(Exception, IDictionary)` | Remove metrics dict. Track metrics separately via `TrackMetric()`. | +| `TrackAvailability` | 8-param with trailing `IDictionary` | 7-param — metrics removed | Remove metrics dict. Track metrics separately via `TrackMetric()`. | + +### Example fix — TrackEvent + +**2.x (breaks in 3.x):** +```csharp +_telemetryClient.TrackEvent("OrderCreated", + new Dictionary { ["OrderId"] = orderId }, + new Dictionary { ["ProcessingTimeMs"] = elapsed }); +``` + +**3.x:** +```csharp +_telemetryClient.TrackEvent("OrderCreated", + new Dictionary { ["OrderId"] = orderId }); +_telemetryClient.TrackMetric("OrderProcessingTimeMs", elapsed); +``` + +## Other changed APIs + +| API | Change | Migration | +| --- | --- | --- | +| `TrackDependency` (obsolete 5-param overload) | **Removed** | Use the full overload with `dependencyTypeName`, `target`, `data`, `startTime`, `duration`, `success`. | +| `GetMetric` (all overloads) | `MetricConfiguration` and `MetricAggregationScope` parameters **removed** | Call the simplified overload: `GetMetric(metricId)` or `GetMetric(metricId, dim1, ...)`. | +| `Track(ITelemetry)` | Internal routing changed — now delegates to specific Track methods | Review any direct `Track(ITelemetry)` calls; prefer specific Track methods. | +| `StartOperation` / `StopOperation` | Now uses OpenTelemetry Activities internally | No code change needed — API is the same. | + +## Unchanged Track methods — no action needed + +These methods and their signatures are **identical** in 3.x: + +- `TrackEvent(string eventName)` — single-param overload +- `TrackEvent(string eventName, IDictionary properties)` — 2-param overload (no metrics) +- `TrackException(Exception exception)` — single-param overload +- `TrackException(Exception exception, IDictionary properties)` — 2-param overload (no metrics) +- `TrackTrace(string message)` and `TrackTrace(string, SeverityLevel)` and `TrackTrace(string, SeverityLevel, IDictionary)` +- `TrackMetric(string name, double value)` and other `TrackMetric` overloads +- `TrackRequest(...)` — all overloads +- `TrackDependency(...)` — full overload (not the obsolete 5-param) +- `TrackAvailability(...)` — 7-param overload (without metrics dict) +- `Flush()` and `FlushAsync(CancellationToken)` + +## TelemetryContext changes + +Several sub-context classes are now **internal** in 3.x: + +| Sub-context | Status | Previously accessible properties | +| --- | --- | --- | +| `Context.Cloud` | **Internal** | `RoleName`, `RoleInstance` — use `ConfigureResource(r => r.AddService("name"))` instead | +| `Context.Component` | **Internal** | `Version` — use `ApplicationVersion` in service options | +| `Context.Device` | **Internal** | `Type`, `Id`, `OperatingSystem`, etc. | +| `Context.Session` | **Internal** | `Id`, `IsFirst` | + +These remain **public**: `Context.User` (`Id`, `AuthenticatedUserId`, `UserAgent`), `Context.Operation` (`Name`), `Context.Location` (`Ip`), `Context.GlobalProperties`. + +Properties made internal on remaining public sub-contexts: +- `User.AccountId` — internal; set via properties dict on Track calls or custom processor +- `Operation.Id`, `Operation.ParentId` — internal; managed by OpenTelemetry correlation +- `Operation.CorrelationVector` — removed; no longer needed + +## Extensibility — ConfigureOpenTelemetryBuilder + +To register custom OpenTelemetry processors or instrumentations alongside TelemetryClient in 3.x: + +```csharp +var config = TelemetryConfiguration.CreateDefault(); +config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; +config.ConfigureOpenTelemetryBuilder(otel => +{ + otel.WithTracing(t => t.AddProcessor()); + otel.WithLogging(l => l.AddProcessor()); +}); +``` + +When using ASP.NET Core DI, use the service options + `ConfigureOpenTelemetryProvider` pattern instead — see ConfigureOpenTelemetryProvider.md. + +## Quick decision guide + +| Code pattern found | Action required? | +| --- | --- | +| `TrackEvent(name, props, metrics)` — 3 args | **Yes** — remove metrics dict, use `TrackMetric()` separately | +| `TrackException(ex, props, metrics)` — 3 args | **Yes** — remove metrics dict, use `TrackMetric()` separately | +| `TrackAvailability(..., metrics)` — 8 args | **Yes** — remove metrics dict, use `TrackMetric()` separately | +| `TrackPageView(...)` | **Yes** — replace with `TrackEvent()` or `TrackRequest()` | +| `GetMetric(..., MetricConfiguration, ...)` | **Yes** — remove config/scope params | +| `new TelemetryClient()` (no args) | **Yes** — use DI or `TelemetryClient(TelemetryConfiguration)` | +| `client.InstrumentationKey = ...` | **Yes** — use `TelemetryConfiguration.ConnectionString` | +| `Context.Cloud.RoleName = ...` | **Yes** — use `ConfigureResource` | +| `TrackEvent(name)` or `TrackEvent(name, props)` | No | +| `TrackTrace(...)` | No | +| `TrackMetric(...)` | No | +| `TrackRequest(...)` | No | +| `TrackDependency(...)` (full overload) | No | +| `TrackException(ex)` or `TrackException(ex, props)` | No | +| `Flush()` | No | diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/TelemetryConfigurationBuilder.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/TelemetryConfigurationBuilder.md new file mode 100644 index 0000000000..5ade4a5d38 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/api-reference/dotnet/TelemetryConfigurationBuilder.md @@ -0,0 +1,173 @@ +--- +title: TelemetryConfigurationBuilder +category: api-reference +applies-to: 3.x +--- + +# TelemetryConfiguration.ConfigureOpenTelemetryBuilder + +The non-DI extensibility API for Application Insights 3.x. Use this when you don't have an `IServiceCollection` — classic ASP.NET (`Global.asax`), console apps, or test scenarios. + +For DI-based apps (ASP.NET Core, Worker Service), use ConfigureOpenTelemetryTracerProvider(see in ConfigureOpenTelemetryProvider.md) instead. + +## API + +```csharp +TelemetryConfiguration.ConfigureOpenTelemetryBuilder( + Action configure) +``` + +## Setup pattern + +```csharp +using Microsoft.ApplicationInsights.Extensibility; +using OpenTelemetry; +using OpenTelemetry.Trace; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; + +var config = TelemetryConfiguration.CreateDefault(); +config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + +config.ConfigureOpenTelemetryBuilder(otel => +{ + // Add custom trace processors + otel.WithTracing(tracing => + { + tracing.AddProcessor(); + tracing.AddProcessor(); + }); + + // Add custom log processors + otel.WithLogging(logging => + { + logging.AddProcessor(); + }); + + // Set resource attributes (Cloud.RoleName, etc.) + otel.ConfigureResource(r => r.AddService( + serviceName: "MyWebApp", + serviceInstanceId: Environment.MachineName, + serviceVersion: "1.0.0")); +}); +``` + +## Add instrumentations + +```csharp +using OpenTelemetry; +using OpenTelemetry.Trace; + +config.ConfigureOpenTelemetryBuilder(otel => +{ + otel.WithTracing(tracing => + { + tracing.AddRedisInstrumentation(); + tracing.AddSqlClientInstrumentation(options => + { + options.SetDbStatementForText = true; + }); + }); +}); +``` + +## Add exporters (dual export) + +```csharp +using OpenTelemetry; +using OpenTelemetry.Trace; +using OpenTelemetry.Metrics; +using OpenTelemetry.Logs; + +config.ConfigureOpenTelemetryBuilder(otel => +{ + // Console exporter for debugging + otel.WithTracing(tracing => tracing.AddConsoleExporter()); + otel.WithLogging(logging => logging.AddConsoleExporter()); + + // OTLP exporter for secondary destination + otel.WithTracing(tracing => tracing.AddOtlpExporter()); + otel.WithMetrics(metrics => metrics.AddOtlpExporter()); +}); +``` + +## TelemetryClient usage + +In classic ASP.NET, create a **single static** `TelemetryClient` instance. `TelemetryConfiguration.CreateDefault()` returns a singleton shared with `ApplicationInsightsHttpModule` — do not create per-request instances. + +```csharp +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using OpenTelemetry; +using OpenTelemetry.Trace; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; + +public class MvcApplication : HttpApplication +{ + public static TelemetryClient TelemetryClient { get; private set; } + + protected void Application_Start() + { + var config = TelemetryConfiguration.CreateDefault(); + config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + config.ConfigureOpenTelemetryBuilder(otel => + { + otel.WithTracing(tracing => tracing.AddProcessor()); + otel.ConfigureResource(r => r.AddService("MyWebApp")); + }); + + TelemetryClient = new TelemetryClient(config); + + AreaRegistration.RegisterAllAreas(); + RouteConfig.RegisterRoutes(RouteTable.Routes); + } + + protected void Application_End() + { + TelemetryClient?.Flush(); + System.Threading.Tasks.Task.Delay(1000).Wait(); + } +} +``` + +Then in controllers: + +```csharp +public class HomeController : Controller +{ + public ActionResult Index() + { + MvcApplication.TelemetryClient.TrackEvent("HomeVisited"); + return View(); + } +} +``` + +## Relationship to ConfigureOpenTelemetryTracerProvider + +| Scenario | API to use | +|---|---| +| ASP.NET Core / Worker Service (DI) | `services.ConfigureOpenTelemetryTracerProvider(...)` | +| Classic ASP.NET (Global.asax) | `config.ConfigureOpenTelemetryBuilder(...)` | +| Console apps without DI | `config.ConfigureOpenTelemetryBuilder(...)` | +| Tests | `config.ConfigureOpenTelemetryBuilder(...)` | + +Both APIs provide access to the same OTel builders (`TracerProviderBuilder`, `MeterProviderBuilder`, `LoggerProviderBuilder`). The difference is the entry point — `IServiceCollection` extension vs `TelemetryConfiguration` method. + +## Key differences from 2.x + +| 2.x Pattern | 3.x via `ConfigureOpenTelemetryBuilder` | +|---|---| +| `config.TelemetryInitializers.Add(new MyInit())` | `otel.WithTracing(t => t.AddProcessor())` in `OnStart` | +| `config.TelemetryProcessorChainBuilder.Use(...)` | `otel.WithTracing(t => t.AddProcessor())` in `OnEnd` | +| `config.TelemetryChannel = new InMemoryChannel()` | Not needed — export managed by OTel | +| `config.TelemetrySinks.Add(...)` | `otel.WithTracing(t => t.AddOtlpExporter())` | + +## Notes + +- **`using OpenTelemetry;` is required** — the `WithTracing()`, `WithLogging()`, `WithMetrics()`, and `ConfigureResource()` extension methods on `IOpenTelemetryBuilder` are defined in the root `OpenTelemetry` namespace. Without this using directive, these methods will not resolve. This is separate from `using OpenTelemetry.Trace;` / `using OpenTelemetry.Logs;` which are also needed. +- `CreateDefault()` returns a **static singleton** in 3.x (not a new instance). Call `ConfigureOpenTelemetryBuilder` only once, in `Application_Start`. +- Connection string is **required** — 3.x throws if not set. For tests, use a dummy: `InstrumentationKey=00000000-0000-0000-0000-000000000000`. +- Call `TelemetryClient.Flush()` in `Application_End` followed by a short delay to avoid data loss on shutdown. +- `GlobalProperties` on `TelemetryClient.Context` works for custom properties. Other `Context` properties (User.Id, Operation.Name) have known propagation limitations in 3.x. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/appinsights-aspnetcore.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/appinsights-aspnetcore.md new file mode 100644 index 0000000000..f8b87cc34f --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/appinsights-aspnetcore.md @@ -0,0 +1,113 @@ +--- +title: Application Insights for ASP.NET Core +category: concept +applies-to: 3.x +--- + +# Application Insights for ASP.NET Core + +**Category:** Concept +**Applies to:** 3.x + +## Overview + +The `Microsoft.ApplicationInsights.AspNetCore` package provides built-in telemetry collection for ASP.NET Core applications, sending data to Azure Application Insights. + +## What It Provides + +One line of code enables full observability: + +```csharp +builder.Services.AddApplicationInsightsTelemetry(); +``` + +### Automatic Collection +- **Requests** — All incoming HTTP requests with timing, status codes, and URLs +- **Dependencies** — Outgoing HTTP calls, SQL queries, and Azure SDK calls +- **Exceptions** — Unhandled exceptions with full stack traces +- **Performance Counters** — CPU, memory, GC, and thread pool metrics +- **Logs** — ILogger output (Information level and above by default) + +### Built-in Features +- **Live Metrics** — Real-time monitoring via QuickPulse +- **Adaptive Sampling** — Automatic volume control (configurable via `TracesPerSecond`) +- **JavaScript Snippet** — Browser-side telemetry injection for Razor pages +- **AAD Authentication** — Token-based auth via `TokenCredential` + +## Quick Start + +### 1. Install Package + +```bash +dotnet add package Microsoft.ApplicationInsights.AspNetCore +``` + +### 2. Add to Program.cs + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddApplicationInsightsTelemetry(); +var app = builder.Build(); +``` + +### 3. Configure Connection String + +**Environment variable (recommended):** +```bash +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=xxx;IngestionEndpoint=https://... +``` + +**Or in appsettings.json:** +```json +{ + "ApplicationInsights": { + "ConnectionString": "InstrumentationKey=xxx;IngestionEndpoint=https://..." + } +} +``` + +## Configuration Options + +Use `ApplicationInsightsServiceOptions` to customize behavior: + +```csharp +builder.Services.AddApplicationInsightsTelemetry(options => +{ + options.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + options.EnableQuickPulseMetricStream = true; + options.SamplingRatio = 0.5f; // Collect 50% of telemetry +}); +``` + +| Property | Default | Description | +|----------|---------|-------------| +| `ConnectionString` | `null` | Application Insights connection string | +| `Credential` | `null` | AAD `TokenCredential` for token-based auth | +| `EnableQuickPulseMetricStream` | `true` | Enables Live Metrics | +| `EnableDependencyTrackingTelemetryModule` | `true` | Tracks HTTP/SQL dependencies | +| `EnableRequestTrackingTelemetryModule` | `true` | Tracks incoming requests | +| `TracesPerSecond` | `5` | Rate-limited sampling target | +| `SamplingRatio` | `null` | Fixed-rate sampling (0.0–1.0); overrides `TracesPerSecond` | + +## Connection String Resolution Order + +1. `ApplicationInsightsServiceOptions.ConnectionString` (code) +2. Environment variable `APPLICATIONINSIGHTS_CONNECTION_STRING` +3. Config key `ApplicationInsights:ConnectionString` + +## Extending with OpenTelemetry + +Application Insights 3.x is built on OpenTelemetry. You can add additional sources: + +```csharp +builder.Services.AddApplicationInsightsTelemetry(); +builder.Services.AddOpenTelemetry() + .WithTracing(t => t.AddSource("MyApp.CustomSource")) + .WithMetrics(m => m.AddMeter("MyApp.CustomMeter")); +``` + +## See Also + +- AddApplicationInsightsTelemetry API (see in AddApplicationInsightsTelemetry.md) +- ASP.NET Core Setup Example (see in aspnetcore-setup.md) +- App Insights 2.x to 3.x Migration (see in appinsights-2x-to-3x-code-migration.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/aspnet-classic-appinsights.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/aspnet-classic-appinsights.md new file mode 100644 index 0000000000..51d592746b --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/aspnet-classic-appinsights.md @@ -0,0 +1,95 @@ +--- +title: Application Insights for Classic ASP.NET +category: concepts +applies-to: dotnet-framework +--- + +# Application Insights for Classic ASP.NET + +## Overview + +Classic ASP.NET applications (.NET Framework 4.6.2+) use `Microsoft.ApplicationInsights.Web` for automatic request, dependency, and exception tracking. In 3.x, the package is built on OpenTelemetry internally. + +## How it works + +1. **NuGet package install** creates `ApplicationInsights.config` and registers HTTP modules in `Web.config` +2. **`ApplicationInsightsHttpModule`** runs on every request — reads config, initializes `TelemetryConfiguration`, sets up the OpenTelemetry pipeline +3. **`TelemetryHttpModule`** (from `OpenTelemetry.Instrumentation.AspNet`) captures request spans via `System.Diagnostics.Activity` +4. **No code changes needed** — request tracking, dependency tracking, and exception tracking are automatic + +## Key configuration files + +| File | Purpose | +|---|---| +| `ApplicationInsights.config` | Connection string, sampling, feature flags | +| `Web.config` | HTTP module registration (auto-configured by NuGet) | + +## ApplicationInsights.config (3.x format) + +```xml + + + InstrumentationKey=...;IngestionEndpoint=https://... + 5.0 + true + true + true + true + true + true + +``` + +## Web.config HTTP modules (auto-configured) + +```xml + + + + + + +``` + +## Extensibility — Custom processors + +Use `TelemetryConfiguration.ConfigureOpenTelemetryBuilder` in `Global.asax.cs`: + +```csharp +using Microsoft.ApplicationInsights.Extensibility; +using OpenTelemetry.Trace; +using OpenTelemetry.Resources; + +protected void Application_Start() +{ + var config = TelemetryConfiguration.CreateDefault(); + config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + config.ConfigureOpenTelemetryBuilder(otel => + { + otel.WithTracing(tracing => tracing.AddProcessor()); + otel.ConfigureResource(r => r.AddService("MyServiceName")); + }); + + AreaRegistration.RegisterAllAreas(); + RouteConfig.RegisterRoutes(RouteTable.Routes); +} +``` + +## Requirements + +- .NET Framework **4.6.2** or later (3.x raised minimum from 4.5.2) +- IIS or IIS Express +- `Microsoft.ApplicationInsights.Web` 3.x NuGet package + +## What's automatic (no code needed) + +- HTTP request tracking (all incoming requests) +- SQL dependency tracking +- HTTP dependency tracking (outgoing `HttpClient` / `WebRequest` calls) +- Exception tracking +- Performance counter collection +- Live Metrics (Quick Pulse) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/azure-monitor-distro.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/azure-monitor-distro.md index 623df570ad..4c10bcb665 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/azure-monitor-distro.md +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/azure-monitor-distro.md @@ -99,4 +99,4 @@ builder.Services.AddOpenTelemetry().UseAzureMonitor(options => - OpenTelemetry Pipeline(see in opentelemetry-pipeline.md) - UseAzureMonitor API(see in api-reference/dotnet/UseAzureMonitor.md) -- Basic Setup Example(see in examples/dotnet/aspnetcore-setup.md) +- Basic Setup Example(see in examples/dotnet/aspnetcore-distro-setup.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/opentelemetry-pipeline.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/opentelemetry-pipeline.md index 73f8d10e0a..43d542dfd2 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/opentelemetry-pipeline.md +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/dotnet/opentelemetry-pipeline.md @@ -53,5 +53,5 @@ See the AddOpenTelemetry API reference(see in AddOpenTelemetry.md) for full setu ## See Also -- Azure Monitor Distro(see in azure-monitor-distro.md) +- Application Insights for ASP.NET Core(see in appinsights-aspnetcore.md) - AddOpenTelemetry API(see in AddOpenTelemetry.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/nodejs/azure-monitor-overview.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/nodejs/azure-monitor-overview.md new file mode 100644 index 0000000000..18b7e91851 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/nodejs/azure-monitor-overview.md @@ -0,0 +1,106 @@ +# Azure Monitor for Node.js + +Azure Monitor OpenTelemetry for Node.js provides automatic instrumentation and telemetry collection for Node.js applications. + +## Key Features + +- **Automatic Instrumentation**: Captures HTTP requests, database calls, and external dependencies +- **Custom Telemetry**: Track custom events, metrics, and traces +- **Performance Monitoring**: Monitor response times, throughput, and failures +- **Dependency Tracking**: Understand outgoing calls to databases, APIs, and services +- **Distributed Tracing**: Follow requests across microservices + +## Supported Frameworks + +- Express.js +- Next.js (via instrumentation hook — requires webpack externals configuration) +- Fastify +- NestJS +- Koa +- Hapi +- And many more through OpenTelemetry auto-instrumentation + +> **Note**: Next.js requires special setup due to its webpack bundling. See the Next.js Setup Guide(see in basic-setup-nextjs.md) for details on externalizing server-only packages. + +## Installation + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Basic Setup + +```javascript +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor at startup +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Your application code follows... +const express = require('express'); +const app = express(); +``` + +## Configuration Options + +The `useAzureMonitor()` function accepts configuration for: +- Connection string +- Sampling rate +- Custom resource attributes +- Instrumentation configuration +- Logging options + +## What Gets Instrumented + +### Automatically Captured +- HTTP/HTTPS requests and responses +- Database queries (MongoDB, MySQL, PostgreSQL, etc.) +- Redis operations +- External HTTP calls +- Exceptions and errors + +### Custom Telemetry +```javascript +const { trace } = require('@opentelemetry/api'); + +// Get current span +const span = trace.getActiveSpan(); +span?.setAttribute('custom.attribute', 'value'); + +// Log custom events +console.log('Custom event logged'); // Captured as trace +``` + +## Best Practices + +1. **Initialize Early**: Call `useAzureMonitor()` before loading other modules +2. **Use Environment Variables**: Store connection string in `.env` file +3. **Enable Sampling**: For high-traffic apps, configure sampling to manage costs +4. **Add Context**: Use custom attributes to enrich telemetry +5. **Monitor Performance**: Set up alerts in Azure Monitor for key metrics + +## Connection String + +Get your connection string from Azure Portal: +1. Navigate to your Application Insights resource +2. Go to "Overview" section +3. Copy the "Connection String" value + +Set it as an environment variable: +```bash +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=..." +``` + +Or in `.env` file: +``` +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=... +``` + +## Links + +- [Azure Monitor OpenTelemetry for Node.js Documentation](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=nodejs) +- [OpenTelemetry for Node.js](https://opentelemetry.io/docs/instrumentation/js/) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/nodejs/opentelemetry-pipeline.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/nodejs/opentelemetry-pipeline.md new file mode 100644 index 0000000000..15d9caefe0 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/nodejs/opentelemetry-pipeline.md @@ -0,0 +1,201 @@ +# OpenTelemetry Pipeline for Node.js + +## Overview + +The OpenTelemetry pipeline is the data flow path for telemetry signals (traces, metrics, logs) from your Node.js application to observability backends like Azure Monitor. + +## Pipeline Components + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Instrumentation │ ──▶ │ Processors │ ──▶ │ Exporters │ +│ (Sources) │ │ (Transform) │ │ (Backends) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +### 1. Instrumentation (Sources) +- **HTTP Instrumentation** - Automatically captures HTTP requests/responses +- **Database Instrumentation** - Tracks database queries (MongoDB, PostgreSQL, MySQL, etc.) +- **Custom Spans** - Manual instrumentation using OpenTelemetry API +- **Logs** - Console logs and structured logging + +### 2. Processors (Transform) +- **Span Processors** - Modify or filter spans before export +- **Batch Processors** - Batch telemetry for efficient export +- **Sampling** - Control volume of telemetry sent + +### 3. Exporters (Backends) +- **Azure Monitor Exporter** - Sends to Application Insights +- **OTLP Exporter** - Sends to any OTLP-compatible backend +- **Console Exporter** - Debug output to console + +## In Node.js with Express + +```javascript +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +const { trace } = require('@opentelemetry/api'); + +// Initialize with Azure Monitor exporter +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + // Optional: Configure sampling + samplingRatio: 1.0 // 100% of requests +}); + +// Your Express app +const express = require('express'); +const app = express(); + +// HTTP requests are automatically instrumented +app.get('/api/users', async (req, res) => { + // Get current span for custom attributes + const span = trace.getActiveSpan(); + span?.setAttribute('user.role', 'admin'); + + res.json({ users: [] }); +}); +``` + +## In Next.js + +Next.js uses a special `instrumentation.js` hook instead of inline initialization. The key differences from Express/standard Node.js: + +1. **Instrumentation hook**: Create `instrumentation.js` at the project root with a `register()` export +2. **Runtime check**: Guard with `process.env.NEXT_RUNTIME === 'nodejs'` to avoid Edge runtime +3. **Webpack externals**: Must externalize OpenTelemetry packages in `next.config.js` to prevent webpack from bundling Node.js-only modules +4. **Logging libraries (Next.js only)**: In Next.js, libraries like Bunyan and Winston must also be added to webpack externals because they have native/optional dependencies that webpack cannot resolve. This is **not** required in standard Node.js apps (Express, Fastify, etc.) where these libraries work out of the box. + +```javascript +// instrumentation.js (project root) +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +export function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } + }); + } +} +``` + +See Next.js Setup Guide(see in basic-setup-nextjs.md) for the complete configuration including webpack externals. + +## Automatic Instrumentation + +The `@azure/monitor-opentelemetry` package automatically instruments: + +- ✅ **HTTP/HTTPS** - Incoming and outgoing requests +- ✅ **Express** - Routes, middleware, error handlers +- ✅ **Next.js** - API routes, Server Components (via instrumentation hook) +- ✅ **MongoDB** - Queries and operations +- ✅ **MySQL/PostgreSQL** - Database queries +- ✅ **Redis** - Cache operations +- ✅ **DNS** - DNS lookups +- ✅ **File System** - I/O operations (when configured) + +## Manual Instrumentation + +For custom telemetry: + +```javascript +const { trace } = require('@opentelemetry/api'); + +// Get tracer +const tracer = trace.getTracer('my-app'); + +// Create custom span +const span = tracer.startSpan('process-data'); +try { + // Your business logic + span.setAttribute('record.count', 100); + span.addEvent('Processing started'); + + // ... do work ... + + span.addEvent('Processing completed'); +} catch (error) { + span.recordException(error); + span.setStatus({ code: SpanStatusCode.ERROR }); +} finally { + span.end(); +} +``` + +## Configuration Options + +```javascript +useAzureMonitor({ + // Connection string + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + + // Sampling (0.0 to 1.0) + samplingRatio: 0.5, // 50% of requests + + // Resource attributes + resource: { + attributes: { + 'service.name': 'my-express-api', + 'service.version': '1.0.0', + 'deployment.environment': 'production' + } + }, + + // Instrumentation configuration + instrumentationOptions: { + http: { enabled: true }, + mongoDb: { enabled: true }, + express: { enabled: true } + } +}); +``` + +## Telemetry Types + +### Traces (Spans) +- Request/response flows +- Database queries +- External API calls +- Custom operations + +### Metrics +- Request counts +- Response times +- Error rates +- Custom counters/gauges + +### Logs +- Console output (`console.log`, `console.error`) +- Structured logging frameworks (Winston, Bunyan) +- Exception traces + +## Best Practices + +1. **Initialize Early** - Call `useAzureMonitor()` before importing application code +2. **Use Environment Variables** - Store connection strings securely +3. **Enable Sampling** - For high-volume apps, use sampling to control costs +4. **Add Context** - Use custom attributes to enrich telemetry +5. **Handle Errors** - Always record exceptions in custom spans + +## Comparison: Application Insights SDK vs OpenTelemetry + +| Feature | Classic SDK | OpenTelemetry | +|---------|-------------|---------------| +| Initialization | `appInsights.setup()` | `useAzureMonitor()` | +| Custom Tracking | `trackEvent()`, `trackTrace()` | `span.addEvent()`, `console.log()` | +| Dependencies | Automatic | Automatic (via instrumentations) | +| Vendor Lock-in | Azure-specific | Vendor-neutral (CNCF standard) | +| Future Support | Limited | Full Microsoft commitment | + +## See Also + +- Azure Monitor for Node.js(see in azure-monitor-nodejs.md) +- Express Setup Guide(see in basic-setup-express.md) +- Next.js Setup Guide(see in basic-setup-nextjs.md) +- Bunyan Setup Guide(see in basic-setup-bunyan-nodejs.md) +- [OpenTelemetry Documentation](https://opentelemetry.io/docs/instrumentation/js/) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/python/azure-monitor-overview.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/python/azure-monitor-overview.md new file mode 100644 index 0000000000..58474924df --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/python/azure-monitor-overview.md @@ -0,0 +1,122 @@ +--- +title: Azure Monitor OpenTelemetry for Python +category: concept +applies-to: python +--- + +# Azure Monitor for Python + +**Category:** Concept +**Applies to:** Python + +Azure Monitor OpenTelemetry Distro for Python provides automatic instrumentation and telemetry collection for Python applications. + +## Key Features + +- **Automatic Instrumentation**: Captures HTTP requests, database calls, and external dependencies +- **Custom Telemetry**: Track custom events, metrics, and traces +- **Performance Monitoring**: Monitor response times, throughput, and failures +- **Dependency Tracking**: Understand outgoing calls to databases, APIs, and services +- **Distributed Tracing**: Follow requests across microservices + +## Supported Frameworks + +The Azure Monitor Distro automatically instruments: +- **Django** - Full-featured web framework +- **Flask** - Lightweight WSGI framework +- **FastAPI** - Modern async API framework +- **Requests** - HTTP library +- **urllib/urllib3** - Standard HTTP libraries +- **Psycopg2** - PostgreSQL adapter + +Additional instrumentations available via OpenTelemetry contrib packages. + +## Installation + +```bash +pip install azure-monitor-opentelemetry +``` + +## Basic Setup + +```python +from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure Monitor - must be called before importing frameworks +configure_azure_monitor() + +# Now import and use your framework +from flask import Flask +app = Flask(__name__) +``` + +## Configuration Options + +The `configure_azure_monitor()` function uses environment variables: + +| Variable | Description | +|----------|-------------| +| `APPLICATIONINSIGHTS_CONNECTION_STRING` | Required. Your App Insights connection string | +| `OTEL_SERVICE_NAME` | Optional. Your application name | +| `OTEL_RESOURCE_ATTRIBUTES` | Optional. Custom resource attributes | +| `OTEL_TRACES_SAMPLER` | Optional. Sampling strategy | +| `OTEL_TRACES_SAMPLER_ARG` | Optional. Sampling rate (0.0-1.0) | + +## What Gets Instrumented + +### Automatically Captured (Distro Bundled) +- HTTP requests via Django, Flask, FastAPI +- HTTP client calls via requests, urllib, urllib3 +- PostgreSQL queries via psycopg2 +- Azure SDK calls via azure-core + +### Manual Instrumentation Required +For other libraries (Redis, MongoDB, Celery, etc.), install the OpenTelemetry instrumentation package: + +```bash +pip install opentelemetry-instrumentation-redis +``` + +### Custom Telemetry +```python +from opentelemetry import trace + +# Get current tracer +tracer = trace.get_tracer(__name__) + +# Create custom span +with tracer.start_as_current_span("custom-operation") as span: + span.set_attribute("custom.attribute", "value") + # Your business logic here +``` + +## Best Practices + +1. **Initialize Early**: Call `configure_azure_monitor()` before importing frameworks +2. **Use Environment Variables**: Store connection string in `.env` file +3. **Enable Sampling**: For high-traffic apps, configure sampling to manage costs +4. **Add Context**: Use custom attributes to enrich telemetry +5. **Monitor Performance**: Set up alerts in Azure Monitor for key metrics + +## Connection String + +Get your connection string from Azure Portal: +1. Navigate to your Application Insights resource +2. Go to "Overview" section +3. Copy the "Connection String" value + +Set it as an environment variable: +```bash +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=..." +``` + +Or in `.env` file: +``` +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=... +``` + +## Links + +- [Azure Monitor OpenTelemetry Distro](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python) +- [OpenTelemetry Python](https://opentelemetry.io/docs/instrumentation/python/) +- [Azure SDK for Python](https://github.com/Azure/azure-sdk-for-python) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/python/opentelemetry-pipeline.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/python/opentelemetry-pipeline.md new file mode 100644 index 0000000000..37eee09cbe --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/concepts/python/opentelemetry-pipeline.md @@ -0,0 +1,154 @@ +--- +title: OpenTelemetry Pipeline for Python +category: concept +applies-to: python +--- + +# OpenTelemetry Pipeline for Python + +**Category:** Concept +**Applies to:** Python + +This document explains how OpenTelemetry works in Python applications and how Azure Monitor integrates with it. + +## OpenTelemetry Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Python Application │ +├─────────────────────────────────────────────────────────────┤ +│ Instrumentation Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Django │ │ Flask │ │ FastAPI │ │ +│ │ Instrumentor│ │ Instrumentor│ │ Instrumentor│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ OpenTelemetry SDK │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Traces │ │ Metrics │ │ Logs │ │ +│ │ Provider │ │ Provider │ │ Provider │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ Exporters │ +│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ +│ │ Azure Monitor OpenTelemetry Distro (Thin Wrapper) │ │ +│ │ ┌───────────────────────────────────────────────────┐ │ │ +│ │ │ Azure Monitor OpenTelemetry Exporter │ │ │ +│ │ └───────────────────────────────────────────────────┘ │ │ +│ │ • Sets up TracerProvider, LoggerProvider, etc. │ │ +│ │ • Attaches Flask/Django/FastAPI/Requests instrumentors│ │ +│ │ • Reads APPLICATIONINSIGHTS_CONNECTION_STRING env var │ │ +│ │ • Detects Azure App Service/Functions/AKS metadata │ │ +│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Azure Monitor │ + │ (App Insights) │ + └─────────────────┘ +``` + +## Core Components + +### 1. Instrumentors +Instrumentors automatically capture telemetry from libraries: +- `FlaskInstrumentor` - Captures Flask HTTP requests +- `DjangoInstrumentor` - Captures Django requests and middleware +- `FastAPIInstrumentor` - Captures FastAPI/Starlette requests +- `RequestsInstrumentor` - Captures outbound HTTP calls + +### 2. Providers +Providers manage the lifecycle of telemetry: +- **TracerProvider**: Manages trace/span creation +- **MeterProvider**: Manages metrics collection +- **LoggerProvider**: Manages log correlation + +### 3. Exporters +Exporters send telemetry to backends: +- **Azure Monitor Exporter**: Sends to Application Insights +- **Console Exporter**: Prints to stdout (for debugging) +- **OTLP Exporter**: Sends to any OTLP-compatible backend + +## How Azure Monitor Distro Works + +The `azure-monitor-opentelemetry` package simplifies setup by: + +1. **Auto-configuring providers** - Sets up trace, metric, and log providers +2. **Auto-instrumenting** - Enables bundled instrumentations automatically +3. **Configuring export** - Sets up the Azure Monitor exporter +4. **Resource detection** - Detects Azure resource metadata + +### One-Line Setup +```python +from azure.monitor.opentelemetry import configure_azure_monitor + +configure_azure_monitor() # That's it! +``` + +This replaces what would otherwise be 50+ lines of manual configuration. + +## Signal Types + +### Traces +Distributed traces track requests across services: +```python +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("my-operation") as span: + span.set_attribute("user.id", user_id) + # ... operation code +``` + +### Metrics +Metrics capture aggregated measurements: +```python +from opentelemetry import metrics + +meter = metrics.get_meter(__name__) +request_counter = meter.create_counter("requests") + +request_counter.add(1, {"endpoint": "/api/users"}) +``` + +### Logs +Logs are correlated with traces: +```python +import logging + +logger = logging.getLogger(__name__) +logger.info("Processing request") # Automatically correlated with active span +``` + +## Context Propagation + +OpenTelemetry automatically propagates trace context: +- Across HTTP calls (via W3C Trace Context headers) +- Between services (distributed tracing) +- To logs (correlation IDs) + +## Sampling + +Control telemetry volume with sampling: + +```bash +# Sample 10% of traces +export OTEL_TRACES_SAMPLER=traceidratio +export OTEL_TRACES_SAMPLER_ARG=0.1 +``` + +## Best Practices + +1. **Initialize First**: Configure OpenTelemetry before importing instrumented libraries +2. **Use Semantic Conventions**: Follow OpenTelemetry naming standards for attributes +3. **Enrich Spans**: Add business context via custom attributes +4. **Handle Errors**: Record exceptions on spans for better debugging +5. **Configure Sampling**: Balance observability needs with costs + +## Links + +- [OpenTelemetry Python Documentation](https://opentelemetry.io/docs/instrumentation/python/) +- [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/) +- [Azure Monitor OpenTelemetry](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-overview) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnet-classic-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnet-classic-setup.md new file mode 100644 index 0000000000..40c8edd71b --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnet-classic-setup.md @@ -0,0 +1,80 @@ +--- +title: Classic ASP.NET Setup +category: examples +applies-to: dotnet-framework +--- + +# Classic ASP.NET — Application Insights Setup + +## Prerequisites + +- .NET Framework 4.6.2+ project (ASP.NET MVC, WebForms, or generic ASP.NET) +- Visual Studio or MSBuild for building + +## Step 1 — Install the NuGet package + +In Visual Studio, open the **Package Manager Console** (View → Other Windows → Package Manager Console) and run: + +``` +Install-Package Microsoft.ApplicationInsights.Web +``` + +This automatically: +- Creates `ApplicationInsights.config` with default 3.x settings +- Adds `ApplicationInsightsHttpModule` and `TelemetryHttpModule` to `Web.config` +- Adds all required assembly references to the project +- Updates `packages.config` + +## Step 2 — Set connection string + +In `ApplicationInsights.config`, replace the placeholder `` with your actual connection string: + +```xml +InstrumentationKey=your-key;IngestionEndpoint=https://dc.applicationinsights.azure.com/ +``` + +Or set the `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable. + +## Step 3 — Run and verify + +1. Build and run the application (F5 in Visual Studio) +2. Make a few HTTP requests +3. Check Azure Portal → Application Insights → Live Metrics to see incoming telemetry +4. Check Transaction Search for request and dependency data + +## What's collected automatically + +- All incoming HTTP requests (path, status code, duration) +- SQL and HTTP outgoing dependency calls +- Unhandled exceptions +- Performance counters (CPU, memory, request rate) +- Live Metrics stream + +## Optional: Custom telemetry + +To track custom events or metrics, use `TelemetryClient`: + +```csharp +var client = new TelemetryClient(TelemetryConfiguration.CreateDefault()); +client.TrackEvent("OrderCreated"); +client.TrackMetric("ProcessingTime", elapsed); +``` + +## Optional: Custom processors + +In `Global.asax.cs`: + +```csharp +protected void Application_Start() +{ + var config = TelemetryConfiguration.CreateDefault(); + config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + config.ConfigureOpenTelemetryBuilder(otel => + { + otel.WithTracing(tracing => tracing.AddProcessor()); + }); + + AreaRegistration.RegisterAllAreas(); + RouteConfig.RegisterRoutes(RouteTable.Routes); +} +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-distro-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-distro-setup.md new file mode 100644 index 0000000000..b042c180ec --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-distro-setup.md @@ -0,0 +1,156 @@ +--- +title: Basic ASP.NET Core Setup +category: example +applies-to: 3.x +source: ApplicationInsightsDemo/Program.cs +--- + +# Basic ASP.NET Core Setup + +**Category:** Example +**Applies to:** 3.x + +## Overview + +Complete working example of adding Azure Monitor to a new ASP.NET Core application. + +## Step 1: Add Package + +```bash +dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore +``` + +Or in `.csproj`: + +```xml + +``` + +## Step 2: Configure in Program.cs + +### Minimal Setup + +```csharp +using Azure.Monitor.OpenTelemetry.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add Azure Monitor - one line! +builder.Services.AddOpenTelemetry().UseAzureMonitor(); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.MapControllers(); +app.Run(); +``` + +### With Configuration Options + +```csharp +using Azure.Monitor.OpenTelemetry.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenTelemetry().UseAzureMonitor(options => +{ + // Connection string from configuration + options.ConnectionString = builder.Configuration["AzureMonitor:ConnectionString"]; + + // Sample 50% of requests in production + if (!builder.Environment.IsDevelopment()) + { + options.SamplingRatio = 0.5f; + } +}); + +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.MapControllers(); +app.Run(); +``` + +## Step 3: Configure Connection String + +### Option A: Environment Variable (Recommended for Production) + +```bash +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEndpoint=https://..." +``` + +### Option B: appsettings.json + +```json +{ + "AzureMonitor": { + "ConnectionString": "InstrumentationKey=xxx;IngestionEndpoint=https://..." + } +} +``` + +### Option C: User Secrets (Development) + +```bash +dotnet user-secrets set "AzureMonitor:ConnectionString" "InstrumentationKey=xxx;..." +``` + +## Complete Program.cs + +```csharp +using Azure.Monitor.OpenTelemetry.AspNetCore; + +var builder = WebApplication.CreateBuilder(args); + +// Add services +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// Add Azure Monitor OpenTelemetry +builder.Services.AddOpenTelemetry().UseAzureMonitor(); + +var app = builder.Build(); + +// Configure pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); +``` + +## What You Get Automatically + +After this setup, Azure Monitor will collect: + +| Signal | Data | +|--------|------| +| **Requests** | All incoming HTTP requests with timing, status codes | +| **Dependencies** | Outgoing HTTP calls, SQL queries, Azure SDK calls | +| **Exceptions** | Unhandled exceptions with stack traces | +| **Logs** | ILogger output (Information level and above) | +| **Metrics** | Request rate, response time, CPU, memory | + +## Verify It Works + +1. Run your application +2. Make some requests +3. Check Application Insights in Azure Portal (may take 2-5 minutes) +4. Look for: + - Live Metrics (immediate) + - Transaction Search (requests, dependencies) + - Failures (exceptions) + +## See Also + +- Azure Monitor Distro(see in azure-monitor-distro.md) +- UseAzureMonitor API(see in UseAzureMonitor.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-setup.md index b042c180ec..e4a54f2bfb 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-setup.md +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/aspnetcore-setup.md @@ -1,29 +1,28 @@ --- -title: Basic ASP.NET Core Setup +title: ASP.NET Core Setup with Application Insights category: example applies-to: 3.x -source: ApplicationInsightsDemo/Program.cs --- -# Basic ASP.NET Core Setup +# ASP.NET Core Setup with Application Insights -**Category:** Example +**Category:** Example **Applies to:** 3.x ## Overview -Complete working example of adding Azure Monitor to a new ASP.NET Core application. +Complete working example of adding Application Insights to a new ASP.NET Core application using `Microsoft.ApplicationInsights.AspNetCore`. ## Step 1: Add Package ```bash -dotnet add package Azure.Monitor.OpenTelemetry.AspNetCore +dotnet add package Microsoft.ApplicationInsights.AspNetCore ``` Or in `.csproj`: ```xml - + ``` ## Step 2: Configure in Program.cs @@ -31,12 +30,10 @@ Or in `.csproj`: ### Minimal Setup ```csharp -using Azure.Monitor.OpenTelemetry.AspNetCore; - var builder = WebApplication.CreateBuilder(args); -// Add Azure Monitor - one line! -builder.Services.AddOpenTelemetry().UseAzureMonitor(); +// Add Application Insights - one line! +builder.Services.AddApplicationInsightsTelemetry(); builder.Services.AddControllers(); @@ -49,20 +46,15 @@ app.Run(); ### With Configuration Options ```csharp -using Azure.Monitor.OpenTelemetry.AspNetCore; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddOpenTelemetry().UseAzureMonitor(options => +builder.Services.AddApplicationInsightsTelemetry(options => { - // Connection string from configuration - options.ConnectionString = builder.Configuration["AzureMonitor:ConnectionString"]; - - // Sample 50% of requests in production - if (!builder.Environment.IsDevelopment()) - { - options.SamplingRatio = 0.5f; - } + options.ConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"]; + options.EnableQuickPulseMetricStream = true; + options.SamplingRatio = 0.5f; // Collect 50% of telemetry }); builder.Services.AddControllers(); @@ -85,7 +77,7 @@ export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEn ```json { - "AzureMonitor": { + "ApplicationInsights": { "ConnectionString": "InstrumentationKey=xxx;IngestionEndpoint=https://..." } } @@ -94,14 +86,12 @@ export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEn ### Option C: User Secrets (Development) ```bash -dotnet user-secrets set "AzureMonitor:ConnectionString" "InstrumentationKey=xxx;..." +dotnet user-secrets set "ApplicationInsights:ConnectionString" "InstrumentationKey=xxx;..." ``` ## Complete Program.cs ```csharp -using Azure.Monitor.OpenTelemetry.AspNetCore; - var builder = WebApplication.CreateBuilder(args); // Add services @@ -109,8 +99,8 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -// Add Azure Monitor OpenTelemetry -builder.Services.AddOpenTelemetry().UseAzureMonitor(); +// Add Application Insights telemetry +builder.Services.AddApplicationInsightsTelemetry(); var app = builder.Build(); @@ -130,7 +120,7 @@ app.Run(); ## What You Get Automatically -After this setup, Azure Monitor will collect: +After this setup, Application Insights will collect: | Signal | Data | |--------|------| @@ -139,6 +129,19 @@ After this setup, Azure Monitor will collect: | **Exceptions** | Unhandled exceptions with stack traces | | **Logs** | ILogger output (Information level and above) | | **Metrics** | Request rate, response time, CPU, memory | +| **Live Metrics** | Real-time monitoring via QuickPulse | +| **Performance Counters** | GC, thread pool, process metrics | + +## Extending with OpenTelemetry + +Application Insights 3.x is built on OpenTelemetry. You can add custom sources and meters: + +```csharp +builder.Services.AddApplicationInsightsTelemetry(); +builder.Services.AddOpenTelemetry() + .WithTracing(t => t.AddSource("MyApp.CustomSource")) + .WithMetrics(m => m.AddMeter("MyApp.CustomMeter")); +``` ## Verify It Works @@ -152,5 +155,6 @@ After this setup, Azure Monitor will collect: ## See Also -- Azure Monitor Distro(see in azure-monitor-distro.md) -- UseAzureMonitor API(see in UseAzureMonitor.md) +- Application Insights for ASP.NET Core (see in appinsights-aspnetcore.md) +- AddApplicationInsightsTelemetry API (see in AddApplicationInsightsTelemetry.md) +- App Insights 2.x to 3.x Migration (see in appinsights-2x-to-3x-code-migration.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/workerservice-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/workerservice-setup.md new file mode 100644 index 0000000000..2cbbcf20ea --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/dotnet/workerservice-setup.md @@ -0,0 +1,154 @@ +--- +title: Basic Worker Service Setup +category: example +applies-to: 3.x +--- + +# Basic Worker Service Setup + +**Category:** Example +**Applies to:** 3.x + +## Overview + +Complete working example of adding Application Insights telemetry to a .NET Worker Service application using `Microsoft.ApplicationInsights.WorkerService`. + +Worker Services are long-running background services that don't handle HTTP requests directly. Common examples include: +- Background job processors +- Message queue consumers +- Scheduled task runners +- Windows Services / Linux daemons + +## Step 1: Add Package + +```bash +dotnet add package Microsoft.ApplicationInsights.WorkerService --version 3.0.0-rc1 +``` + +Or in `.csproj`: + +```xml + +``` + +## Step 2: Configure in Program.cs + +### Using Host.CreateDefaultBuilder (Traditional Pattern) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Add Application Insights telemetry + services.AddApplicationInsightsTelemetryWorkerService(); + + // Add your worker service + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); +``` + +### Using Host.CreateApplicationBuilder (Modern Pattern) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = Host.CreateApplicationBuilder(args); + +// Add Application Insights telemetry +builder.Services.AddApplicationInsightsTelemetryWorkerService(); + +// Add your worker service +builder.Services.AddHostedService(); + +var host = builder.Build(); +await host.RunAsync(); +``` + +### With Configuration Options + +```csharp +services.AddApplicationInsightsTelemetryWorkerService(options => +{ + // Explicitly set connection string + options.ConnectionString = configuration["ApplicationInsights:ConnectionString"]; + + // Enable dependency tracking + options.EnableDependencyTrackingTelemetryModule = true; +}); +``` + +## Step 3: Configure Connection String + +### Option A: Environment Variable (Recommended for Production) + +```bash +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEndpoint=https://..." +``` + +### Option B: appsettings.json + +```json +{ + "ApplicationInsights": { + "ConnectionString": "InstrumentationKey=xxx;IngestionEndpoint=https://..." + } +} +``` + +## What Gets Instrumented Automatically + +The `AddApplicationInsightsTelemetryWorkerService()` method configures: + +- **Dependency Tracking**: HTTP client calls, SQL queries, Azure SDK calls +- **Performance Counters**: CPU, memory, GC metrics +- **Exception Tracking**: Unhandled exceptions +- **Custom Telemetry**: Via `TelemetryClient` injection + +## Adding Custom Telemetry + +Inject `TelemetryClient` into your worker: + +```csharp +public class Worker : BackgroundService +{ + private readonly TelemetryClient _telemetryClient; + private readonly ILogger _logger; + + public Worker(TelemetryClient telemetryClient, ILogger logger) + { + _telemetryClient = telemetryClient; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + // Track custom events + _telemetryClient.TrackEvent("WorkerIteration", new Dictionary + { + ["timestamp"] = DateTime.UtcNow.ToString("O") + }); + + // Track custom metrics + _telemetryClient.TrackMetric("ItemsProcessed", processedCount); + + await Task.Delay(1000, stoppingToken); + } + } +} +``` + +## Best Practices + +1. **Use structured logging**: ILogger integration sends logs to Application Insights automatically +2. **Track operation context**: Use `TelemetryClient.StartOperation` for long-running operations +3. **Flush on shutdown**: Call `TelemetryClient.Flush()` before application exits +4. **Configure sampling**: For high-volume services, configure adaptive sampling to control costs diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/bunyan-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/bunyan-setup.md new file mode 100644 index 0000000000..9976ab8784 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/bunyan-setup.md @@ -0,0 +1,301 @@ +# Basic Azure Monitor Setup for Node.js with Bunyan Logging + +This guide shows how to add Azure Monitor OpenTelemetry to a Node.js application using Bunyan for logging. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application with Bunyan (`bunyan` package) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor with Bunyan log collection enabled +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + instrumentationOptions: { + bunyan: { enabled: true } + } +}); + +// Now load your application code +const express = require('express'); +const bunyan = require('bunyan'); + +// Create Bunyan logger +const logger = bunyan.createLogger({ + name: 'my-app', + level: process.env.LOG_LEVEL || 'info', + serializers: bunyan.stdSerializers +}); + +const app = express(); +const port = process.env.PORT || 3000; + +app.use(express.json()); + +// Request logging middleware +app.use((req, res, next) => { + logger.info({ req }, 'Incoming request'); + next(); +}); + +app.get('/api/users', (req, res) => { + logger.info('Fetching users'); + res.json([{ id: 1, name: 'Alice' }]); +}); + +app.listen(port, () => { + logger.info({ port }, 'Server listening'); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +LOG_LEVEL=info +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## What Gets Collected + +With Bunyan instrumentation enabled, the following log data is sent to Azure Monitor: + +- **Log level**: fatal, error, warn, info, debug, trace +- **Log message**: The log content +- **Log fields**: All additional fields passed to the logger +- **Timestamp**: When the log was created +- **Trace context**: Correlation with distributed traces + +## Log Level Mapping + +Bunyan log levels are mapped to Application Insights severity levels: + +| Bunyan Level | Numeric | Application Insights Severity | +|-------------|---------|------------------------------| +| fatal | 60 | Critical | +| error | 50 | Error | +| warn | 40 | Warning | +| info | 30 | Information | +| debug | 20 | Verbose | +| trace | 10 | Verbose | + +## Step 4: Structured Logging Best Practices + +Bunyan excels at structured logging. Use its features effectively: + +### Add Context to Logs + +```javascript +// Good: Structured logging with context +logger.info({ userId: user.id, email: user.email }, 'User created'); + +// Good: Error logging with error serializer +logger.error({ err: error, email: req.body.email }, 'Failed to create user'); + +// Good: Request/response logging +logger.info({ req, res }, 'Request completed'); +``` + +### Child Loggers for Request Context + +```javascript +app.use((req, res, next) => { + // Create a child logger with request context + req.log = logger.child({ + requestId: req.headers['x-request-id'] || Math.random().toString(36).substr(2, 9), + path: req.path, + method: req.method + }); + next(); +}); + +app.get('/api/users/:id', (req, res) => { + // Use the request-scoped logger + req.log.info({ userId: req.params.id }, 'Fetching user'); + + try { + const user = getUserById(req.params.id); + req.log.debug({ user }, 'User found'); + res.json(user); + } catch (error) { + req.log.error({ err: error, userId: req.params.id }, 'User not found'); + res.status(404).json({ error: 'User not found' }); + } +}); +``` + +### Custom Serializers + +```javascript +const logger = bunyan.createLogger({ + name: 'my-app', + serializers: { + ...bunyan.stdSerializers, + user: (user) => ({ + id: user.id, + email: user.email, + role: user.role + }), + order: (order) => ({ + id: order.id, + total: order.total, + itemCount: order.items.length + }) + } +}); + +// Use custom serializers +logger.info({ user }, 'User logged in'); +logger.info({ order }, 'Order created'); +``` + +## Step 5: Custom Telemetry (Optional) + +Combine logging with custom spans: + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/orders', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-order', async (span) => { + req.log.info({ itemCount: req.body.items.length }, 'Creating order'); + + try { + const order = await createOrder(req.body); + + req.log.info({ order }, 'Order created successfully'); + + span.setAttribute('order.id', order.id); + res.status(201).json(order); + } catch (error) { + req.log.error({ err: error }, 'Failed to create order'); + + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Failed to create order' }); + } finally { + span.end(); + } + }); +}); +``` + +## Viewing Logs in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Logs" under Monitoring +3. Query traces table: + +```kusto +traces +| where timestamp > ago(1h) +| where customDimensions.name == "my-app" +| project timestamp, message, severityLevel, customDimensions +| order by timestamp desc +``` + +4. Use "Transaction search" to see logs correlated with requests + +## Troubleshooting + +### Bunyan logs not appearing + +1. Ensure `bunyan` instrumentation is enabled in options +2. Verify `useAzureMonitor()` is called **before** importing `bunyan` +3. Check that the connection string is valid + +### Using Bunyan with Next.js + +> **Note**: The standard setup in this guide (Steps 1-5) works for Express, Fastify, NestJS, and other standard Node.js frameworks with no special configuration. The issues below are **specific to Next.js** because it bundles server-side code with webpack. + +Bunyan has optional native dependencies (`dtrace-provider`, `source-map-support`) that Next.js's webpack bundler cannot resolve. You'll see errors like: + +- `Module not found: Can't resolve 'source-map-support'` +- `Module not found: Can't resolve './src/build'` (from `dtrace-provider`) + +**Fix**: Add `bunyan` to both `serverComponentsExternalPackages` and `webpack.externals` in your `next.config.js`: + +```javascript +const nextConfig = { + experimental: { + instrumentationHook: true, + serverComponentsExternalPackages: [ + '@azure/monitor-opentelemetry', + // ... other OpenTelemetry packages ... + 'bunyan', + ], + }, + webpack: (config, { isServer }) => { + if (isServer) { + config.externals = config.externals || []; + config.externals.push({ + // ... other OpenTelemetry externals ... + bunyan: 'commonjs bunyan', + }); + } + return config; + }, +}; +``` + +In Next.js, also mark API routes using bunyan with `export const runtime = 'nodejs'` to ensure they run on the Node.js runtime (not Edge). This is a Next.js-specific concept and does not apply to standard Node.js apps. + +See the Next.js Setup Guide(see in basic-setup-nextjs.md) for the full Next.js + bunyan configuration. + +### Log fields not appearing correctly + +Ensure you're using the correct Bunyan logging format: + +```javascript +// Correct: fields object first, then message +logger.info({ userId: 123 }, 'User action'); + +// Incorrect: message first (fields won't be captured properly) +logger.info('User action', { userId: 123 }); +``` + +### Too many logs being sent + +Control log volume by adjusting the log level: + +```javascript +const logger = bunyan.createLogger({ + name: 'my-app', + level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug' +}); +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/console-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/console-setup.md new file mode 100644 index 0000000000..488bf31e7c --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/console-setup.md @@ -0,0 +1,284 @@ +# Basic Azure Monitor Setup for Node.js with Console Logging + +This guide shows how to add Azure Monitor to a Node.js application that uses built-in console logging. + +> **Important**: To capture `console.log`, `console.warn`, `console.error`, etc. as telemetry in Application Insights, you must use the `applicationinsights` npm package (the Application Insights SDK). The `@azure/monitor-opentelemetry` package does **not** support automatic console log collection — it only supports structured logging libraries like Bunyan and Winston. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application using console logging +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install applicationinsights +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const appInsights = require('applicationinsights'); + +// Initialize Application Insights with console log collection +appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) + .setAutoCollectConsole(true, true) // Enable console.log and console.error collection + .start(); + +// Now load your application code +const express = require('express'); + +const app = express(); +const port = process.env.PORT || 3000; + +app.use(express.json()); + +// Request logging middleware +app.use((req, res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`); + next(); +}); + +app.get('/api/users', (req, res) => { + console.log('Fetching users'); + res.json([{ id: 1, name: 'Alice' }]); +}); + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## Why `applicationinsights` Instead of `@azure/monitor-opentelemetry`? + +The `@azure/monitor-opentelemetry` package is the recommended OpenTelemetry-based SDK for Node.js, but it only supports automatic log collection from **Bunyan** and **Winston** logging libraries. It does **not** have a `console` instrumentation option. + +The `applicationinsights` package (Application Insights Node.js SDK) provides built-in support for capturing `console.log` output via `setAutoCollectConsole(true)`. If you want to use the OpenTelemetry-based SDK instead, consider migrating to Winston or Bunyan for structured logging. + +## What Gets Collected + +With `setAutoCollectConsole(true, true)` enabled, the following is captured: + +| Console Method | Application Insights Severity | +|---------------|------------------------------| +| console.error() | Error | +| console.warn() | Warning | +| console.info() | Information | +| console.log() | Information | +| console.debug() | Verbose | +| console.trace() | Verbose | + +## Step 4: Best Practices for Console Logging + +### Use Appropriate Log Levels + +```javascript +// Error conditions +console.error('Failed to connect to database:', error.message); + +// Warning conditions +console.warn('API rate limit approaching:', currentRate); + +// Informational messages +console.info('User logged in:', userId); +console.log('Processing request for:', endpoint); + +// Debug information +console.debug('Request body:', JSON.stringify(body)); +``` + +### Structured-ish Logging with Console + +While console doesn't support true structured logging, you can format messages consistently: + +```javascript +function log(level, message, data = {}) { + const timestamp = new Date().toISOString(); + const dataStr = Object.keys(data).length ? ` ${JSON.stringify(data)}` : ''; + console[level](`[${timestamp}] ${message}${dataStr}`); +} + +// Usage +log('info', 'User created', { userId: 123, email: 'user@example.com' }); +log('error', 'Database connection failed', { host: 'localhost', error: err.message }); +``` + +### Request Context Helper + +```javascript +function createRequestLogger(req) { + const requestId = req.headers['x-request-id'] || Math.random().toString(36).substr(2, 9); + + return { + info: (message, data = {}) => { + console.log(`[${requestId}] ${message}`, data); + }, + error: (message, data = {}) => { + console.error(`[${requestId}] ${message}`, data); + }, + warn: (message, data = {}) => { + console.warn(`[${requestId}] ${message}`, data); + } + }; +} + +app.use((req, res, next) => { + req.log = createRequestLogger(req); + next(); +}); + +app.get('/api/users/:id', (req, res) => { + req.log.info(`Fetching user ${req.params.id}`); + // ... +}); +``` + +## Step 5: Custom Telemetry (Optional) + +Combine logging with custom spans: + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/orders', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-order', async (span) => { + console.log('Creating order with', req.body.items.length, 'items'); + + try { + const order = await createOrder(req.body); + + console.log('Order created successfully:', order.id); + + span.setAttribute('order.id', order.id); + res.status(201).json(order); + } catch (error) { + console.error('Failed to create order:', error.message); + + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Failed to create order' }); + } finally { + span.end(); + } + }); +}); +``` + +## When to Consider Upgrading to a Logging Library + +Console logging is fine for simple applications, but consider Winston or Bunyan when you need: + +- **Structured logging**: Proper JSON logging with metadata +- **Log levels**: Configurable filtering by environment +- **Multiple transports**: File, HTTP, or other destinations +- **Better performance**: Async logging for high-throughput apps +- **Request correlation**: Automatic trace context propagation + +## Viewing Logs in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Logs" under Monitoring +3. Query traces table: + +```kusto +traces +| where timestamp > ago(1h) +| project timestamp, message, severityLevel +| order by timestamp desc +``` + +4. Filter by severity: + +```kusto +traces +| where timestamp > ago(1h) +| where severityLevel >= 3 // Warning and above +| project timestamp, message, severityLevel +| order by timestamp desc +``` + +## Troubleshooting + +### Console logs not appearing + +1. Ensure `setAutoCollectConsole(true, true)` is called during setup +2. Verify `appInsights.setup().start()` is called **before** any console.log calls +3. Check that the connection string is valid +4. For short-lived apps (scripts that exit immediately), call `appInsights.defaultClient.flush()` before the process exits to ensure telemetry is sent + +### Too many logs being sent + +Console instrumentation captures all console output. To reduce volume: + +1. Use sampling: + +```javascript +const appInsights = require('applicationinsights'); +appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) + .setAutoCollectConsole(true, true) + .start(); + +// Set sampling percentage (e.g., 50%) +appInsights.defaultClient.config.samplingPercentage = 50; +``` + +2. Or only collect console.error (not console.log): + +```javascript +// First parameter: collect console.log (false = disabled) +// Second parameter: collect console.error (true = enabled) +appInsights.setup(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING) + .setAutoCollectConsole(false, true) + .start(); +``` + +## Migration Path to Structured Logging + +When you're ready to upgrade, here's a simple migration to Winston: + +```javascript +// Before: console +console.log('User created:', userId); +console.error('Database error:', error.message); + +// After: Winston +const winston = require('winston'); +const logger = winston.createLogger({ + level: 'info', + format: winston.format.json(), + transports: [new winston.transports.Console()] +}); + +logger.info('User created', { userId }); +logger.error('Database error', { error: error.message }); +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/express-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/express-setup.md new file mode 100644 index 0000000000..f8d87d9cde --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/express-setup.md @@ -0,0 +1,169 @@ +# Basic Azure Monitor Setup for Express.js + +This guide shows how to add Azure Monitor OpenTelemetry to an Express.js application. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Express.js application +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now load your application code +const express = require('express'); +const app = express(); +const port = process.env.PORT || 3000; + +// Your middleware and routes +app.use(express.json()); + +app.get('/', (req, res) => { + res.json({ message: 'Hello World!' }); +}); + +app.get('/api/users', (req, res) => { + // This request will be automatically tracked + res.json([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]); +}); + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## Step 4: Add Custom Telemetry (Optional) + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.get('/api/process', async (req, res) => { + const span = trace.getActiveSpan(); + + // Add custom attributes + span?.setAttribute('user.id', req.headers['user-id']); + span?.setAttribute('operation.type', 'data-processing'); + + try { + // Your business logic + const result = await processData(); + res.json(result); + } catch (error) { + // Exceptions are automatically tracked + span?.recordException(error); + res.status(500).json({ error: 'Processing failed' }); + } +}); +``` + +## What Gets Tracked Automatically + +✅ **HTTP Requests**: All incoming requests with duration, status, URL +✅ **Dependencies**: Outgoing HTTP calls, database queries +✅ **Exceptions**: Unhandled errors and exceptions +✅ **Performance**: Response times and request counts +✅ **Custom Logs**: `console.log()` statements are captured as traces + +## Verify It Works + +1. Start your application: + ```bash + npm start + ``` + +2. Make some HTTP requests: + ```bash + curl http://localhost:3000/ + curl http://localhost:3000/api/users + ``` + +3. Check Azure Portal: + - Navigate to your Application Insights resource + - Go to "Transaction search" or "Live Metrics" + - You should see requests appearing within 1-2 minutes + +## Complete package.json Example + +```json +{ + "name": "express-azure-monitor-demo", + "version": "1.0.0", + "description": "Express app with Azure Monitor", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "dependencies": { + "@azure/monitor-opentelemetry": "^1.0.0", + "express": "^4.18.0", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.0" + } +} +``` + +## Troubleshooting + +**No telemetry appearing?** +- Verify connection string is correct +- Ensure `useAzureMonitor()` is called BEFORE loading Express +- Check console for error messages +- Wait 2-3 minutes for initial data to appear + +**Performance impact?** +- Azure Monitor has minimal overhead (<5% in most cases) +- Use sampling for high-traffic applications +- Disable in development if needed + +## Next Steps + +- Configure custom dimensions and metrics +- Set up alerts and dashboards in Azure Portal +- Enable profiler for performance analysis +- Add distributed tracing across microservices diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/fastify-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/fastify-setup.md new file mode 100644 index 0000000000..4481731da1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/fastify-setup.md @@ -0,0 +1,237 @@ +# Basic Azure Monitor Setup for Fastify + +This guide shows how to add Azure Monitor OpenTelemetry to a Fastify application. + +## Prerequisites + +- Node.js 18.x or higher +- npm or yarn +- Fastify application +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Update your main entry point (typically `index.js`, `server.js`, or `app.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration - must be called before other requires +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now load your application code +const fastify = require('fastify')({ logger: true }); + +// Register routes +fastify.get('/', async (request, reply) => { + return { message: 'Hello World!' }; +}); + +fastify.get('/api/users', async (request, reply) => { + // This request will be automatically tracked + return [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' } + ]; +}); + +// Start server +const start = async () => { + try { + await fastify.listen({ port: process.env.PORT || 3000 }); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +}; + +start(); +``` + +> **Important**: `useAzureMonitor()` must be called before requiring Fastify or any other modules to ensure proper instrumentation. + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## Step 4: Add Custom Telemetry (Optional) + +```javascript +const { trace } = require('@opentelemetry/api'); + +fastify.get('/api/process/:id', async (request, reply) => { + const span = trace.getActiveSpan(); + + // Add custom attributes to the current span + span?.setAttribute('process.id', request.params.id); + span?.setAttribute('operation.type', 'data-processing'); + + try { + // Your business logic + const result = await processData(request.params.id); + return result; + } catch (error) { + // Exceptions are automatically tracked + span?.recordException(error); + reply.status(500).send({ error: 'Processing failed' }); + } +}); +``` + +## Using with TypeScript + +For TypeScript projects, create your entry file: + +```typescript +// IMPORTANT: This must be the first import +import { useAzureMonitor } from '@azure/monitor-opentelemetry'; + +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +import Fastify from 'fastify'; + +const fastify = Fastify({ logger: true }); + +fastify.get('/', async () => { + return { message: 'Hello World!' }; +}); + +fastify.listen({ port: 3000 }); +``` + +## What Gets Tracked Automatically + +✅ **HTTP Requests**: All incoming requests with duration, status, URL +✅ **Dependencies**: Outgoing HTTP calls, database queries +✅ **Exceptions**: Unhandled errors and exceptions +✅ **Performance**: Response times and request counts +✅ **Custom Logs**: Fastify logger output is captured as traces + +## Using with Fastify Plugins + +Azure Monitor works seamlessly with Fastify plugins: + +```javascript +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +const fastify = require('fastify')({ logger: true }); + +// Register plugins - they will be automatically instrumented +fastify.register(require('@fastify/postgres'), { + connectionString: process.env.DATABASE_URL +}); + +fastify.register(require('@fastify/redis'), { + host: process.env.REDIS_HOST +}); + +// Routes using the plugins +fastify.get('/users', async (request, reply) => { + const { rows } = await fastify.pg.query('SELECT * FROM users'); + return rows; +}); +``` + +## Verify It Works + +1. Start your application: + ```bash + npm start + ``` + +2. Make some HTTP requests: + ```bash + curl http://localhost:3000/ + curl http://localhost:3000/api/users + ``` + +3. Check Azure Portal: + - Navigate to your Application Insights resource + - Go to "Transaction search" or "Live Metrics" + - You should see requests appearing within 1-2 minutes + +## Complete package.json Example + +```json +{ + "name": "fastify-azure-monitor-demo", + "version": "1.0.0", + "description": "Fastify app with Azure Monitor", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "dependencies": { + "@azure/monitor-opentelemetry": "^1.0.0", + "fastify": "^4.24.0", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "nodemon": "^3.0.0" + } +} +``` + +## Troubleshooting + +**No telemetry appearing?** +- Verify connection string is correct +- Ensure `useAzureMonitor()` is called BEFORE requiring Fastify +- Check console for error messages +- Wait 2-3 minutes for initial data to appear + +**Fastify logger not working with telemetry?** +- Both work independently; Fastify logs go to stdout, telemetry goes to Azure +- Use `@opentelemetry/api` for custom spans within telemetry + +**Performance impact?** +- Azure Monitor has minimal overhead (<5% in most cases) +- Use sampling for high-traffic applications +- Disable in development if needed + +## Next Steps + +- Configure custom dimensions and metrics +- Set up alerts and dashboards in Azure Portal +- Enable profiler for performance analysis +- Add distributed tracing across microservices diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/langchain-js-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/langchain-js-setup.md new file mode 100644 index 0000000000..616c54b3d6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/langchain-js-setup.md @@ -0,0 +1,310 @@ +# Basic Azure Monitor Setup for LangChain.js + +This guide shows how to add Azure Monitor OpenTelemetry to a LangChain.js application for observability into LLM calls, chains, and agents. + +## Prerequisites + +- Node.js 18.x or higher +- npm or yarn +- LangChain.js application +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Create Tracing File + +Create a separate tracing file to ensure OpenTelemetry initializes before LangChain imports. This is critical for proper instrumentation. + +**For CommonJS projects** - create `tracing.js`: + +```javascript +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +// This must be called before any other imports to ensure proper instrumentation +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); +``` + +**For ES Module projects** - create `tracing.mjs`: + +```javascript +import { useAzureMonitor } from '@azure/monitor-opentelemetry'; + +// Enable Azure Monitor integration +// This must be called before any other imports to ensure proper instrumentation +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); +``` + +## Step 3: Import Tracing First + +Update your main entry point to import tracing **as the very first line**: + +**For CommonJS** (`index.js`): + +```javascript +require('./tracing'); // MUST be the first import + +const { ChatOpenAI } = require('@langchain/openai'); +const { PromptTemplate } = require('@langchain/core/prompts'); +const { StringOutputParser } = require('@langchain/core/output_parsers'); + +// Your LangChain application code +async function main() { + const model = new ChatOpenAI({ + modelName: 'gpt-4', + temperature: 0.7 + }); + + const prompt = PromptTemplate.fromTemplate( + 'Tell me a short joke about {topic}' + ); + + const chain = prompt.pipe(model).pipe(new StringOutputParser()); + + const result = await chain.invoke({ topic: 'programming' }); + console.log(result); +} + +main().catch(console.error); +``` + +**For ES Modules** (`index.mjs`): + +```javascript +import './tracing.mjs'; // MUST be the first import + +import { ChatOpenAI } from '@langchain/openai'; +import { PromptTemplate } from '@langchain/core/prompts'; +import { StringOutputParser } from '@langchain/core/output_parsers'; + +// Your LangChain application code +async function main() { + const model = new ChatOpenAI({ + modelName: 'gpt-4', + temperature: 0.7 + }); + + const prompt = PromptTemplate.fromTemplate( + 'Tell me a short joke about {topic}' + ); + + const chain = prompt.pipe(model).pipe(new StringOutputParser()); + + const result = await chain.invoke({ topic: 'programming' }); + console.log(result); +} + +main().catch(console.error); +``` + +## Step 4: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +OPENAI_API_KEY=your-openai-key +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Update your tracing file to load environment variables first: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of tracing setup +``` + +## Step 5: Add Custom Telemetry (Optional) + +Track custom attributes for LLM operations: + +```javascript +const { trace } = require('@opentelemetry/api'); + +async function processWithTelemetry(userQuery) { + const span = trace.getActiveSpan(); + + // Add custom attributes + span?.setAttribute('llm.query.length', userQuery.length); + span?.setAttribute('llm.model', 'gpt-4'); + span?.setAttribute('operation.type', 'chat-completion'); + + try { + const result = await chain.invoke({ query: userQuery }); + + // Track response metrics + span?.setAttribute('llm.response.length', result.length); + span?.setAttribute('llm.success', true); + + return result; + } catch (error) { + span?.recordException(error); + span?.setAttribute('llm.success', false); + throw error; + } +} +``` + +## What Gets Tracked Automatically + +✅ **HTTP Requests**: Outgoing calls to LLM APIs (OpenAI, Azure OpenAI, etc.) +✅ **Dependencies**: External service calls and database queries +✅ **Exceptions**: Errors from LLM providers, rate limits, timeouts +✅ **Performance**: Latency of LLM calls and chains +✅ **Token Usage**: When using supported providers + +## Using with Different LLM Providers + +### Azure OpenAI + +```javascript +const { AzureChatOpenAI } = require('@langchain/openai'); + +const model = new AzureChatOpenAI({ + azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_DEPLOYMENT, + azureOpenAIApiVersion: '2024-02-15-preview', +}); +``` + +### Anthropic Claude + +```javascript +const { ChatAnthropic } = require('@langchain/anthropic'); + +const model = new ChatAnthropic({ + modelName: 'claude-3-opus-20240229', +}); +``` + +## Using with Agents + +```javascript +require('./tracing'); + +const { ChatOpenAI } = require('@langchain/openai'); +const { AgentExecutor, createOpenAIToolsAgent } = require('langchain/agents'); +const { TavilySearchResults } = require('@langchain/community/tools/tavily_search'); +const { trace } = require('@opentelemetry/api'); + +async function runAgent(input) { + const span = trace.getActiveSpan(); + span?.setAttribute('agent.input', input); + + const tools = [new TavilySearchResults()]; + const model = new ChatOpenAI({ modelName: 'gpt-4' }); + + const agent = await createOpenAIToolsAgent({ llm: model, tools, prompt }); + const executor = new AgentExecutor({ agent, tools }); + + const result = await executor.invoke({ input }); + + span?.setAttribute('agent.steps', result.intermediateSteps?.length || 0); + return result; +} +``` + +## Verify It Works + +1. Start your application: + ```bash + node index.js + ``` + +2. Run some LLM operations and check Azure Portal: + - Navigate to your Application Insights resource + - Go to "Transaction search" or "Application map" + - You should see outgoing requests to LLM APIs + - Check "Dependencies" to see LLM call latencies + +## Complete package.json Example + +**CommonJS:** +```json +{ + "name": "langchain-azure-monitor-demo", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@azure/monitor-opentelemetry": "^1.0.0", + "@langchain/core": "^0.2.0", + "@langchain/openai": "^0.2.0", + "dotenv": "^16.0.0" + } +} +``` + +**ES Modules:** +```json +{ + "name": "langchain-azure-monitor-demo", + "version": "1.0.0", + "type": "module", + "main": "index.mjs", + "scripts": { + "start": "node index.mjs" + }, + "dependencies": { + "@azure/monitor-opentelemetry": "^1.0.0", + "@langchain/core": "^0.2.0", + "@langchain/openai": "^0.2.0", + "dotenv": "^16.0.0" + } +} +``` + +## Project Structure + +``` +my-langchain-app/ +├── tracing.js ← Azure Monitor setup (load first) +├── index.js ← Main entry point +├── chains/ +│ └── qa-chain.js ← Your LangChain chains +├── .env ← Connection strings and API keys +└── package.json +``` + +## Troubleshooting + +**No telemetry appearing?** +- Verify the tracing import is the FIRST line in your entry file +- Check that connection string is correct +- Ensure `dotenv.config()` is called in tracing file before `useAzureMonitor()` + +**LLM calls not tracked?** +- Make sure OpenTelemetry initializes BEFORE importing LangChain +- HTTP instrumentation should capture LLM API calls automatically + +**ES Module vs CommonJS issues?** +- Check your `package.json` for `"type": "module"` +- Use `.mjs` extension for ES modules or `.cjs` for CommonJS +- Match import/require syntax to your module system + +## Next Steps + +- Add custom metrics for token usage and costs +- Set up alerts for LLM error rates or latency +- Create dashboards for LLM operation insights +- Enable distributed tracing for multi-service architectures +- Track RAG pipeline performance with custom spans diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/mongodb-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/mongodb-setup.md new file mode 100644 index 0000000000..3e0d8d0f8b --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/mongodb-setup.md @@ -0,0 +1,185 @@ +# Basic Azure Monitor Setup for Node.js with MongoDB + +This guide shows how to add Azure Monitor OpenTelemetry to a Node.js application using MongoDB. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application with MongoDB (`mongodb` package) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor - MongoDB operations will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now load your application code +const express = require('express'); +const { MongoClient } = require('mongodb'); + +const app = express(); +const port = process.env.PORT || 3000; + +// MongoDB connection +const mongoUrl = process.env.MONGODB_URL || 'mongodb://localhost:27017'; +const dbName = process.env.MONGODB_DB || 'mydb'; +let db; + +async function connectToDatabase() { + const client = new MongoClient(mongoUrl); + await client.connect(); + db = client.db(dbName); + console.log('Connected to MongoDB'); +} + +app.use(express.json()); + +app.get('/api/users', async (req, res) => { + // This query will be automatically tracked as a dependency + const users = await db.collection('users').find({}).limit(10).toArray(); + res.json(users); +}); + +connectToDatabase().then(() => { + app.listen(port, () => { + console.log(`Server listening on port ${port}`); + }); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +MONGODB_URL=mongodb://localhost:27017 +MONGODB_DB=mydb +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## What Gets Instrumented Automatically + +With Azure Monitor OpenTelemetry, the following MongoDB operations are automatically tracked: + +- **Operations**: find, insert, update, delete, aggregate, etc. +- **Operation duration**: Time taken for each database operation +- **Collection name**: Which collection was accessed +- **Database name**: Which database was used +- **Success/failure**: Operation status + +## Step 4: Add Custom Telemetry (Optional) + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/users', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-user', async (span) => { + try { + span.setAttribute('user.email', req.body.email); + + const result = await db.collection('users').insertOne({ + name: req.body.name, + email: req.body.email, + createdAt: new Date() + }); + + span.setAttribute('user.id', result.insertedId.toString()); + res.status(201).json({ _id: result.insertedId, ...req.body }); + } catch (error) { + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Database error' }); + } finally { + span.end(); + } + }); +}); +``` + +## Using with Mongoose + +If you're using Mongoose ODM, the setup is the same: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +const mongoose = require('mongoose'); +const express = require('express'); + +// Mongoose operations will automatically be instrumented +mongoose.connect(process.env.MONGODB_URL); + +const UserSchema = new mongoose.Schema({ + name: String, + email: String +}); + +const User = mongoose.model('User', UserSchema); + +const app = express(); + +app.get('/api/users', async (req, res) => { + const users = await User.find().limit(10); + res.json(users); +}); +``` + +## Viewing Telemetry in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Application Map" to see MongoDB as a dependency +3. Use "Transaction search" to find specific database operations +4. Check "Dependencies" under "Investigate" to see operation performance + +## Troubleshooting + +### MongoDB operations not appearing + +1. Ensure `useAzureMonitor()` is called **before** importing `mongodb` or `mongoose` +2. Verify the connection string is set correctly +3. Check that operations are being executed (not just connections) + +### High cardinality in telemetry + +MongoDB instrumentation captures collection names. For applications with dynamic collections, consider using a consistent naming pattern. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/mysql-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/mysql-setup.md new file mode 100644 index 0000000000..8dd98de339 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/mysql-setup.md @@ -0,0 +1,231 @@ +# Basic Azure Monitor Setup for Node.js with MySQL + +This guide shows how to add Azure Monitor OpenTelemetry to a Node.js application using MySQL. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application with MySQL (`mysql2` package) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor - MySQL queries will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now load your application code +const express = require('express'); +const mysql = require('mysql2/promise'); + +const app = express(); +const port = process.env.PORT || 3000; + +// MySQL connection pool +let pool; + +async function createPool() { + pool = mysql.createPool({ + host: process.env.MYSQL_HOST || 'localhost', + port: process.env.MYSQL_PORT || 3306, + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASSWORD || '', + database: process.env.MYSQL_DATABASE || 'mydb', + waitForConnections: true, + connectionLimit: 10 + }); + console.log('MySQL connection pool created'); +} + +app.use(express.json()); + +app.get('/api/users', async (req, res) => { + // This query will be automatically tracked as a dependency + const [rows] = await pool.execute('SELECT * FROM users LIMIT 10'); + res.json(rows); +}); + +createPool().then(() => { + app.listen(port, () => { + console.log(`Server listening on port ${port}`); + }); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=your_password +MYSQL_DATABASE=mydb +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## What Gets Instrumented Automatically + +With Azure Monitor OpenTelemetry, the following MySQL operations are automatically tracked: + +- **Queries**: All SQL queries executed via `mysql2` client +- **Query duration**: Time taken for each database operation +- **Query results**: Success/failure status +- **Connection info**: Database name and server details + +## Step 4: Add Custom Telemetry (Optional) + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/users', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-user', async (span) => { + try { + span.setAttribute('user.email', req.body.email); + + const [result] = await pool.execute( + 'INSERT INTO users (name, email) VALUES (?, ?)', + [req.body.name, req.body.email] + ); + + span.setAttribute('user.id', result.insertId); + res.status(201).json({ id: result.insertId, ...req.body }); + } catch (error) { + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Database error' }); + } finally { + span.end(); + } + }); +}); +``` + +## Using with Azure Database for MySQL + +For Azure Database for MySQL, update your configuration: + +```env +MYSQL_HOST=your-server.mysql.database.azure.com +MYSQL_PORT=3306 +MYSQL_USER=your_user@your-server +MYSQL_PASSWORD=your_password +MYSQL_DATABASE=mydb +``` + +And enable SSL in your connection: + +```javascript +const pool = mysql.createPool({ + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + ssl: { + rejectUnauthorized: true + } +}); +``` + +## Using with Sequelize ORM + +If you're using Sequelize, the setup is similar: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +const { Sequelize, DataTypes } = require('sequelize'); +const express = require('express'); + +const sequelize = new Sequelize( + process.env.MYSQL_DATABASE, + process.env.MYSQL_USER, + process.env.MYSQL_PASSWORD, + { + host: process.env.MYSQL_HOST, + dialect: 'mysql' + } +); + +const User = sequelize.define('User', { + name: DataTypes.STRING, + email: DataTypes.STRING +}); + +const app = express(); + +app.get('/api/users', async (req, res) => { + const users = await User.findAll({ limit: 10 }); + res.json(users); +}); +``` + +## Viewing Telemetry in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Application Map" to see MySQL as a dependency +3. Use "Transaction search" to find specific database operations +4. Check "Dependencies" under "Investigate" to see query performance + +## Troubleshooting + +### MySQL queries not appearing + +1. Ensure `useAzureMonitor()` is called **before** importing `mysql2` +2. Verify the connection string is set correctly +3. Check that queries are being executed (not just connections) + +### High latency in telemetry + +MySQL instrumentation captures all queries. For high-throughput applications, consider: + +```javascript +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + samplingRatio: 0.5 // Sample 50% of requests +}); +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/nestjs-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/nestjs-setup.md new file mode 100644 index 0000000000..5f73a13859 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/nestjs-setup.md @@ -0,0 +1,184 @@ +# Basic Azure Monitor Setup for NestJS + +This guide shows how to add Azure Monitor OpenTelemetry to a NestJS application. + +## Prerequisites + +- Node.js 18.x or higher +- npm or yarn +- NestJS application +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Create Tracing File + +Create a new file `src/tracing.ts` for OpenTelemetry initialization: + +```typescript +import { useAzureMonitor } from '@azure/monitor-opentelemetry'; + +// Enable Azure Monitor integration +// This must be called before any other imports to ensure proper instrumentation +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); +``` + +## Step 3: Import Tracing in main.ts + +Update your `src/main.ts` to import tracing **as the very first line**: + +```typescript +import './tracing'; // MUST be the first import + +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); +} +bootstrap(); +``` + +> **Important**: The tracing import must be before all other imports to ensure proper instrumentation of HTTP modules and other dependencies. + +## Step 4: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +PORT=3000 +``` + +Install and configure `@nestjs/config` for environment variables: + +```bash +npm install @nestjs/config +``` + +Update `app.module.ts`: + +```typescript +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + ], + // ... your other modules +}) +export class AppModule {} +``` + +## Step 5: Add Custom Telemetry (Optional) + +```typescript +import { Controller, Get, Param } from '@nestjs/common'; +import { trace } from '@opentelemetry/api'; + +@Controller('users') +export class UsersController { + @Get(':id') + async findOne(@Param('id') id: string) { + const span = trace.getActiveSpan(); + + // Add custom attributes to the current span + span?.setAttribute('user.id', id); + span?.setAttribute('operation.type', 'user-lookup'); + + try { + const user = await this.userService.findById(id); + return user; + } catch (error) { + // Exceptions are automatically tracked + span?.recordException(error); + throw error; + } + } +} +``` + +## What Gets Tracked Automatically + +✅ **HTTP Requests**: All incoming requests to your NestJS controllers +✅ **Dependencies**: Outgoing HTTP calls, database queries (TypeORM, Prisma, etc.) +✅ **Exceptions**: Unhandled errors and NestJS exceptions +✅ **Performance**: Response times, request counts, and latency +✅ **Custom Logs**: Console statements are captured as traces + +## Verify It Works + +1. Start your application: + ```bash + npm run start:dev + ``` + +2. Make some HTTP requests: + ```bash + curl http://localhost:3000/ + curl http://localhost:3000/users/1 + ``` + +3. Check Azure Portal: + - Navigate to your Application Insights resource + - Go to "Transaction search" or "Live Metrics" + - You should see requests appearing within 1-2 minutes + +## Complete package.json Example + +```json +{ + "name": "nestjs-azure-monitor-demo", + "version": "1.0.0", + "scripts": { + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main" + }, + "dependencies": { + "@azure/monitor-opentelemetry": "^1.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.0" + } +} +``` + +## Troubleshooting + +**No telemetry appearing?** +- Verify the tracing import is the FIRST line in `main.ts` +- Check that connection string is correct +- Ensure environment variables are loaded before `useAzureMonitor()` is called +- Wait 2-3 minutes for initial data to appear + +**TypeScript compilation errors?** +- Ensure `@types/node` is installed +- Add `"esModuleInterop": true` to tsconfig.json if needed + +**Performance impact?** +- Azure Monitor has minimal overhead (<5% in most cases) +- Use sampling for high-traffic applications + +## Next Steps + +- Configure custom dimensions and metrics +- Set up alerts and dashboards in Azure Portal +- Enable distributed tracing across microservices +- Add interceptors for custom span attributes diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/nextjs-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/nextjs-setup.md new file mode 100644 index 0000000000..4506567acd --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/nextjs-setup.md @@ -0,0 +1,320 @@ +# Basic Azure Monitor Setup for Next.js + +This guide shows how to add Azure Monitor OpenTelemetry to a Next.js application using the instrumentation hook. + +## Prerequisites + +- Node.js 18.x or higher +- npm or yarn +- Next.js 13.4+ application (App Router recommended) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Create Instrumentation File + +Create a new file `instrumentation.js` (or `instrumentation.ts`) in your project root: + +```javascript +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +export function register() { + // Only initialize on server-side + if (process.env.NEXT_RUNTIME === 'nodejs') { + useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } + }); + } +} +``` + +> **Note**: The `register()` function is called once when the Next.js server starts. The `NEXT_RUNTIME` check ensures telemetry only initializes on the server, not in Edge runtime. + +## Step 3: Enable Instrumentation Hook and Externalize Packages + +Update your `next.config.js` to enable the instrumentation hook **and** externalize server-only packages. Without this, Next.js's webpack bundler will try to resolve Node.js built-in modules (`fs`, `stream`, etc.) used by `@grpc/grpc-js` and other OpenTelemetry dependencies, causing `Module not found` errors. + +```javascript +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + instrumentationHook: true, + serverComponentsExternalPackages: [ + '@azure/monitor-opentelemetry', + '@opentelemetry/sdk-node', + '@opentelemetry/api', + '@opentelemetry/exporter-logs-otlp-grpc', + '@opentelemetry/otlp-grpc-exporter-base', + '@grpc/grpc-js', + '@grpc/proto-loader', + '@opentelemetry/instrumentation', + ], + }, + webpack: (config, { isServer }) => { + if (isServer) { + config.externals = config.externals || []; + config.externals.push({ + '@azure/monitor-opentelemetry': 'commonjs @azure/monitor-opentelemetry', + '@opentelemetry/sdk-node': 'commonjs @opentelemetry/sdk-node', + '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', + '@opentelemetry/api': 'commonjs @opentelemetry/api', + '@grpc/grpc-js': 'commonjs @grpc/grpc-js', + }); + } + return config; + }, +}; + +module.exports = nextConfig; +``` + +> **Important**: Both `serverComponentsExternalPackages` and `webpack.externals` are required. The `serverComponentsExternalPackages` tells Next.js to skip bundling these packages for Server Components, while the `webpack.externals` configuration ensures the instrumentation hook itself (which runs outside the Server Components context) also resolves these packages from `node_modules` at runtime instead of bundling them. + +> **Note (Next.js-specific)**: If you use logging libraries like **Bunyan** or **Winston** in a Next.js app, you must also add them to both `serverComponentsExternalPackages` and `webpack.externals`. Bunyan in particular has optional native dependencies (`dtrace-provider`, `source-map-support`) that webpack cannot resolve. This is not an issue in standard Node.js apps (Express, Fastify, etc.) where these libraries work without special configuration. See the [Using Logging Libraries](#using-logging-libraries-bunyan-winston) section below. + +## Step 4: Configure Connection String + +Create a `.env.local` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +``` + +> **Important**: Use `.env.local` for local development. For production, set this in your hosting platform's environment variables. + +## Step 5: Add Custom Telemetry (Optional) + +In your API routes or Server Components: + +```typescript +// app/api/users/route.ts +import { trace } from '@opentelemetry/api'; +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const span = trace.getActiveSpan(); + + // Add custom attributes + span?.setAttribute('api.endpoint', '/api/users'); + span?.setAttribute('operation.type', 'list-users'); + + try { + const users = await fetchUsers(); + return NextResponse.json(users); + } catch (error) { + span?.recordException(error as Error); + return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 }); + } +} +``` + +In Server Components: + +```typescript +// app/users/page.tsx +import { trace } from '@opentelemetry/api'; + +export default async function UsersPage() { + const span = trace.getActiveSpan(); + span?.setAttribute('page.name', 'users'); + + const users = await fetchUsers(); + + return ( +
+ {users.map(user => )} +
+ ); +} +``` + +## Using Logging Libraries (Bunyan, Winston) + +> **Next.js-specific**: The configuration in this section is only required because Next.js bundles server code with webpack. In standard Node.js applications (Express, Fastify, NestJS, etc.), Bunyan and Winston work out of the box — just install them, enable the instrumentation option, and use them. No externalization or runtime exports are needed. See the Bunyan Setup Guide(see in basic-setup-bunyan-nodejs.md) for the standard approach. + +In Next.js, you need extra configuration because webpack tries to bundle these libraries and their native/optional dependencies, which causes `Module not found` errors. + +### Step 1: Install the logging library + +```bash +npm install bunyan +``` + +### Step 2: Add to externals in next.config.js + +Add the logging library to **both** `serverComponentsExternalPackages` and `webpack.externals`: + +```javascript +experimental: { + serverComponentsExternalPackages: [ + // ... existing packages ... + 'bunyan', // Add this + ], +}, +webpack: (config, { isServer }) => { + if (isServer) { + config.externals = config.externals || []; + config.externals.push({ + // ... existing externals ... + bunyan: 'commonjs bunyan', // Add this + }); + } + return config; +}, +``` + +> **Why is this needed in Next.js?** Bunyan has optional native dependencies (`dtrace-provider`, `source-map-support`) that webpack cannot resolve. Next.js bundles server code with webpack, so these modules must be externalized. In standard Node.js apps (Express, Fastify, etc.), this is not an issue because there is no webpack bundling step. + +### Step 3: Enable bunyan instrumentation + +Update your `instrumentation.js` to enable bunyan log collection: + +```javascript +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +export function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + instrumentationOptions: { + bunyan: { enabled: true } + } + }); + } +} +``` + +### Step 4: Use in API routes (Next.js-specific) + +In Next.js, mark API routes that use bunyan with `export const runtime = 'nodejs'` to ensure they run on the Node.js runtime (as opposed to Next.js's Edge runtime). This is a Next.js concept and does not apply to standard Node.js apps: + +```javascript +import { NextResponse } from 'next/server'; +import bunyan from 'bunyan'; + +export const runtime = 'nodejs'; + +const logger = bunyan.createLogger({ name: 'my-nextjs-app' }); + +export async function GET(request) { + logger.info({ action: 'fetch-data' }, 'Handling request'); + logger.warn({ reason: 'slow-query' }, 'Query took longer than expected'); + logger.error({ err: new Error('Something failed') }, 'Operation failed'); + + return NextResponse.json({ success: true }); +} +``` + +Bunyan logs will be collected by the OpenTelemetry bunyan instrumentation and exported to Application Insights as traces with proper severity mapping. + +> **Winston**: The same Next.js-specific pattern applies — add `winston` to webpack externals and enable `winston: { enabled: true }` in `instrumentationOptions`. In standard Node.js apps, just enable the instrumentation option — no externalization needed. + +## What Gets Tracked Automatically + +✅ **Server-Side Requests**: All API routes and Server Component renders +✅ **Dependencies**: Outgoing HTTP calls via `fetch()` +✅ **Exceptions**: Unhandled errors in API routes and Server Components +✅ **Performance**: Response times and request counts +✅ **Database Calls**: Queries through supported ORMs (Prisma, etc.) + +> **Note**: Client-side rendering and navigation are NOT tracked by server-side telemetry. Use Application Insights JavaScript SDK for client-side monitoring. + +## Verify It Works + +1. Start your development server: + ```bash + npm run dev + ``` + +2. Make some requests: + ```bash + curl http://localhost:3000/api/users + ``` + Or navigate to pages in your browser. + +3. Check Azure Portal: + - Navigate to your Application Insights resource + - Go to "Transaction search" or "Live Metrics" + - You should see requests appearing within 1-2 minutes + +## Complete package.json Example + +```json +{ + "name": "nextjs-azure-monitor-demo", + "version": "1.0.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@azure/monitor-opentelemetry": "^1.0.0", + "next": "^14.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } +} +``` + +## Project Structure + +``` +my-nextjs-app/ +├── app/ +│ ├── api/ +│ │ └── users/ +│ │ └── route.ts +│ ├── layout.tsx +│ └── page.tsx +├── instrumentation.js ← Azure Monitor setup +├── next.config.js ← Enable instrumentationHook +├── .env.local ← Connection string +└── package.json +``` + +## Troubleshooting + +**Module not found: Can't resolve 'fs' / 'stream' / 'net' / 'tls'?** +- This is the most common issue. Next.js tries to bundle server-only Node.js modules used by `@grpc/grpc-js` and OpenTelemetry packages. +- **Fix**: Add `serverComponentsExternalPackages` AND `webpack.externals` to `next.config.js` as shown in Step 3. +- Both configurations are required — `serverComponentsExternalPackages` alone is not sufficient for the instrumentation hook. +- Ensure `@opentelemetry/instrumentation`, `@opentelemetry/sdk-node`, and `@grpc/grpc-js` are all included in `webpack.externals`. + +**Module not found: Can't resolve 'source-map-support' or './src/build' (dtrace-provider)?** +- This occurs when using Bunyan in Next.js. Bunyan has optional native dependencies that webpack cannot resolve. +- **Fix**: Add `bunyan` to both `serverComponentsExternalPackages` and `webpack.externals` as shown in the [Using Logging Libraries](#using-logging-libraries-bunyan-winston) section. + +**Module not found: Can't resolve '@azure/functions-core'?** +- This is a harmless warning. `@azure/functions-core` is an optional dependency used only in Azure Functions environments. +- It can be safely ignored for standalone Next.js deployments. + +**No telemetry appearing?** +- Verify `instrumentationHook: true` is in `next.config.js` +- Check that `instrumentation.js` is in the project root (not in `/app` or `/src`) +- Ensure connection string is correct +- Wait 2-3 minutes for initial data to appear + +**Edge Runtime not supported?** +- Azure Monitor OpenTelemetry only works with Node.js runtime +- Ensure your API routes are not using Edge runtime +- The `NEXT_RUNTIME` check prevents errors in Edge environments + +**Development vs Production?** +- Telemetry works in both `npm run dev` and `npm run start` +- In development, you may see more verbose logging + +## Next Steps + +- Add client-side monitoring with Application Insights JavaScript SDK +- Configure custom dimensions and metrics +- Set up alerts and dashboards in Azure Portal +- Enable distributed tracing across microservices diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/postgres-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/postgres-setup.md new file mode 100644 index 0000000000..00aae1be1e --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/postgres-setup.md @@ -0,0 +1,147 @@ +# Basic Azure Monitor Setup for Node.js with PostgreSQL + +This guide shows how to add Azure Monitor OpenTelemetry to a Node.js application using PostgreSQL. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application with PostgreSQL (`pg` package) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor - PostgreSQL queries will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now load your application code +const express = require('express'); +const { Pool } = require('pg'); + +const app = express(); +const port = process.env.PORT || 3000; + +// PostgreSQL connection pool +const pool = new Pool({ + connectionString: process.env.DATABASE_URL +}); + +app.use(express.json()); + +app.get('/api/users', async (req, res) => { + // This query will be automatically tracked as a dependency + const result = await pool.query('SELECT * FROM users LIMIT 10'); + res.json(result.rows); +}); + +app.listen(port, () => { + console.log(`Server listening on port ${port}`); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +DATABASE_URL=postgresql://user:password@localhost:5432/mydb +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## What Gets Instrumented Automatically + +With Azure Monitor OpenTelemetry, the following PostgreSQL operations are automatically tracked: + +- **Queries**: All SQL queries executed via `pg` client +- **Query duration**: Time taken for each database operation +- **Query results**: Success/failure status +- **Connection info**: Database name and server details + +## Step 4: Add Custom Telemetry (Optional) + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/users', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-user', async (span) => { + try { + span.setAttribute('user.email', req.body.email); + + const result = await pool.query( + 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *', + [req.body.name, req.body.email] + ); + + span.setAttribute('user.id', result.rows[0].id); + res.status(201).json(result.rows[0]); + } catch (error) { + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Database error' }); + } finally { + span.end(); + } + }); +}); +``` + +## Viewing Telemetry in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Application Map" to see PostgreSQL as a dependency +3. Use "Transaction search" to find specific database operations +4. Check "Dependencies" under "Investigate" to see query performance + +## Troubleshooting + +### PostgreSQL queries not appearing + +1. Ensure `useAzureMonitor()` is called **before** importing `pg` +2. Verify the connection string is set correctly +3. Check that queries are being executed (not just connections) + +### High latency in telemetry + +PostgreSQL instrumentation captures all queries. For high-throughput applications, consider: + +```javascript +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + samplingRatio: 0.5 // Sample 50% of requests +}); +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/redis-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/redis-setup.md new file mode 100644 index 0000000000..0393cc3e9e --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/redis-setup.md @@ -0,0 +1,198 @@ +# Basic Azure Monitor Setup for Node.js with Redis + +This guide shows how to add Azure Monitor OpenTelemetry to a Node.js application using Redis. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application with Redis (`redis` or `ioredis` package) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor - Redis operations will be automatically instrumented +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +// Now load your application code +const express = require('express'); +const { createClient } = require('redis'); + +const app = express(); +const port = process.env.PORT || 3000; + +// Redis client +const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; +const redisClient = createClient({ url: redisUrl }); + +async function connectToRedis() { + redisClient.on('error', err => console.error('Redis Client Error:', err)); + await redisClient.connect(); + console.log('Connected to Redis'); +} + +app.use(express.json()); + +app.get('/api/cache/:key', async (req, res) => { + // This operation will be automatically tracked as a dependency + const value = await redisClient.get(req.params.key); + if (value === null) { + return res.status(404).json({ error: 'Key not found' }); + } + res.json({ key: req.params.key, value: JSON.parse(value) }); +}); + +app.post('/api/cache', async (req, res) => { + const { key, value, ttl } = req.body; + const options = ttl ? { EX: ttl } : {}; + await redisClient.set(key, JSON.stringify(value), options); + res.status(201).json({ key, value }); +}); + +connectToRedis().then(() => { + app.listen(port, () => { + console.log(`Server listening on port ${port}`); + }); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +REDIS_URL=redis://localhost:6379 +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## What Gets Instrumented Automatically + +With Azure Monitor OpenTelemetry, the following Redis operations are automatically tracked: + +- **Commands**: GET, SET, DEL, HGET, HSET, LPUSH, etc. +- **Command duration**: Time taken for each operation +- **Database index**: Which Redis database was used +- **Success/failure**: Operation status + +## Using with ioredis + +If you're using `ioredis`, the setup is the same: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); + +const Redis = require('ioredis'); +const express = require('express'); + +const redis = new Redis(process.env.REDIS_URL); + +const app = express(); + +app.get('/api/cache/:key', async (req, res) => { + const value = await redis.get(req.params.key); + res.json({ key: req.params.key, value }); +}); +``` + +## Step 4: Add Custom Telemetry (Optional) + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/session', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-session', async (span) => { + try { + const sessionId = `session:${Date.now()}`; + span.setAttribute('session.id', sessionId); + + await redisClient.set(sessionId, JSON.stringify(req.body), { EX: 3600 }); + + res.status(201).json({ sessionId }); + } catch (error) { + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Cache error' }); + } finally { + span.end(); + } + }); +}); +``` + +## Using with Azure Cache for Redis + +For Azure Cache for Redis, update your connection string: + +```env +REDIS_URL=rediss://:YOUR_ACCESS_KEY@YOUR_REDIS_NAME.redis.cache.windows.net:6380 +``` + +Note the `rediss://` protocol for SSL connections (required by Azure Cache for Redis). + +## Viewing Telemetry in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Application Map" to see Redis as a dependency +3. Use "Transaction search" to find specific cache operations +4. Check "Dependencies" under "Investigate" to see operation performance + +## Troubleshooting + +### Redis operations not appearing + +1. Ensure `useAzureMonitor()` is called **before** importing `redis` or `ioredis` +2. Verify the connection string is set correctly +3. Check that operations are being executed (not just connections) + +### High volume of telemetry + +Redis is often used for high-frequency operations. Consider sampling: + +```javascript +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + samplingRatio: 0.1 // Sample 10% of requests +}); +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/winston-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/winston-setup.md new file mode 100644 index 0000000000..0c5f8c0019 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/nodejs/winston-setup.md @@ -0,0 +1,260 @@ +# Basic Azure Monitor Setup for Node.js with Winston Logging + +This guide shows how to add Azure Monitor OpenTelemetry to a Node.js application using Winston for logging. + +## Prerequisites + +- Node.js 14.x or higher +- npm or yarn +- Node.js application with Winston (`winston` package) +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +npm install @azure/monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Create or update your main entry point (typically `index.js` or `server.js`): + +```javascript +// IMPORTANT: This must be the first line, before any other imports +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Initialize Azure Monitor with Winston log collection enabled +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + instrumentationOptions: { + winston: { enabled: true } + } +}); + +// Now load your application code +const express = require('express'); +const winston = require('winston'); + +// Create Winston logger +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { service: 'my-app' }, + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + }) + ] +}); + +const app = express(); +const port = process.env.PORT || 3000; + +app.use(express.json()); + +// Request logging middleware +app.use((req, res, next) => { + logger.info('Incoming request', { + method: req.method, + path: req.path + }); + next(); +}); + +app.get('/api/users', (req, res) => { + logger.info('Fetching users'); + res.json([{ id: 1, name: 'Alice' }]); +}); + +app.listen(port, () => { + logger.info(`Server listening on port ${port}`); +}); +``` + +## Step 3: Configure Connection String + +Create a `.env` file in your project root: + +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +LOG_LEVEL=info +PORT=3000 +``` + +Install `dotenv` to load environment variables: + +```bash +npm install dotenv +``` + +Load it at the very top of your entry file: + +```javascript +require('dotenv').config(); +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); +// ... rest of code +``` + +## What Gets Collected + +With Winston instrumentation enabled, the following log data is sent to Azure Monitor: + +- **Log level**: error, warn, info, debug, etc. +- **Log message**: The log content +- **Metadata**: Any additional properties passed to the logger +- **Timestamp**: When the log was created +- **Trace context**: Correlation with distributed traces + +## Log Level Mapping + +Winston log levels are mapped to Application Insights severity levels: + +| Winston Level | Application Insights Severity | +|--------------|------------------------------| +| error | Error | +| warn | Warning | +| info | Information | +| http | Information | +| verbose | Verbose | +| debug | Verbose | +| silly | Verbose | + +## Step 4: Structured Logging Best Practices + +### Add Context to Logs + +```javascript +// Good: Structured logging with context +logger.info('User created', { + userId: user.id, + email: user.email, + action: 'create' +}); + +// Good: Error logging with stack trace +logger.error('Failed to create user', { + error: err.message, + stack: err.stack, + email: req.body.email +}); +``` + +### Correlation with Requests + +Logs are automatically correlated with HTTP requests when using Express: + +```javascript +app.get('/api/users/:id', (req, res) => { + // This log will be correlated with the request trace + logger.info('Fetching user', { userId: req.params.id }); + + try { + const user = getUserById(req.params.id); + logger.debug('User found', { user }); + res.json(user); + } catch (error) { + logger.error('User not found', { + userId: req.params.id, + error: error.message + }); + res.status(404).json({ error: 'User not found' }); + } +}); +``` + +## Step 5: Custom Telemetry (Optional) + +Combine logging with custom spans: + +```javascript +const { trace } = require('@opentelemetry/api'); + +app.post('/api/orders', async (req, res) => { + const tracer = trace.getTracer('my-app'); + + await tracer.startActiveSpan('create-order', async (span) => { + logger.info('Creating order', { items: req.body.items.length }); + + try { + const order = await createOrder(req.body); + + logger.info('Order created successfully', { + orderId: order.id, + total: order.total + }); + + span.setAttribute('order.id', order.id); + res.status(201).json(order); + } catch (error) { + logger.error('Failed to create order', { + error: error.message, + stack: error.stack + }); + + span.recordException(error); + span.setStatus({ code: 2, message: error.message }); + res.status(500).json({ error: 'Failed to create order' }); + } finally { + span.end(); + } + }); +}); +``` + +## Viewing Logs in Azure Portal + +1. Open your Application Insights resource in Azure Portal +2. Navigate to "Logs" under Monitoring +3. Query traces table: + +```kusto +traces +| where timestamp > ago(1h) +| where customDimensions.service == "my-app" +| project timestamp, message, severityLevel, customDimensions +| order by timestamp desc +``` + +4. Use "Transaction search" to see logs correlated with requests + +## Troubleshooting + +### Winston logs not appearing + +1. Ensure `winston` instrumentation is enabled in options +2. Verify `useAzureMonitor()` is called **before** importing `winston` +3. Check that the connection string is valid + +### Too many logs being sent + +Control log volume by adjusting the log level: + +```javascript +const logger = winston.createLogger({ + level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug', + // ... +}); +``` + +Or use sampling: + +```javascript +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + }, + instrumentationOptions: { + winston: { enabled: true } + }, + samplingRatio: 0.5 // Sample 50% of telemetry +}); +``` diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/console-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/console-setup.md new file mode 100644 index 0000000000..dc082f1cb5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/console-setup.md @@ -0,0 +1,392 @@ +# Basic Azure Monitor Setup for Python Console/Script Applications + +This guide shows how to add Azure Monitor OpenTelemetry to standalone Python scripts, console applications, or background workers. + +## Prerequisites + +- Python 3.8 or higher +- Python script or console application +- Azure Application Insights resource + +## Step 1: Install Packages + +```bash +pip install azure-monitor-opentelemetry>=1.8.3 +``` + +Or add to your `requirements.txt`: +``` +azure-monitor-opentelemetry>=1.8.3 +``` + +## Step 2: Initialize at Startup + +Update your main script file (e.g., `app.py`, `main.py`): + +```python +# IMPORTANT: Configure Azure Monitor at the very top +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import your other libraries +import time +import logging + +# Your application code +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def main(): + logger.info("Application started") + # Your logic here + time.sleep(1) + logger.info("Application completed") + +if __name__ == "__main__": + main() +``` + +## Step 3: Configure Connection String + +Create a `.env` file: +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +``` + +Load environment variables: +```python +from dotenv import load_dotenv +load_dotenv() + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# ... rest of your app +``` + +## What Gets Auto-Instrumented + +The Azure Monitor Distro automatically captures: +- ✅ Basic Python logging (via logging module) +- ✅ Exceptions and error stack traces +- ✅ Application lifecycle events + +**Note**: Unlike web frameworks, console apps don't have automatic HTTP request tracing. You need to add library-specific instrumentations. + +## ⚠️ Important: Instrumentation Order + +**CRITICAL**: The correct initialization order is: +1. Import standard libraries and your dependencies (requests, httpx, etc.) **FIRST** +2. Configure logging with `logging.basicConfig()` **SECOND** +3. Import and call `configure_azure_monitor()` **THIRD** +4. Get logger with `logging.getLogger(__name__)` **FOURTH** +5. Import and call instrumentor `.instrument()` methods **LAST** + +**Incorrect Order** (logs won't appear in Application Insights): +```python +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() # ❌ Before logging config +logging.basicConfig(level=logging.INFO) +LoggingInstrumentor().instrument() +``` + +**Correct Order** (logs will appear in Application Insights): +```python +import logging +import requests + +logging.basicConfig(level=logging.INFO) # ✅ 1. Configure logging after regular imports + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() # ✅ 2. Then configure Azure Monitor + +logger = logging.getLogger(__name__) # ✅ 3. Get logger after configure_azure_monitor + +from opentelemetry.instrumentation.logging import LoggingInstrumentor +LoggingInstrumentor().instrument() # ✅ 4. Finally instrument +``` + +This order applies to all instrumentors (HTTPXClientInstrumentor, URLLib3Instrumentor, AioHttpClientInstrumentor, AsyncioInstrumentor, etc.). + +## Adding Library-Specific Instrumentations + +### For HTTP Clients (requests, httpx, urllib3) + +```bash +pip install opentelemetry-instrumentation-requests +pip install opentelemetry-instrumentation-httpx +``` + +```python +import logging +import requests +import httpx + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +logger = logging.getLogger(__name__) + +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor + +RequestsInstrumentor().instrument() +HTTPXClientInstrumentor().instrument() +LoggingInstrumentor().instrument() + +# Now these calls are automatically traced +response = requests.get("https://api.example.com/data") +``` + +### For Database Clients (psycopg2, pymongo, redis) + +```bash +pip install opentelemetry-instrumentation-psycopg2 +pip install opentelemetry-instrumentation-pymongo +pip install opentelemetry-instrumentation-redis +``` + +```python +import logging +import psycopg2 + +logging.basicConfig(level=logging.INFO) + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +logger = logging.getLogger(__name__) + +from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor +Psycopg2Instrumentor().instrument() + +# Database queries are now traced +``` + +### For Async Operations + +```bash +pip install opentelemetry-instrumentation-asyncio +``` + +```python +import logging +import asyncio + +logging.basicConfig(level=logging.INFO) + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +logger = logging.getLogger(__name__) + +from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor +AsyncioInstrumentor().instrument() + +# Async tasks are now traced +``` + +## Adding Custom Tracing + +Use OpenTelemetry APIs to create custom spans for your business logic: + +```python +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +def process_data(data): + with tracer.start_as_current_span("process_data") as span: + span.set_attribute("data.size", len(data)) + + # Your processing logic + result = do_something(data) + + span.set_attribute("result.count", len(result)) + return result + +def do_something(data): + # This automatically becomes a child span + with tracer.start_as_current_span("do_something"): + # Processing logic + return [item * 2 for item in data] +``` + +## Example: Complete Console Application + +```python +"""Example console application with Azure Monitor.""" +from dotenv import load_dotenv +load_dotenv() + +import logging +import requests +import time + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +logger = logging.getLogger(__name__) + +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor +RequestsInstrumentor().instrument() +LoggingInstrumentor().instrument() + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +def fetch_data(url): + """Fetch data from API - automatically traced.""" + logger.info(f"Fetching data from {url}") + response = requests.get(url, timeout=10) + response.raise_for_status() + return response.json() + +def process_batch(items): + """Process items with custom tracing.""" + with tracer.start_as_current_span("process_batch") as span: + span.set_attribute("batch.size", len(items)) + + results = [] + for i, item in enumerate(items): + with tracer.start_as_current_span(f"process_item_{i}"): + time.sleep(0.1) # Simulate processing + results.append(item * 2) + + span.set_attribute("results.count", len(results)) + logger.info(f"Processed {len(results)} items") + return results + +def main(): + """Main application entry point.""" + with tracer.start_as_current_span("main") as span: + logger.info("Application started") + + try: + # Fetch data (HTTP call is auto-traced) + data = fetch_data("https://jsonplaceholder.typicode.com/posts") + span.add_event("data_fetched", {"count": len(data)}) + + # Process data (custom spans) + results = process_batch(data[:10]) + + logger.info(f"Application completed successfully. Processed {len(results)} items") + span.set_status(trace.Status(trace.StatusCode.OK)) + + except Exception as e: + logger.error(f"Application failed: {e}", exc_info=True) + span.record_exception(e) + span.set_status(trace.Status(trace.StatusCode.ERROR, str(e))) + raise + +if __name__ == "__main__": + main() +``` + +## Common Use Cases + +### Batch Processing Scripts +```python +with tracer.start_as_current_span("batch_job") as span: + span.set_attribute("job.type", "daily_report") + process_records() +``` + +### CLI Tools +```python +import click + +@click.command() +@click.option('--input', required=True) +def cli_tool(input): + with tracer.start_as_current_span("cli_execution") as span: + span.set_attribute("cli.input", input) + # Process CLI logic +``` + +### Background Workers +```python +while True: + with tracer.start_as_current_span("worker_iteration"): + process_queue() + time.sleep(60) +``` + +## Available Instrumentations + +| Library | Instrumentation Package | What's Traced | +|---------|------------------------|---------------| +| requests | `opentelemetry-instrumentation-requests` | HTTP requests | +| httpx | `opentelemetry-instrumentation-httpx` | HTTP/2 requests | +| urllib3 | `opentelemetry-instrumentation-urllib3` | Low-level HTTP | +| psycopg2 | `opentelemetry-instrumentation-psycopg2` | PostgreSQL queries | +| pymongo | `opentelemetry-instrumentation-pymongo` | MongoDB operations | +| redis | `opentelemetry-instrumentation-redis` | Redis commands | +| asyncio | `opentelemetry-instrumentation-asyncio` | Async tasks | +| logging | Built-in | Log records | + +## Configuration Options + +### Control Sampling + +Sample only 10% of traces to reduce costs: + +```python +import os +os.environ["OTEL_TRACES_SAMPLER"] = "traceidratio" +os.environ["OTEL_TRACES_SAMPLER_ARG"] = "0.1" + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() +``` + +### Set Service Name + +```python +import os +os.environ["OTEL_SERVICE_NAME"] = "my-batch-processor" + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() +``` + +### Add Resource Attributes + +```python +import os +os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "deployment.environment=production,service.version=1.2.3" + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() +``` + +## Viewing Telemetry + +Once configured, view your console app telemetry in Azure Portal: +1. Go to your Application Insights resource +2. Navigate to **Transaction search** for individual executions +3. Check **Performance** for operation timing +4. Use **Failures** to track exceptions +5. Create custom queries in **Logs** for batch job analytics + +## Next Steps + +- OpenTelemetry Pipeline Concepts(see in opentelemetry-pipeline-python.md) +- Azure Monitor Python Overview(see in azure-monitor-python.md) +- [OpenTelemetry Python Documentation](https://opentelemetry.io/docs/instrumentation/python/) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/django-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/django-setup.md new file mode 100644 index 0000000000..7f8a0d3f34 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/django-setup.md @@ -0,0 +1,269 @@ +# Basic Azure Monitor Setup for Django + +This guide shows how to add Azure Monitor OpenTelemetry to a Django application. + +## Prerequisites + +- Python 3.8 or higher +- Django application +- Azure Application Insights resource + +## Step 1: Install Packages + +```bash +pip install azure-monitor-opentelemetry django +``` + +Or add to your `requirements.txt`: +``` +azure-monitor-opentelemetry +django +``` + +## Step 2: Initialize at Startup + +For Django, you have two options for initialization: + +### Option A: In manage.py (Recommended for Development) + +```python +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + + # Configure Azure Monitor after Django settings are loaded + from azure.monitor.opentelemetry import configure_azure_monitor + configure_azure_monitor() + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed?" + ) from exc + execute_from_command_line(sys.argv) + +if __name__ == '__main__': + main() +``` + +### Option B: In wsgi.py (Recommended for Production) + +```python +""" +WSGI config for mysite project. +""" +import os + +# Configure Azure Monitor BEFORE Django loads +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_wsgi_application() +``` + +### Option C: In asgi.py (For async Django/Channels) + +```python +""" +ASGI config for mysite project. +""" +import os + +# Configure Azure Monitor BEFORE Django loads +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_asgi_application() +``` + +## Step 3: Configure Connection String + +Add to your `.env` file or environment: +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +``` + +Load in `settings.py`: +```python +from dotenv import load_dotenv +load_dotenv() +``` + +## What Gets Auto-Instrumented + +The Azure Monitor Distro automatically captures: +- ✅ All HTTP requests to your Django views +- ✅ Request duration, status codes, and paths +- ✅ Middleware processing time +- ✅ Exceptions and error details +- ✅ Database queries (PostgreSQL via psycopg2) +- ✅ Outbound HTTP calls + +## Step 4: Add Custom Telemetry (Optional) + +### In Views + +```python +# views.py +from django.http import JsonResponse +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +def order_detail(request, order_id): + with tracer.start_as_current_span("fetch-order") as span: + span.set_attribute("order.id", order_id) + + try: + order = Order.objects.get(id=order_id) + span.set_attribute("order.status", order.status) + return JsonResponse({"order": order.to_dict()}) + except Order.DoesNotExist: + span.set_attribute("order.found", False) + return JsonResponse({"error": "Not found"}, status=404) + +def process_order(request): + span = trace.get_active_span() + span.set_attribute("user.id", request.user.id) + + # Your processing logic + return JsonResponse({"status": "processed"}) +``` + +### In Class-Based Views + +```python +from django.views import View +from django.http import JsonResponse +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +class OrderView(View): + def get(self, request, order_id): + with tracer.start_as_current_span("get-order") as span: + span.set_attribute("order.id", order_id) + order = Order.objects.get(id=order_id) + return JsonResponse(order.to_dict()) + + def post(self, request): + with tracer.start_as_current_span("create-order") as span: + # Create order logic + order = Order.objects.create(**request.POST) + span.set_attribute("order.id", order.id) + return JsonResponse({"id": order.id}, status=201) +``` + +## Step 5: Add Logging Integration + +```python +# settings.py +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} +``` + +```python +# views.py +import logging + +logger = logging.getLogger(__name__) + +def my_view(request): + logger.info("Processing request") # Automatically correlated with trace + # ... +``` + +## Complete Example Structure + +``` +mysite/ +├── manage.py # Add configure_azure_monitor() here +├── requirements.txt +├── .env +└── mysite/ + ├── __init__.py + ├── settings.py + ├── urls.py + ├── wsgi.py # Or add configure_azure_monitor() here + └── asgi.py +``` + +### manage.py +```python +#!/usr/bin/env python +import os +import sys +from dotenv import load_dotenv + +load_dotenv() + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + from django.core.management import execute_from_command_line + execute_from_command_line(sys.argv) + +if __name__ == '__main__': + main() +``` + +## Running the Application + +```bash +# Set connection string +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=..." + +# Run Django +python manage.py runserver +``` + +Or for production with Gunicorn: +```bash +gunicorn mysite.wsgi:application +``` + +## Verification + +1. Make requests to your Django endpoints +2. Go to Azure Portal → Application Insights +3. Check "Transaction search" for your requests +4. View database dependency calls + +## Links + +- [Django Documentation](https://docs.djangoproject.com/) +- [Azure Monitor Python](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/fastapi-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/fastapi-setup.md new file mode 100644 index 0000000000..98085ff2e1 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/fastapi-setup.md @@ -0,0 +1,256 @@ +# Basic Azure Monitor Setup for FastAPI + +This guide shows how to add Azure Monitor OpenTelemetry to a FastAPI application. + +## Prerequisites + +- Python 3.8 or higher +- FastAPI application +- Azure Application Insights resource + +## Step 1: Install Packages + +```bash +pip install azure-monitor-opentelemetry fastapi uvicorn +``` + +Or add to your `requirements.txt`: +``` +azure-monitor-opentelemetry +fastapi +uvicorn[standard] +``` + +## Step 2: Initialize at Startup + +Update your main application file (e.g., `main.py` or `app.py`): + +```python +# IMPORTANT: Configure Azure Monitor BEFORE importing FastAPI +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import FastAPI +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +app = FastAPI(title="My API") + +@app.get("/") +async def root(): + return {"message": "Hello, World!"} + +@app.get("/api/users/{user_id}") +async def get_user(user_id: int): + # This request is automatically tracked + return {"id": user_id, "name": f"User {user_id}"} +``` + +## Step 3: Configure Connection String + +Create a `.env` file: +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +``` + +Load environment variables: +```python +from dotenv import load_dotenv +load_dotenv() + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from fastapi import FastAPI +# ... rest of your app +``` + +## What Gets Auto-Instrumented + +The Azure Monitor Distro automatically captures: +- ✅ All HTTP requests to your FastAPI routes +- ✅ Request duration, status codes, and paths +- ✅ Path parameters and query strings +- ✅ Exceptions and error details +- ✅ Outbound HTTP calls (httpx, requests, aiohttp) +- ✅ Database queries (asyncpg, psycopg2) + +## Step 4: Add Custom Telemetry (Optional) + +```python +from fastapi import FastAPI, Request +from opentelemetry import trace + +app = FastAPI() +tracer = trace.get_tracer(__name__) + +class Order(BaseModel): + item: str + quantity: int + +@app.post("/api/orders") +async def create_order(order: Order, request: Request): + with tracer.start_as_current_span("create-order") as span: + span.set_attribute("order.item", order.item) + span.set_attribute("order.quantity", order.quantity) + + # Your business logic + order_id = await save_order(order) + + span.set_attribute("order.id", order_id) + return {"id": order_id, "status": "created"} + +@app.get("/api/orders/{order_id}") +async def get_order(order_id: str): + span = trace.get_active_span() + span.set_attribute("order.id", order_id) + + order = await fetch_order(order_id) + if not order: + span.set_attribute("order.found", False) + raise HTTPException(status_code=404, detail="Order not found") + + return order +``` + +## Step 5: Add Middleware for Custom Context + +```python +from fastapi import FastAPI, Request +from opentelemetry import trace + +app = FastAPI() + +@app.middleware("http") +async def add_custom_context(request: Request, call_next): + span = trace.get_active_span() + + # Add custom attributes to every request + if span: + span.set_attribute("http.user_agent", request.headers.get("user-agent", "")) + span.set_attribute("custom.request_id", request.headers.get("x-request-id", "")) + + response = await call_next(request) + return response +``` + +## Step 6: Add Logging Integration + +```python +import logging +from fastapi import FastAPI + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = FastAPI() + +@app.get("/api/process/{item_id}") +async def process_item(item_id: str): + logger.info(f"Processing item {item_id}") # Correlated with trace + + try: + result = await do_processing(item_id) + logger.info(f"Successfully processed item {item_id}") + return result + except Exception as e: + logger.error(f"Failed to process item {item_id}: {e}") + raise +``` + +## Complete Example + +```python +# main.py +import os +import logging +from dotenv import load_dotenv + +# Load environment variables first +load_dotenv() + +# Configure Azure Monitor BEFORE importing FastAPI +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import FastAPI and other dependencies +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel +from opentelemetry import trace + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create FastAPI app +app = FastAPI( + title="My API", + description="API with Azure Monitor integration", + version="1.0.0" +) +tracer = trace.get_tracer(__name__) + +class Item(BaseModel): + name: str + price: float + +@app.get("/") +async def root(): + return {"status": "healthy"} + +@app.get("/api/items") +async def list_items(): + with tracer.start_as_current_span("list-items") as span: + items = [ + {"id": 1, "name": "Item 1", "price": 10.0}, + {"id": 2, "name": "Item 2", "price": 20.0} + ] + span.set_attribute("items.count", len(items)) + logger.info(f"Returning {len(items)} items") + return items + +@app.post("/api/items") +async def create_item(item: Item): + with tracer.start_as_current_span("create-item") as span: + span.set_attribute("item.name", item.name) + span.set_attribute("item.price", item.price) + logger.info(f"Creating item: {item.name}") + return {"id": 1, **item.dict()} + +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception): + logger.error(f"Unhandled error: {exc}") + return {"error": "Internal server error"} +``` + +## Running the Application + +```bash +# Set connection string +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=..." + +# Run with uvicorn +uvicorn main:app --reload +``` + +Or programmatically: +```python +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +## Verification + +1. Make requests to your FastAPI endpoints +2. Use the auto-generated docs at `/docs` to test +3. Go to Azure Portal → Application Insights +4. Check "Transaction search" for your requests +5. View "Live Metrics" for real-time data + +## Links + +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [Azure Monitor Python](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python) +- [OpenTelemetry FastAPI Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/fastapi/fastapi.html) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/flask-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/flask-setup.md new file mode 100644 index 0000000000..918cfa553d --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/flask-setup.md @@ -0,0 +1,218 @@ +# Basic Azure Monitor Setup for Flask + +This guide shows how to add Azure Monitor OpenTelemetry to a Flask application. + +## Prerequisites + +- Python 3.8 or higher +- Flask application +- Azure Application Insights resource + +## Step 1: Install Packages + +```bash +pip install azure-monitor-opentelemetry flask +``` + +Or add to your `requirements.txt`: +``` +azure-monitor-opentelemetry +flask +``` + +## Step 2: Initialize at Startup + +Update your main application file (e.g., `app.py`): + +```python +# IMPORTANT: Configure Azure Monitor BEFORE importing Flask +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import Flask +from flask import Flask, jsonify, request + +app = Flask(__name__) + +@app.route('/') +def hello(): + return jsonify({"message": "Hello, World!"}) + +@app.route('/api/users') +def get_users(): + # This request is automatically tracked + return jsonify([ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"} + ]) + +if __name__ == '__main__': + app.run(debug=True) +``` + +## Step 3: Configure Connection String + +Create a `.env` file: +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +FLASK_ENV=development +``` + +Load environment variables: +```python +from dotenv import load_dotenv +load_dotenv() + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from flask import Flask +# ... rest of your app +``` + +## What Gets Auto-Instrumented + +The Azure Monitor Distro automatically captures: +- ✅ All HTTP requests to your Flask routes +- ✅ Request duration, status codes, and paths +- ✅ Exceptions and error details +- ✅ Outbound HTTP calls (requests, urllib3) +- ✅ Database queries (psycopg2 for PostgreSQL) + +## Step 4: Add Custom Telemetry (Optional) + +```python +from flask import Flask, jsonify +from opentelemetry import trace + +app = Flask(__name__) +tracer = trace.get_tracer(__name__) + +@app.route('/api/orders/') +def get_order(order_id): + # Add custom span for business logic + with tracer.start_as_current_span("fetch-order") as span: + span.set_attribute("order.id", order_id) + + # Simulate database fetch + order = fetch_order_from_db(order_id) + + span.set_attribute("order.status", order.get("status")) + return jsonify(order) + +@app.route('/api/process', methods=['POST']) +def process_data(): + span = trace.get_active_span() + + # Add context to current request span + span.set_attribute("user.id", request.headers.get("X-User-ID")) + span.set_attribute("request.size", len(request.data)) + + try: + result = do_processing(request.json) + return jsonify(result) + except Exception as e: + span.record_exception(e) + return jsonify({"error": str(e)}), 500 +``` + +## Step 5: Add Logging Integration + +```python +import logging +from flask import Flask + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = Flask(__name__) + +@app.route('/api/users/') +def get_user(user_id): + logger.info(f"Fetching user {user_id}") # Correlated with trace + + user = fetch_user(user_id) + + if not user: + logger.warning(f"User {user_id} not found") + return jsonify({"error": "Not found"}), 404 + + logger.info(f"Found user {user_id}") + return jsonify(user) +``` + +## Complete Example + +```python +# app.py +import os +import logging +from dotenv import load_dotenv + +# Load environment variables first +load_dotenv() + +# Configure Azure Monitor BEFORE importing Flask +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import Flask and other dependencies +from flask import Flask, jsonify, request +from opentelemetry import trace + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Create Flask app +app = Flask(__name__) +tracer = trace.get_tracer(__name__) + +@app.route('/') +def index(): + logger.info("Index page accessed") + return jsonify({"status": "healthy"}) + +@app.route('/api/items') +def list_items(): + with tracer.start_as_current_span("list-items") as span: + items = [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}] + span.set_attribute("items.count", len(items)) + return jsonify(items) + +@app.errorhandler(Exception) +def handle_error(error): + logger.error(f"Unhandled error: {error}") + return jsonify({"error": "Internal server error"}), 500 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) +``` + +## Running the Application + +```bash +# Set connection string +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=..." + +# Run Flask +python app.py +``` + +Or with Flask CLI: +```bash +flask run +``` + +## Verification + +1. Make requests to your Flask endpoints +2. Go to Azure Portal → Application Insights +3. Check "Transaction search" for your requests +4. View "Application map" for dependencies + +## Links + +- [Flask Documentation](https://flask.palletsprojects.com/) +- [Azure Monitor Python](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/genai-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/genai-setup.md new file mode 100644 index 0000000000..db55e72179 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/genai-setup.md @@ -0,0 +1,214 @@ +# Basic Azure Monitor Setup for GenAI Applications + +This guide shows how to add Azure Monitor OpenTelemetry to applications using GenAI libraries like OpenAI, LangChain, or Anthropic. + +## Prerequisites + +- Python 3.8 or higher +- GenAI library (openai, langchain, anthropic, etc.) +- Azure Application Insights resource + +## Step 1: Install Packages + +For OpenAI applications: +```bash +pip install azure-monitor-opentelemetry openai opentelemetry-instrumentation-openai +``` + +For LangChain applications: +```bash +pip install azure-monitor-opentelemetry langchain opentelemetry-instrumentation-langchain +``` + +For Anthropic applications: +```bash +pip install azure-monitor-opentelemetry anthropic opentelemetry-instrumentation-anthropic +``` + +Or add to your `requirements.txt`: +``` +azure-monitor-opentelemetry>=1.8.3 +openai +opentelemetry-instrumentation-openai +``` + +## Step 2: Initialize at Startup + +Update your main application file (e.g., `app.py`): + +```python +# IMPORTANT: Configure Azure Monitor BEFORE importing GenAI libraries +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import your GenAI libraries +from openai import OpenAI + +client = OpenAI() + +def generate_completion(prompt: str): + """Generate a completion - automatically traced by Azure Monitor.""" + response = client.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": prompt} + ] + ) + return response.choices[0].message.content + +if __name__ == "__main__": + result = generate_completion("What is the capital of France?") + print(f"Response: {result}") +``` + +## Step 3: Configure Connection String + +Create a `.env` file: +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +OPENAI_API_KEY=sk-... +``` + +Load environment variables: +```python +from dotenv import load_dotenv +load_dotenv() + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from openai import OpenAI +# ... rest of your app +``` + +## What Gets Auto-Instrumented + +The Azure Monitor Distro with GenAI instrumentations automatically captures: +- ✅ All LLM API calls (OpenAI, Anthropic, etc.) +- ✅ Request duration and latency +- ✅ Token usage (prompt tokens, completion tokens, total) +- ✅ Model names and parameters (temperature, max_tokens, etc.) +- ✅ Prompt and completion content (configurable) +- ✅ Error details and exceptions +- ✅ Chain execution in LangChain applications +- ✅ Agent interactions and tool calls + +## Advanced: Custom Tracing + +Add custom spans for business logic: + +```python +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +def process_user_query(user_id: str, query: str): + with tracer.start_as_current_span("process_query") as span: + span.set_attribute("user.id", user_id) + span.set_attribute("query.length", len(query)) + + # Your LLM call is automatically traced as a child span + response = generate_completion(query) + + span.set_attribute("response.length", len(response)) + return response +``` + +## Supported GenAI Libraries + +| Library | Instrumentation Package | What's Traced | +|---------|------------------------|---------------| +| OpenAI | `opentelemetry-instrumentation-openai` | Chat completions, embeddings, fine-tuning | +| Anthropic | `opentelemetry-instrumentation-anthropic` | Messages API, Claude models | +| LangChain | `opentelemetry-instrumentation-langchain` | Chains, agents, tools, retrievers | +| OpenAI Agents | `opentelemetry-instrumentation-openai-agents` | Agent runs, function calls | + +## Example: LangChain Application + +```python +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from langchain.chat_models import ChatOpenAI +from langchain.chains import LLMChain +from langchain.prompts import PromptTemplate + +llm = ChatOpenAI(model="gpt-4") +template = PromptTemplate( + input_variables=["topic"], + template="Tell me a joke about {topic}" +) +chain = LLMChain(llm=llm, prompt=template) + +# This entire chain execution is traced +result = chain.run(topic="programming") +print(result) +``` + +## Example: OpenAI Agents + +```python +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +from openai import OpenAI +from openai_agents import Agent, function_tool + +@function_tool +def get_weather(location: str) -> dict: + """Get weather for a location.""" + return {"location": location, "temperature": 72} + +agent = Agent( + name="Weather Assistant", + instructions="You are a helpful weather assistant.", + tools=[get_weather], + model="gpt-4" +) + +# Agent runs and tool calls are automatically traced +response = agent.run("What's the weather in San Francisco?") +print(response) +``` + +## Configuration Options + +### Disable Content Logging + +To avoid logging prompt/completion content (for privacy): + +```python +import os +os.environ["OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"] = "openai-v2" + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() +``` + +### Control Sampling + +To sample only 10% of traces (reduce costs): + +```python +import os +os.environ["OTEL_TRACES_SAMPLER"] = "traceidratio" +os.environ["OTEL_TRACES_SAMPLER_ARG"] = "0.1" + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() +``` + +## Viewing Telemetry + +Once configured, view your GenAI telemetry in Azure Portal: +1. Go to your Application Insights resource +2. Navigate to **Performance** → **Dependencies** to see LLM calls +3. Check **Transaction search** for individual requests +4. Use **Application Map** to visualize your application topology +5. Create custom dashboards to track token usage and costs + +## Next Steps + +- OpenTelemetry Pipeline Concepts(see in opentelemetry-pipeline-python.md) +- Azure Monitor Python Overview(see in azure-monitor-python.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/generic-setup.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/generic-setup.md new file mode 100644 index 0000000000..4e24dc973d --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/examples/python/generic-setup.md @@ -0,0 +1,164 @@ +# Basic Azure Monitor Setup for Python + +This guide shows how to add Azure Monitor OpenTelemetry to a Python application. + +## Prerequisites + +- Python 3.8 or higher +- pip +- Azure Application Insights resource + +## Step 1: Install Package + +```bash +pip install azure-monitor-opentelemetry +``` + +Or add to your `requirements.txt`: +``` +azure-monitor-opentelemetry +``` + +## Step 2: Initialize at Startup + +Add the following to your application entry point: + +```python +from azure.monitor.opentelemetry import configure_azure_monitor + +# Configure Azure Monitor - MUST be called before importing other libraries +configure_azure_monitor() + +# Now import your application code +# ... +``` + +## Step 3: Configure Connection String + +### Option A: Environment Variable (Recommended) + +```bash +export APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://..." +``` + +### Option B: .env File + +Create a `.env` file: +```env +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://... +``` + +Load it with python-dotenv: +```bash +pip install python-dotenv +``` + +```python +from dotenv import load_dotenv +load_dotenv() + +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() +``` + +### Option C: Direct Configuration + +```python +from azure.monitor.opentelemetry import configure_azure_monitor + +configure_azure_monitor( + connection_string="InstrumentationKey=..." +) +``` + +## Step 4: Optional Configuration + +### Service Name +```bash +export OTEL_SERVICE_NAME="my-python-app" +``` + +### Resource Attributes +```bash +export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0" +``` + +### Sampling +```bash +export OTEL_TRACES_SAMPLER=traceidratio +export OTEL_TRACES_SAMPLER_ARG=0.1 # 10% sampling +``` + +## Step 5: Add Custom Telemetry (Optional) + +```python +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +def process_order(order_id: str): + with tracer.start_as_current_span("process-order") as span: + span.set_attribute("order.id", order_id) + + try: + # Your business logic + result = do_processing(order_id) + span.set_attribute("order.status", "completed") + return result + except Exception as e: + span.record_exception(e) + span.set_attribute("order.status", "failed") + raise +``` + +## Verification + +After setup, you should see telemetry in Azure Portal: +1. Navigate to your Application Insights resource +2. Go to "Transaction search" or "Live Metrics" +3. Make some requests to your application +4. Verify data appears within a few minutes + +## Complete Example + +```python +# app.py +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Configure Azure Monitor FIRST +from azure.monitor.opentelemetry import configure_azure_monitor +configure_azure_monitor() + +# Now import and run your application +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def main(): + logger.info("Application started") + # Your application logic here + logger.info("Application finished") + +if __name__ == "__main__": + main() +``` + +## Troubleshooting + +### No Data in Azure Portal +1. Verify connection string is correct +2. Check for firewall/proxy blocking outbound HTTPS +3. Enable debug logging: `export OTEL_LOG_LEVEL=debug` + +### Import Order Issues +Ensure `configure_azure_monitor()` is called before importing instrumented libraries. + +## Links + +- [Azure Monitor OpenTelemetry](https://learn.microsoft.com/azure/azure-monitor/app/opentelemetry-enable?tabs=python) +- [Troubleshooting Guide](https://learn.microsoft.com/troubleshoot/azure/azure-monitor/app-insights/telemetry/opentelemetry-troubleshooting-python) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnet-classic-greenfield.json b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnet-classic-greenfield.json index d4804efe62..42c20959a5 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnet-classic-greenfield.json +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnet-classic-greenfield.json @@ -5,7 +5,7 @@ "decision": { "intent": "onboard", "solution": "applicationinsights-web", - "rationale": "Classic ASP.NET greenfield application (.NET Framework). Microsoft.ApplicationInsights.Web v3.x provides automatic instrumentation with zero code changes — the NuGet package auto-configures HTTP modules and telemetry pipeline." + "rationale": "Classic ASP.NET greenfield application (.NET Framework). Requires Visual Studio — the NuGet Package Manager Console runs Install-Package which auto-configures ApplicationInsights.config, Web.config HTTP modules, and assembly references." }, "actions": [ { @@ -21,35 +21,27 @@ { "id": "add-appinsights-package", "type": "add-package", - "title": "Add Application Insights Web SDK package", + "title": "Install Application Insights Web SDK via Package Manager Console", "dependsOn": "review-education", - "packageManager": "nuget-pm", + "packageManager": "nuget-vs", "packageName": "Microsoft.ApplicationInsights.Web", "version": "latest-stable" }, { - "id": "validate-install", - "type": "validate-install", - "title": "Verify package auto-configured Web.config and created ApplicationInsights.config", + "id": "verify-install", + "type": "manual-step", + "title": "Verify ApplicationInsights.config and Web.config were configured", "dependsOn": "add-appinsights-package", - "filesToExist": [ - "ApplicationInsights.config" - ], - "fileContentChecks": { - "Web.config": [ - "ApplicationInsightsHttpModule", - "TelemetryHttpModule" - ] - } + "instructions": "After Install-Package completes, verify:\n1. ApplicationInsights.config was created in the project root\n2. Web.config contains ApplicationInsightsWebTracking and TelemetryHttpModule in \n3. Project references were added for Microsoft.ApplicationInsights and Microsoft.AI.Web assemblies" }, { "id": "add-connection-string", "type": "add-config", - "title": "Configure Application Insights connection string", - "dependsOn": "validate-install", + "title": "Set Application Insights connection string", + "dependsOn": "verify-install", "targetFile": "ApplicationInsights.config", "configKey": "ConnectionString", - "configValue": "", + "configValue": "YOUR_CONNECTION_STRING_HERE", "configFormat": "xml", "validationKey": "APPLICATIONINSIGHTS_CONNECTION_STRING" } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnetcore-greenfield.json b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnetcore-greenfield.json index 73597b6307..77ee57ac7a 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnetcore-greenfield.json +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/aspnetcore-greenfield.json @@ -4,8 +4,8 @@ "appType": "aspnetcore", "decision": { "intent": "onboard", - "solution": "azure-monitor-distro", - "rationale": "ASP.NET Core greenfield application. UseAzureMonitor() provides automatic instrumentation for traces, metrics, and logs." + "solution": "appinsights-3x", + "rationale": "ASP.NET Core greenfield application. AddApplicationInsightsTelemetry() provides automatic instrumentation for requests, dependencies, exceptions, logs, and metrics." }, "actions": [ { @@ -14,36 +14,35 @@ "title": "Review educational materials before implementation", "resources": [ "learn://concepts/dotnet/opentelemetry-pipeline.md", - "learn://concepts/dotnet/azure-monitor-distro.md", - "learn://api-reference/dotnet/UseAzureMonitor.md", - "learn://api-reference/dotnet/AddOpenTelemetry.md", + "learn://concepts/dotnet/appinsights-aspnetcore.md", + "learn://api-reference/dotnet/AddApplicationInsightsTelemetry.md", "learn://examples/dotnet/aspnetcore-setup.md" ] }, { - "id": "add-distro-package", + "id": "add-appinsights-package", "type": "add-package", - "title": "Add Azure Monitor OpenTelemetry distro package", + "title": "Add Application Insights ASP.NET Core package", "dependsOn": "review-education", "packageManager": "nuget", - "packageName": "Azure.Monitor.OpenTelemetry.AspNetCore", + "packageName": "Microsoft.ApplicationInsights.AspNetCore", "version": "latest-stable" }, { - "id": "configure-opentelemetry", + "id": "configure-appinsights", "type": "modify-code", - "title": "Add OpenTelemetry with Azure Monitor to service configuration", - "dependsOn": "add-distro-package", + "title": "Add Application Insights telemetry to service configuration", + "dependsOn": "add-appinsights-package", "codeTemplate": "templates/aspnetcore/instrumentation-code.cs", "insertionHint": "WebApplication.CreateBuilder", - "requiredImport": "Azure.Monitor.OpenTelemetry.AspNetCore" + "requiredImport": "Microsoft.Extensions.DependencyInjection" }, { "id": "add-connection-string", "type": "add-config", - "title": "Configure Azure Monitor connection string", + "title": "Configure Application Insights connection string", "targetFile": "appsettings.json", - "configKey": "AzureMonitor.ConnectionString", + "configKey": "ApplicationInsights:ConnectionString", "configValue": "", "validationKey": "APPLICATIONINSIGHTS_CONNECTION_STRING" } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/aspnetcore/instrumentation-code.cs b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/aspnetcore/instrumentation-code.cs index f2c92929ae..84f393e722 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/aspnetcore/instrumentation-code.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/aspnetcore/instrumentation-code.cs @@ -1 +1 @@ -builder.Services.AddOpenTelemetry().UseAzureMonitor(); +builder.Services.AddApplicationInsightsTelemetry(); diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/express/instrumentation-code.js b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/express/instrumentation-code.js new file mode 100644 index 0000000000..c4b99f58c8 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/generator-configs/templates/express/instrumentation-code.js @@ -0,0 +1,8 @@ +const { useAzureMonitor } = require('@azure/monitor-opentelemetry'); + +// Enable Azure Monitor integration +useAzureMonitor({ + azureMonitorExporterOptions: { + connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING + } +}); \ No newline at end of file diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/instrumentations/python-instrumentations.json b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/instrumentations/python-instrumentations.json new file mode 100644 index 0000000000..e48c51e449 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/instrumentations/python-instrumentations.json @@ -0,0 +1,441 @@ +{ + "$schema": "./python-instrumentations.schema.json", + "metadata": { + "lastUpdated": "2026-01-15", + "source": "https://github.com/open-telemetry/opentelemetry-python-contrib", + "distroSource": "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/monitor/azure-monitor-opentelemetry" + }, + "azureMonitorPackages": [ + "azure-monitor-opentelemetry", + "azure-monitor-opentelemetry-exporter" + ], + "openTelemetryCorePackages": [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation" + ], + "applicationInsightsPackages": [ + "applicationinsights", + "opencensus-ext-azure" + ], + "distroInstrumentations": [ + "django", + "flask", + "fastapi", + "requests", + "urllib", + "urllib3", + "psycopg2", + "psycopg2-binary", + "azure-core" + ], + "instrumentations": { + "framework": [ + { + "libraryName": "django", + "displayName": "Django", + "moduleName": "django", + "instrumentationPackage": "opentelemetry-instrumentation-django", + "inDistro": true + }, + { + "libraryName": "flask", + "displayName": "Flask", + "moduleName": "flask", + "instrumentationPackage": "opentelemetry-instrumentation-flask", + "inDistro": true + }, + { + "libraryName": "fastapi", + "displayName": "FastAPI", + "moduleName": "fastapi", + "instrumentationPackage": "opentelemetry-instrumentation-fastapi", + "inDistro": true + }, + { + "libraryName": "falcon", + "displayName": "Falcon", + "moduleName": "falcon", + "instrumentationPackage": "opentelemetry-instrumentation-falcon", + "inDistro": false + }, + { + "libraryName": "pyramid", + "displayName": "Pyramid", + "moduleName": "pyramid", + "instrumentationPackage": "opentelemetry-instrumentation-pyramid", + "inDistro": false + }, + { + "libraryName": "starlette", + "displayName": "Starlette", + "moduleName": "starlette", + "instrumentationPackage": "opentelemetry-instrumentation-starlette", + "inDistro": false + }, + { + "libraryName": "tornado", + "displayName": "Tornado", + "moduleName": "tornado", + "instrumentationPackage": "opentelemetry-instrumentation-tornado", + "inDistro": false + } + ], + "http": [ + { + "libraryName": "requests", + "displayName": "Requests", + "moduleName": "requests", + "instrumentationPackage": "opentelemetry-instrumentation-requests", + "inDistro": true + }, + { + "libraryName": "urllib", + "displayName": "URLLib", + "moduleName": "urllib", + "instrumentationPackage": "opentelemetry-instrumentation-urllib", + "inDistro": true + }, + { + "libraryName": "urllib3", + "displayName": "URLLib3", + "moduleName": "urllib3", + "instrumentationPackage": "opentelemetry-instrumentation-urllib3", + "inDistro": true + }, + { + "libraryName": "httpx", + "displayName": "HTTPX", + "moduleName": "httpx", + "instrumentationPackage": "opentelemetry-instrumentation-httpx", + "inDistro": false + }, + { + "libraryName": "aiohttp", + "displayName": "aiohttp", + "moduleName": "aiohttp", + "instrumentationPackage": "opentelemetry-instrumentation-aiohttp-client", + "inDistro": false + }, + { + "libraryName": "grpcio", + "displayName": "gRPC", + "moduleName": "grpcio", + "instrumentationPackage": "opentelemetry-instrumentation-grpc", + "inDistro": false + }, + { + "libraryName": "grpcio-tools", + "displayName": "gRPC Tools", + "moduleName": "grpcio_tools", + "instrumentationPackage": "opentelemetry-instrumentation-grpc", + "inDistro": false + } + ], + "database": [ + { + "libraryName": "psycopg2", + "displayName": "Psycopg2", + "moduleName": "psycopg2", + "instrumentationPackage": "opentelemetry-instrumentation-psycopg2", + "inDistro": true + }, + { + "libraryName": "psycopg2-binary", + "displayName": "Psycopg2 Binary", + "moduleName": "psycopg2", + "instrumentationPackage": "opentelemetry-instrumentation-psycopg2", + "inDistro": true + }, + { + "libraryName": "psycopg", + "displayName": "Psycopg", + "moduleName": "psycopg", + "instrumentationPackage": "opentelemetry-instrumentation-psycopg", + "inDistro": false + }, + { + "libraryName": "sqlalchemy", + "displayName": "SQLAlchemy", + "moduleName": "sqlalchemy", + "instrumentationPackage": "opentelemetry-instrumentation-sqlalchemy", + "inDistro": false + }, + { + "libraryName": "pymongo", + "displayName": "PyMongo", + "moduleName": "pymongo", + "instrumentationPackage": "opentelemetry-instrumentation-pymongo", + "inDistro": false + }, + { + "libraryName": "redis", + "displayName": "Redis", + "moduleName": "redis", + "instrumentationPackage": "opentelemetry-instrumentation-redis", + "inDistro": false + }, + { + "libraryName": "pymysql", + "displayName": "PyMySQL", + "moduleName": "pymysql", + "instrumentationPackage": "opentelemetry-instrumentation-pymysql", + "inDistro": false + }, + { + "libraryName": "mysqlclient", + "displayName": "MySQLClient", + "moduleName": "mysqlclient", + "instrumentationPackage": "opentelemetry-instrumentation-mysql", + "inDistro": false + }, + { + "libraryName": "mysql-connector-python", + "displayName": "MySQL Connector", + "moduleName": "mysql_connector_python", + "instrumentationPackage": "opentelemetry-instrumentation-mysql", + "inDistro": false + }, + { + "libraryName": "asyncpg", + "displayName": "asyncpg", + "moduleName": "asyncpg", + "instrumentationPackage": "opentelemetry-instrumentation-asyncpg", + "inDistro": false + }, + { + "libraryName": "aiopg", + "displayName": "aiopg", + "moduleName": "aiopg", + "instrumentationPackage": "opentelemetry-instrumentation-aiopg", + "inDistro": false + }, + { + "libraryName": "elasticsearch", + "displayName": "Elasticsearch", + "moduleName": "elasticsearch", + "instrumentationPackage": "opentelemetry-instrumentation-elasticsearch", + "inDistro": false + }, + { + "libraryName": "cassandra-driver", + "displayName": "Cassandra", + "moduleName": "cassandra", + "instrumentationPackage": "opentelemetry-instrumentation-cassandra", + "inDistro": false + }, + { + "libraryName": "pymemcache", + "displayName": "pymemcache", + "moduleName": "pymemcache", + "instrumentationPackage": "opentelemetry-instrumentation-pymemcache", + "inDistro": false + }, + { + "libraryName": "pymssql", + "displayName": "pymssql", + "moduleName": "pymssql", + "instrumentationPackage": "opentelemetry-instrumentation-pymssql", + "inDistro": false + }, + { + "libraryName": "sqlite3", + "displayName": "SQLite3", + "moduleName": "sqlite3", + "instrumentationPackage": "opentelemetry-instrumentation-sqlite3", + "inDistro": false + }, + { + "libraryName": "tortoise-orm", + "displayName": "Tortoise ORM", + "moduleName": "tortoise", + "instrumentationPackage": "opentelemetry-instrumentation-tortoiseorm", + "inDistro": false + } + ], + "messaging": [ + { + "libraryName": "celery", + "displayName": "Celery", + "moduleName": "celery", + "instrumentationPackage": "opentelemetry-instrumentation-celery", + "inDistro": false + }, + { + "libraryName": "kafka-python", + "displayName": "Kafka Python", + "moduleName": "kafka", + "instrumentationPackage": "opentelemetry-instrumentation-kafka-python", + "inDistro": false + }, + { + "libraryName": "confluent-kafka", + "displayName": "Confluent Kafka", + "moduleName": "confluent_kafka", + "instrumentationPackage": "opentelemetry-instrumentation-confluent-kafka", + "inDistro": false + }, + { + "libraryName": "pika", + "displayName": "Pika", + "moduleName": "pika", + "instrumentationPackage": "opentelemetry-instrumentation-pika", + "inDistro": false + }, + { + "libraryName": "aio-pika", + "displayName": "aio-pika", + "moduleName": "aio_pika", + "instrumentationPackage": "opentelemetry-instrumentation-aio-pika", + "inDistro": false + }, + { + "libraryName": "aiokafka", + "displayName": "aiokafka", + "moduleName": "aiokafka", + "instrumentationPackage": "opentelemetry-instrumentation-aiokafka", + "inDistro": false + }, + { + "libraryName": "remoulade", + "displayName": "Remoulade", + "moduleName": "remoulade", + "instrumentationPackage": "opentelemetry-instrumentation-remoulade", + "inDistro": false + } + ], + "cloud": [ + { + "libraryName": "boto", + "displayName": "Boto", + "moduleName": "boto", + "instrumentationPackage": "opentelemetry-instrumentation-boto", + "inDistro": false + }, + { + "libraryName": "boto3", + "displayName": "Boto3", + "moduleName": "boto3", + "instrumentationPackage": "opentelemetry-instrumentation-boto3sqs", + "inDistro": false + }, + { + "libraryName": "botocore", + "displayName": "Botocore", + "moduleName": "botocore", + "instrumentationPackage": "opentelemetry-instrumentation-botocore", + "inDistro": false + }, + { + "libraryName": "azure-core", + "displayName": "Azure SDK", + "moduleName": "azure.core", + "instrumentationPackage": "azure-core-tracing-opentelemetry", + "inDistro": true, + "note": "Azure SDK uses plugin system, not standard instrumentor" + } + ], + "genai": [ + { + "libraryName": "openai", + "displayName": "OpenAI", + "moduleName": "openai", + "instrumentationPackage": "opentelemetry-instrumentation-openai", + "inDistro": false + }, + { + "libraryName": "anthropic", + "displayName": "Anthropic", + "moduleName": "anthropic", + "instrumentationPackage": "opentelemetry-instrumentation-anthropic", + "inDistro": false + }, + { + "libraryName": "langchain", + "displayName": "LangChain", + "moduleName": "langchain", + "instrumentationPackage": "opentelemetry-instrumentation-langchain", + "inDistro": false + }, + { + "libraryName": "langchain-core", + "displayName": "LangChain Core", + "moduleName": "langchain_core", + "instrumentationPackage": "opentelemetry-instrumentation-langchain", + "inDistro": false + }, + { + "libraryName": "langchain-community", + "displayName": "LangChain Community", + "moduleName": "langchain_community", + "instrumentationPackage": "opentelemetry-instrumentation-langchain", + "inDistro": false + }, + { + "libraryName": "google-cloud-aiplatform", + "displayName": "Google Cloud AI Platform", + "moduleName": "google.cloud.aiplatform", + "instrumentationPackage": "opentelemetry-instrumentation-vertexai", + "inDistro": false + }, + { + "libraryName": "google-genai", + "displayName": "Google GenAI", + "moduleName": "google.genai", + "instrumentationPackage": "opentelemetry-instrumentation-google-genai", + "inDistro": false + }, + { + "libraryName": "openai-agents", + "displayName": "OpenAI Agents", + "moduleName": "openai.agents", + "instrumentationPackage": "opentelemetry-instrumentation-openai-agents", + "inDistro": false + }, + { + "libraryName": "weaviate-client", + "displayName": "Weaviate Client", + "moduleName": "weaviate", + "instrumentationPackage": "opentelemetry-instrumentation-weaviate", + "inDistro": false + } + ], + "other": [ + { + "libraryName": "jinja2", + "displayName": "Jinja2", + "moduleName": "jinja2", + "instrumentationPackage": "opentelemetry-instrumentation-jinja2", + "inDistro": false + }, + { + "libraryName": "asyncio", + "displayName": "asyncio", + "moduleName": "asyncio", + "instrumentationPackage": "opentelemetry-instrumentation-asyncio", + "inDistro": false + }, + { + "libraryName": "logging", + "displayName": "Logging", + "moduleName": "logging", + "instrumentationPackage": "opentelemetry-instrumentation-logging", + "inDistro": false + }, + { + "libraryName": "asgiref", + "displayName": "ASGI", + "moduleName": "asgiref", + "instrumentationPackage": "opentelemetry-instrumentation-asgi", + "inDistro": false + }, + { + "libraryName": "psutil", + "displayName": "psutil", + "moduleName": "psutil", + "instrumentationPackage": "opentelemetry-instrumentation-system-metrics", + "inDistro": false + } + ] + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/aad-authentication-migration.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/aad-authentication-migration.md new file mode 100644 index 0000000000..4cf2d8b8aa --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/aad-authentication-migration.md @@ -0,0 +1,150 @@ +--- +title: AAD Authentication Migration (2.x to 3.x) +category: migration +applies-to: 3.x +--- + +# AAD Authentication Migration (2.x → 3.x) + +## What changed + +In 2.x, `SetAzureTokenCredential(object)` existed on `TelemetryConfiguration` but accepted an `object` parameter — it used reflection internally to avoid a hard dependency on `Azure.Core`. Some teams also used `IConfigureOptions` workarounds to configure credentials in DI scenarios. + +In 3.x, the method signature changed to **strongly typed** and new DI-friendly options were added: +- `TelemetryConfiguration.SetAzureTokenCredential(TokenCredential)` — **Signature changed** from `object` to `TokenCredential`. Must be called before a `TelemetryClient` is created from that configuration. +- `ApplicationInsightsServiceOptions.Credential` — **New in 3.x**. Preferred for DI scenarios (ASP.NET Core / Worker Service). Set your `TokenCredential` directly in the options lambda. + +Other key changes: +- `TelemetryConfiguration.Active` — **Removed**. Use `TelemetryConfiguration.CreateDefault()` instead. Note: `CreateDefault()` now returns an internal static configuration (singleton-like) rather than a new instance each time. + +--- + +## DI Scenario (ASP.NET Core / Worker Service) + +In 2.x, AAD auth in DI apps was commonly configured via `IConfigureOptions` workarounds. In 3.x, use the new `Credential` property on `ApplicationInsightsServiceOptions` directly — simpler and no extra class needed. + +### Before (2.x — IConfigureOptions pattern) + +```csharp +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.Extensions.Options; +using Azure.Identity; + +public class TelemetryConfigurationEnricher : IConfigureOptions +{ + public void Configure(TelemetryConfiguration options) + { + // 2.x signature: SetAzureTokenCredential(object) — accepts object, uses reflection + object credential = new DefaultAzureCredential(); + options.SetAzureTokenCredential(credential); + } +} + +// In Startup.cs / Program.cs: +services.AddSingleton, TelemetryConfigurationEnricher>(); +services.AddApplicationInsightsTelemetry(); +``` + +### After (3.x — Credential property on options) + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ApplicationInsights.AspNetCore.Extensions; +using Azure.Identity; + +builder.Services.AddApplicationInsightsTelemetry(options => +{ + options.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + options.Credential = new DefaultAzureCredential(); +}); +``` + +### Migration steps (DI) + +1. **Remove the `IConfigureOptions` class** that calls `SetAzureTokenCredential()` (e.g., `TelemetryConfigurationEnricher`). Delete the entire class file if it only handled AAD auth. + +2. **Remove the DI registration** for the configurator: + ```csharp + // Delete this line: + services.AddSingleton, TelemetryConfigurationEnricher>(); + ``` + +3. **Set the `Credential` property** in your `AddApplicationInsightsTelemetry()` options: + ```csharp + builder.Services.AddApplicationInsightsTelemetry(options => + { + options.Credential = new DefaultAzureCredential(); + }); + ``` + +4. For **Worker Service** apps, the same applies to `AddApplicationInsightsTelemetryWorkerService()`: + ```csharp + services.AddApplicationInsightsTelemetryWorkerService(options => + { + options.Credential = new DefaultAzureCredential(); + }); + ``` + +--- + +## Non-DI Scenario (Console apps, manual TelemetryConfiguration) + +For apps that create `TelemetryConfiguration` directly (console apps, batch jobs, etc.), the new `SetAzureTokenCredential` method provides built-in AAD support. The main breaking change is that `TelemetryConfiguration.Active` is removed. + +### Before (2.x — TelemetryConfiguration.Active) + +```csharp +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using Azure.Identity; + +var config = TelemetryConfiguration.Active; +config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; +// 2.x signature: SetAzureTokenCredential(object) — accepts object, uses reflection +config.SetAzureTokenCredential((object)new DefaultAzureCredential()); + +var client = new TelemetryClient(config); +``` + +### After (3.x — CreateDefault) + +```csharp +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using Azure.Identity; + +var config = TelemetryConfiguration.CreateDefault(); +config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; +// 3.x signature: SetAzureTokenCredential(TokenCredential) — strongly typed +config.SetAzureTokenCredential(new DefaultAzureCredential()); + +var client = new TelemetryClient(config); +``` + +### Migration steps (non-DI) + +1. **Replace `TelemetryConfiguration.Active`** with `TelemetryConfiguration.CreateDefault()` — the `Active` static property is removed in 3.x. Note that `CreateDefault()` returns an internal static configuration (singleton-like) rather than creating a new instance. + +2. **Use `SetAzureTokenCredential(TokenCredential)`** — the parameter type changed from `object` (2.x) to strongly typed `TokenCredential` (3.x). If your code previously cast to `object`, remove the cast. Call it **before** creating a `TelemetryClient` from that configuration. + +3. If using a custom `TokenCredential` (e.g., `ManagedIdentityCredential` with a specific client ID), pass it directly: + ```csharp + var credential = new ManagedIdentityCredential("your-client-id"); + var config = TelemetryConfiguration.CreateDefault(); + config.SetAzureTokenCredential(credential); + var client = new TelemetryClient(config); + ``` + +--- + +## Notes + +- The `Azure.Identity` package is still required — no change there. +- If you previously used `SetAzureTokenCredential` conditionally (e.g., only in certain environments), apply the same condition when setting `options.Credential` (DI) or calling `SetAzureTokenCredential` (non-DI). +- Both paths ultimately set `AzureMonitorExporterOptions.Credential` under the hood. + +## See also + +- [AddApplicationInsightsTelemetry API reference](learn://api-reference/dotnet/AddApplicationInsightsTelemetry.md) — full list of `ApplicationInsightsServiceOptions` properties +- [UseAzureMonitor API reference](learn://api-reference/dotnet/UseAzureMonitor.md) — if migrating to Azure Monitor OpenTelemetry Distro instead +- [App Insights 2.x → 3.x code migration](learn://migration/dotnet/appinsights-2x-to-3x-code-migration.md) — full migration guide diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/appinsights-2x-to-3x-code-migration.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/appinsights-2x-to-3x-code-migration.md index 0a8928e5d3..0201651b45 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/appinsights-2x-to-3x-code-migration.md +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/appinsights-2x-to-3x-code-migration.md @@ -102,6 +102,36 @@ Or set `APPLICATIONINSIGHTS_CONNECTION_STRING` as an environment variable and ca | `client.InstrumentationKey` | **Removed**. Use `TelemetryConfiguration.ConnectionString`. | | `TrackTrace`, `TrackMetric`, `TrackRequest`, `TrackDependency` (full overload), `Flush` | **Unchanged** — no action needed. | +## Middleware telemetry access + +In 2.x, middleware could access `RequestTelemetry` via `HttpContext.Features`: + +```csharp +// 2.x +var requestTelemetry = context.Features.Get(); +requestTelemetry?.Properties.Add("ResponseBody", responseBody); +``` + +In 3.x, `RequestTelemetry` is no longer placed in `HttpContext.Features`. Use `Activity.Current` instead: + +```csharp +// 3.x +using System.Diagnostics; + +var activity = Activity.Current; +activity?.SetTag("ResponseBody", responseBody); +``` + +This applies to any code that accessed `RequestTelemetry` or `DependencyTelemetry` via `HttpContext.Features.Get()`. + +## Manually constructed telemetry objects + +Telemetry types (`RequestTelemetry`, `DependencyTelemetry`, `TraceTelemetry`, `EventTelemetry`, `ExceptionTelemetry`, `MetricTelemetry`, `AvailabilityTelemetry`) still exist in 3.x and can be passed to `TelemetryClient.Track*(...)`. However: + +- `ISupportProperties` — **Removed**. Use the typed `Properties` dictionary directly on each telemetry class. +- Type checks (`telemetry is RequestTelemetry`) in custom middleware or filters — these still compile but may not match auto-collected telemetry in contexts where the 3.x SDK uses `Activity` internally. Prefer `Activity.Current?.SetTag()` for enrichment. +- `new DependencyTelemetry(...)` with `StartOperation()` — **Still works**. The dependency is correlated automatically. + ## Migration steps 1. Update the package: diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/aspnet-classic-2x-to-3x-code-migration.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/aspnet-classic-2x-to-3x-code-migration.md new file mode 100644 index 0000000000..4d263349ef --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/aspnet-classic-2x-to-3x-code-migration.md @@ -0,0 +1,190 @@ +--- +title: Classic ASP.NET 2.x to 3.x Migration +category: migration +applies-to: 3.x +--- + +# Classic ASP.NET 2.x → 3.x Code Migration + +## What changed + +3.x is built on OpenTelemetry internally. The NuGet package name stays the same — `Microsoft.ApplicationInsights.Web` — but the internal architecture changed. `applicationinsights.config` format is simplified, satellite packages are removed, and extensibility uses OpenTelemetry processors via `ConfigureOpenTelemetryBuilder`. + +Key changes: +- `` → `` (connection string is **required** — 3.x throws without it) +- ``, ``, `` sections → removed entirely +- `` section → removed (export managed by OpenTelemetry) +- 12 satellite packages → removed (functionality built into the main package) +- `TelemetryConfiguration.Active` → `TelemetryConfiguration.CreateDefault()` (returns static singleton in 3.x) +- Minimum .NET Framework: 4.5.2 → **4.6.2** +- Custom initializers/processors → OpenTelemetry processors via `ConfigureOpenTelemetryBuilder` + +## applicationinsights.config — before / after + +**2.x:** +```xml + + + 00000000-0000-0000-0000-000000000000 + + + + search|spider|crawl|Bot|Monitor + + + + + + + + + + + false + + +``` + +**3.x:** +```xml + + + InstrumentationKey=...;IngestionEndpoint=https://dc.applicationinsights.azure.com/ + 5.0 + true + true + true + true + true + true + +``` + +## Web.config changes + +**Remove** `TelemetryCorrelationHttpModule` (OpenTelemetry handles correlation natively). + +**Keep** `ApplicationInsightsHttpModule` — still needed in 3.x. + +**Add** `TelemetryHttpModule` (from `OpenTelemetry.Instrumentation.AspNet`) — added automatically by NuGet install. + +## Satellite packages to remove + +These packages are no longer needed — their functionality is built into `Microsoft.ApplicationInsights.Web` 3.x. **Remove in this order** (dependents before dependencies): + +1. `Microsoft.ApplicationInsights.WindowsServer` +2. `Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel` +3. `Microsoft.ApplicationInsights.DependencyCollector` +4. `Microsoft.ApplicationInsights.PerfCounterCollector` +5. `Microsoft.ApplicationInsights.Agent.Intercept` +6. `Microsoft.AspNet.TelemetryCorrelation` + +Also remove if present (skip any not in packages.config): +- `Microsoft.ApplicationInsights.EventCounterCollector` +- `Microsoft.Extensions.Logging.ApplicationInsights` +- `Microsoft.ApplicationInsights.Log4NetAppender` +- `Microsoft.ApplicationInsights.TraceListener` +- `Microsoft.ApplicationInsights.DiagnosticSourceListener` +- `Microsoft.ApplicationInsights.EtwCollector` +- `Microsoft.ApplicationInsights.EventSourceListener` + +## TelemetryConfiguration changes + +| 2.x Pattern | 3.x Replacement | +|---|---| +| `TelemetryConfiguration.Active` | `TelemetryConfiguration.CreateDefault()` (returns static singleton) | +| `new TelemetryConfiguration(ikey)` | `TelemetryConfiguration.CreateDefault()` + set `ConnectionString` | +| `config.InstrumentationKey = "..."` | `config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."` | +| `config.TelemetryInitializers.Add(...)` | Use `ConfigureOpenTelemetryBuilder` — see below | +| `config.TelemetryProcessorChainBuilder` | Use `ConfigureOpenTelemetryBuilder` — see below | +| `config.TelemetryChannel` | Removed — export managed by OpenTelemetry | +| `config.TelemetrySinks` | Removed — use OpenTelemetry exporters | + +## Custom processor migration (non-DI) + +In classic ASP.NET, use `ConfigureOpenTelemetryBuilder` on `TelemetryConfiguration` in `Global.asax.cs`: + +```csharp +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using OpenTelemetry.Trace; +using OpenTelemetry.Logs; +using OpenTelemetry.Resources; + +public class MvcApplication : HttpApplication +{ + public static TelemetryClient TelemetryClient { get; private set; } + + protected void Application_Start() + { + var config = TelemetryConfiguration.CreateDefault(); + config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + config.ConfigureOpenTelemetryBuilder(otel => + { + otel.WithTracing(tracing => + { + tracing.AddProcessor(); + tracing.AddProcessor(); + }); + otel.ConfigureResource(r => r.AddService("MyWebApp")); + }); + + TelemetryClient = new TelemetryClient(config); + + AreaRegistration.RegisterAllAreas(); + RouteConfig.RegisterRoutes(RouteTable.Routes); + } + + protected void Application_End() + { + TelemetryClient?.Flush(); + System.Threading.Tasks.Task.Delay(1000).Wait(); + } +} +``` + +> **Important:** Create a **single static** `TelemetryClient` instance in `Application_Start`. Do not create per-request or per-controller instances — this causes memory leaks and duplicate telemetry. `TelemetryConfiguration.CreateDefault()` returns a singleton that is shared with the `ApplicationInsightsHttpModule`. + +See TelemetryConfigurationBuilder.md for full API details. + +## Sampling changes + +- Per-type sampling (e.g. exclude exceptions from sampling) is **no longer supported** +- Default: rate-limited at 5 traces/sec via `` +- Fixed-rate: `0.25` (25%) +- Custom OTel samplers not supported with 3.x shim + +## TelemetryClient changes + +See TelemetryClient.md for full breaking changes. Key items: +- `TrackPageView` — removed entirely +- `TrackEvent`/`TrackException`/`TrackAvailability` — metrics dict parameter removed +- Parameterless `new TelemetryClient()` — removed, use `new TelemetryClient(TelemetryConfiguration.CreateDefault())` +- `client.InstrumentationKey` — removed +- Create a **single static instance** — do not create per-request (see processor migration section above) + +## Migration steps + +1. Check .NET Framework target — must be **4.6.2** or later +2. Upgrade `Microsoft.ApplicationInsights.Web` to 3.x via Package Manager Console: `Update-Package Microsoft.ApplicationInsights.Web` +3. Remove satellite packages via Package Manager Console in this order (dependents before dependencies): + - `Microsoft.ApplicationInsights.WindowsServer` + - `Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel` + - `Microsoft.ApplicationInsights.DependencyCollector` + - `Microsoft.ApplicationInsights.PerfCounterCollector` + - `Microsoft.ApplicationInsights.Agent.Intercept` + - `Microsoft.AspNet.TelemetryCorrelation` +4. Rewrite `applicationinsights.config` to 3.x format (remove all ``, ``, ``, `` sections; add `` and feature flags) +5. Update `Web.config` — remove `TelemetryCorrelationHttpModule`, verify `TelemetryHttpModule` present +6. Replace `TelemetryConfiguration.Active` with `TelemetryConfiguration.CreateDefault()` in code +7. Create a single static `TelemetryClient` in `Application_Start`, flush in `Application_End` +8. Migrate custom `ITelemetryInitializer`/`ITelemetryProcessor` to OpenTelemetry processors via `ConfigureOpenTelemetryBuilder` +9. Fix TelemetryClient breaking calls (TrackEvent metrics dict, TrackPageView, etc.) +10. Set `` in applicationinsights.config +11. Build and verify + +## See also + +- ApplicationInsightsWeb API reference(see in ApplicationInsightsWeb.md) +- ConfigureOpenTelemetryBuilder(see in TelemetryConfigurationBuilder.md) +- TelemetryClient breaking changes(see in TelemetryClient.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/console-2x-to-3x-code-migration.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/console-2x-to-3x-code-migration.md new file mode 100644 index 0000000000..dec71c77d5 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/console-2x-to-3x-code-migration.md @@ -0,0 +1,106 @@ +--- +title: Console App Application Insights 2.x to 3.x Migration +category: migration +applies-to: 3.x +--- + +# Console App — AI SDK 2.x → 3.x Migration + +## What changed + +Console apps use `TelemetryConfiguration` directly (non-DI). In 3.x, the core SDK is rebuilt on OpenTelemetry internally. Several APIs on `TelemetryConfiguration` and `TelemetryClient` are removed or changed. + +Key changes: +- `TelemetryConfiguration.Active` — **Removed**. Use `TelemetryConfiguration.CreateDefault()`. Returns a static singleton in 3.x. +- `config.InstrumentationKey` — **Removed**. Use `config.ConnectionString` (required — throws if not set). +- `config.TelemetryInitializers` collection — **Removed**. Use `config.ConfigureOpenTelemetryBuilder(b => b.WithTracing(t => t.AddProcessor()))`. +- `config.TelemetryProcessors` / `TelemetryProcessorChainBuilder` — **Removed**. Use OpenTelemetry processors. +- `config.TelemetryChannel` / `TelemetrySinks` — **Removed**. Export pipeline managed by OpenTelemetry internally. +- `DependencyTrackingTelemetryModule` manual init — **No longer needed**. Dependency tracking is automatic in 3.x. +- `new TelemetryClient()` (parameterless) — **Removed**. Use `new TelemetryClient(config)`. +- `SetAzureTokenCredential(object)` — Signature changed to `SetAzureTokenCredential(TokenCredential)` (strongly typed). + +## Before / after + +**2.x** +```csharp +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DependencyCollector; +using Microsoft.ApplicationInsights.Extensibility; + +var config = TelemetryConfiguration.Active; // or CreateDefault() +config.InstrumentationKey = "your-ikey"; +config.TelemetryInitializers.Add(new MyInitializer()); + +var dtModule = new DependencyTrackingTelemetryModule(); +dtModule.Initialize(config); + +var client = new TelemetryClient(config); +client.TrackEvent("Started"); +// ... +client.Flush(); +``` + +**3.x** +```csharp +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using OpenTelemetry; + +var config = TelemetryConfiguration.CreateDefault(); +config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + +// Extensibility via OpenTelemetry builder +config.ConfigureOpenTelemetryBuilder(builder => +{ + builder.WithTracing(tracing => tracing.AddProcessor()); +}); + +var client = new TelemetryClient(config); +client.TrackEvent("Started"); +// ... +client.Flush(); +``` + +## Removed APIs + +| API | Status | Replacement | +|---|---|---| +| `TelemetryConfiguration.Active` | **Removed** | `TelemetryConfiguration.CreateDefault()` (static singleton) | +| `TelemetryConfiguration(string ikey)` | **Removed** | `CreateDefault()` + set `ConnectionString` | +| `config.InstrumentationKey` | **Removed** | `config.ConnectionString` | +| `config.TelemetryInitializers` | **Removed** | `config.ConfigureOpenTelemetryBuilder(...)` with `AddProcessor()` | +| `config.TelemetryProcessors` | **Removed** | `config.ConfigureOpenTelemetryBuilder(...)` with `AddProcessor()` | +| `config.TelemetryChannel` | **Removed** | Managed internally by OpenTelemetry | +| `config.TelemetrySinks` | **Removed** | Use OpenTelemetry exporters via `ConfigureOpenTelemetryBuilder` | +| `new TelemetryClient()` | **Removed** | `new TelemetryClient(config)` | +| `client.InstrumentationKey` | **Removed** | Use `config.ConnectionString` | +| `DependencyTrackingTelemetryModule` | **Not needed** | Dependency tracking is automatic | +| `OperationCorrelationTelemetryInitializer` | **Not needed** | Correlation is automatic via OpenTelemetry | +| `HttpDependenciesParsingTelemetryInitializer` | **Not needed** | Parsing is automatic | + +## New APIs + +| API | Description | +|---|---| +| `config.ConnectionString` | Required. Set before creating `TelemetryClient`. | +| `config.ConfigureOpenTelemetryBuilder(Action)` | Hook for adding processors, exporters, instrumentation. **Requires `using OpenTelemetry;`** — the `WithTracing()`, `WithLogging()`, `WithMetrics()`, and `ConfigureResource()` extension methods are in the root `OpenTelemetry` namespace. | +| `config.SetAzureTokenCredential(TokenCredential)` | AAD auth (strongly typed — was `object` in 2.x) | +| `config.SamplingRatio` | Fixed-rate sampling (0.0–1.0) | +| `config.TracesPerSecond` | Rate-limited sampling | + +## Migration steps + +1. Replace `TelemetryConfiguration.Active` with `TelemetryConfiguration.CreateDefault()` +2. Replace `config.InstrumentationKey = "..."` with `config.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."` +3. Remove manual `DependencyTrackingTelemetryModule` initialization — dependency tracking is automatic +4. Remove `OperationCorrelationTelemetryInitializer` and `HttpDependenciesParsingTelemetryInitializer` — correlation and parsing are automatic +5. Migrate custom `ITelemetryInitializer` implementations to `BaseProcessor` — register via `config.ConfigureOpenTelemetryBuilder(b => b.WithTracing(t => t.AddProcessor()))` +6. Update `SetAzureTokenCredential((object)cred)` to `SetAzureTokenCredential(cred)` (remove cast — parameter is now `TokenCredential`) +7. Update the NuGet package: `Microsoft.ApplicationInsights` to `3.*`, remove `Microsoft.ApplicationInsights.DependencyCollector` + +## See also + +- [TelemetryConfigurationBuilder API reference](learn://api-reference/dotnet/TelemetryConfigurationBuilder.md) +- [TelemetryClient API reference](learn://api-reference/dotnet/TelemetryClient.md) +- [AAD Authentication Migration](learn://migration/dotnet/aad-authentication-migration.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/ilogger-migration.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/ilogger-migration.md new file mode 100644 index 0000000000..a4b95bc360 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/ilogger-migration.md @@ -0,0 +1,54 @@ +--- +title: ILogger and Log Filtering Migration (2.x to 3.x) +category: migration +applies-to: 3.x +--- + +# ILogger and Log Filtering Migration (2.x → 3.x) + +## What changed + +In 2.x, `AddApplicationInsightsTelemetry()` already captured ILogger output automatically. However, some codebases also called `AddApplicationInsights()` on `ILoggingBuilder` explicitly — for example, to configure `ApplicationInsightsLoggerOptions` or to add category-level log filters targeting `ApplicationInsightsLoggerProvider`. + +In 3.x, ILogger capture remains automatic. The difference is that the underlying provider is now `OpenTelemetryLoggerProvider` (from the OpenTelemetry SDK). Any explicit `AddApplicationInsights()` calls and filters targeting `ApplicationInsightsLoggerProvider` must be updated. + +| Aspect | 2.x | 3.x | +|---|---|---| +| Logger provider | `ApplicationInsightsLoggerProvider` (explicit) | `OpenTelemetryLoggerProvider` (automatic) | +| Registration | `loggingBuilder.AddApplicationInsights()` | Not needed — automatic | +| Category filters | `AddFilter(...)` | `AddFilter(...)` or plain `AddFilter(...)` | +| Advanced filtering | `ITelemetryProcessor` | `BaseProcessor` via `ConfigureOpenTelemetryLoggerProvider` | + +## Before / after + +**2.x** +```csharp +using Microsoft.Extensions.Logging.ApplicationInsights; + +builder.Services.AddLogging(loggingBuilder => +{ + loggingBuilder.AddApplicationInsights(options => + { + options.TrackExceptionsAsExceptionTelemetry = true; + options.IncludeScopes = true; + }); + loggingBuilder.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); + loggingBuilder.AddFilter("MyApp", LogLevel.Information); +}); +``` + +**3.x** +```csharp +using OpenTelemetry.Logs; // Required for OpenTelemetryLoggerProvider + +// Logging is automatic — just configure filters targeting the OTel provider +builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); +builder.Logging.AddFilter("MyApp", LogLevel.Information); +``` + +## Migration steps + +1. **Remove `AddApplicationInsights()`** — delete the call and its options lambda (`TrackExceptionsAsExceptionTelemetry`, `IncludeScopes` no longer exist). +2. **Replace log filters** — change `AddFilter` to `AddFilter`, or use a plain `AddFilter(category, level)`. Add `using OpenTelemetry.Logs;` if targeting the provider specifically. +3. **Update usings** — remove `using Microsoft.Extensions.Logging.ApplicationInsights;`. +4. **Remove the package** — `Microsoft.Extensions.Logging.ApplicationInsights` is no longer needed. diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/workerservice-2x-to-3x-code-migration.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/workerservice-2x-to-3x-code-migration.md new file mode 100644 index 0000000000..f799e57dbe --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/workerservice-2x-to-3x-code-migration.md @@ -0,0 +1,126 @@ +--- +title: WorkerService 2.x to 3.x Code Migration +category: migration +applies-to: 3.x +--- + +# Worker Service 2.x → 3.x Code Migration + +## What changed + +3.x uses OpenTelemetry under the hood. The main entry point is the same — `AddApplicationInsightsTelemetryWorkerService()` — but several options and an extension method were removed. + +Key changes: +- `InstrumentationKey` → use `ConnectionString` +- `EnableAdaptiveSampling` → use `TracesPerSecond` (default `5`) or `SamplingRatio` +- Logging is automatic — no additional logger provider needed +- New: `Credential` for AAD authentication, `EnableTraceBasedLogsSampler` for log sampling control + +## Before / after + +**2.x** +```csharp +using Microsoft.Extensions.DependencyInjection; + +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(options => + { + options.InstrumentationKey = "your-ikey"; // Removed in 3.x + options.EnableAdaptiveSampling = false; // Removed in 3.x + options.DeveloperMode = true; // Removed in 3.x + }); +}); +``` + +**3.x** +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ApplicationInsights.WorkerService; + +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(options => + { + options.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + options.SamplingRatio = 1.0f; // No sampling (collect everything) + // DeveloperMode — no replacement needed, remove the line + }); +}); +``` + +Or set `APPLICATIONINSIGHTS_CONNECTION_STRING` as an environment variable and call `AddApplicationInsightsTelemetryWorkerService()` with no arguments. + +## Property changes + +| Property | Status | Action required | +|---|---|---| +| `ConnectionString` | Unchanged | None. | +| `ApplicationVersion` | Unchanged | None. | +| `EnableQuickPulseMetricStream` | Unchanged | None. Default `true`. | +| `EnablePerformanceCounterCollectionModule` | Unchanged | None. Default `true`. | +| `EnableDependencyTrackingTelemetryModule` | Unchanged | None. Default `true`. | +| `AddAutoCollectedMetricExtractor` | Unchanged | None. Default `true`. | +| `InstrumentationKey` | **Removed** | Use `ConnectionString`. | +| `EnableAdaptiveSampling` | **Removed** | Use `TracesPerSecond` or `SamplingRatio`. | +| `DeveloperMode` | **Removed** | Delete the line. | +| `EndpointAddress` | **Removed** | Endpoint is now part of `ConnectionString`. | +| `EnableHeartbeat` | **Removed** | Delete the line. | +| `EnableDebugLogger` | **Removed** | Delete the line. | +| `DependencyCollectionOptions` | **Removed** | Delete the line. | +| `EnableEventCounterCollectionModule` | **Removed** | Delete the line. | +| `EnableAppServicesHeartbeatTelemetryModule` | **Removed** | Delete the line; heartbeat is automatic. | +| `EnableAzureInstanceMetadataTelemetryModule` | **Removed** | Delete the line; resource detection is automatic. | +| `EnableDiagnosticsTelemetryModule` | **Removed** | Delete the line. | +| `Credential` | **New** | `TokenCredential`, default `null`. Set for AAD auth. | +| `TracesPerSecond` | **New** | `double?`, effective default `5`. Rate-limited sampling. | +| `SamplingRatio` | **New** | `float?`, default `null`. Fixed-rate sampling (0.0–1.0). | +| `EnableTraceBasedLogsSampler` | **New** | `bool?`, effective default `true`. Logs follow parent trace sampling. | + +## Removed extension methods + +| Method | Replacement | +|---|---| +| `AddApplicationInsightsTelemetryWorkerService(string instrumentationKey)` | Use parameterless overload + `ConnectionString` in options or env var. | + +## TelemetryClient changes + +| Change | Details | +|---|---| +| `TrackEvent` | 3-param overload `(string, IDictionary, IDictionary)` **removed** — metrics dict dropped. Use 2-param overload and track metrics separately via `TrackMetric()`. | +| `TrackException` | 3-param overload with `IDictionary` **removed**. Use 2-param overload and track metrics separately via `TrackMetric()`. | +| `TrackAvailability` | 8-param overload with trailing `IDictionary` **removed**. Use 7-param overload and track metrics separately via `TrackMetric()`. | +| `TrackPageView` | **Removed entirely** (both overloads). Use `TrackEvent` or `TrackRequest` instead. | +| `GetMetric` | `MetricConfiguration` and `MetricAggregationScope` params **removed** from all overloads. Use simplified `GetMetric(metricId, ...)`. | +| parameterless `TelemetryClient()` | **Removed**. Use `TelemetryClient(TelemetryConfiguration)` via DI. | +| `client.InstrumentationKey` | **Removed**. Use `TelemetryConfiguration.ConnectionString`. | +| `TrackTrace`, `TrackMetric`, `TrackRequest`, `TrackDependency` (full overload), `Flush` | **Unchanged** — no action needed. | + +## Migration steps + +1. Update the package: + ```xml + + ``` + +2. Find and replace in your code: + - `InstrumentationKey = "..."` → `ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."` + - `EnableAdaptiveSampling = false` → `SamplingRatio = 1.0f` (or set `TracesPerSecond`) + - Delete any lines setting `DeveloperMode`, `EndpointAddress`, `EnableHeartbeat`, `EnableDebugLogger`, `DependencyCollectionOptions`, `EnableEventCounterCollectionModule`, `EnableAppServicesHeartbeatTelemetryModule`, `EnableAzureInstanceMetadataTelemetryModule`, or `EnableDiagnosticsTelemetryModule` + - Replace `AddApplicationInsightsTelemetryWorkerService("your-ikey")` with `AddApplicationInsightsTelemetryWorkerService()` and set `ConnectionString` via options or env var + +3. Migrate TelemetryClient breaking calls: + - Remove `IDictionary metrics` parameter from `TrackEvent`/`TrackException`/`TrackAvailability` calls (track metrics separately via `TrackMetric()`). Replace `TrackPageView` with `TrackEvent` or `TrackRequest`. Remove `GetMetric` overloads that take `MetricConfiguration`/`MetricAggregationScope`. + +4. Build and verify — the `Enable*` flags (`EnableQuickPulseMetricStream`, `EnableDependencyTrackingTelemetryModule`, etc.) still work with the same defaults. No changes needed for those. + +## Behavior notes + +- `TracesPerSecond` is the default sampling mode (effective default `5`). No configuration needed for most apps. +- Connection string resolution order: `ApplicationInsightsServiceOptions.ConnectionString` → `APPLICATIONINSIGHTS_CONNECTION_STRING` env var → `ApplicationInsights:ConnectionString` in config. + +## See also + +- No-code-change migration(see in workerservice-2x-to-3x-no-code-change.md) +- AddApplicationInsightsTelemetryWorkerService API reference(see in AddApplicationInsightsTelemetryWorkerService.md) +- TelemetryClient breaking changes(see in TelemetryClient.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/workerservice-2x-to-3x-no-code-change.md b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/workerservice-2x-to-3x-no-code-change.md new file mode 100644 index 0000000000..f5b883eee6 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Instrumentation/Resources/migration/dotnet/workerservice-2x-to-3x-no-code-change.md @@ -0,0 +1,102 @@ +--- +title: WorkerService 2.x to 3.x — No-Code-Change Migration +category: migration +applies-to: 3.x +--- + +# Worker Service 2.x → 3.x — No Code Change + +## When this applies + +Your migration requires **only a package upgrade** (no code changes) if both of these are true: + +1. You call `AddApplicationInsightsTelemetryWorkerService()` with no arguments, an `IConfiguration`, or with options that only set **unchanged properties**. +2. You do not call `AddApplicationInsightsTelemetryWorkerService(string instrumentationKey)`. + +### Unchanged properties (safe to keep as-is) + +| Property | Default | +|---|---| +| `ConnectionString` | `null` | +| `ApplicationVersion` | Entry assembly version | +| `EnableQuickPulseMetricStream` | `true` | +| `EnablePerformanceCounterCollectionModule` | `true` | +| `EnableDependencyTrackingTelemetryModule` | `true` | +| `AddAutoCollectedMetricExtractor` | `true` | + +If your code only uses these properties (or none at all), no code changes are needed. + +## Migration steps + +### 1. Update the package + +```xml + +``` + +### 2. Build and run + +That's it. No code changes required. + +## Examples that work without changes + +**Parameterless call:** +```csharp +using Microsoft.Extensions.DependencyInjection; + +var builder = Host.CreateDefaultBuilder(args); +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(); +}); +var host = builder.Build(); +await host.RunAsync(); +``` + +**IConfiguration overload:** +```csharp +using Microsoft.Extensions.DependencyInjection; + +var builder = Host.CreateDefaultBuilder(args); +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(builder.Configuration); +}); +var host = builder.Build(); +await host.RunAsync(); +``` + +**Options with only unchanged properties:** +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ApplicationInsights.WorkerService; + +var builder = Host.CreateDefaultBuilder(args); +builder.ConfigureServices(services => +{ + services.AddApplicationInsightsTelemetryWorkerService(options => + { + options.ConnectionString = "InstrumentationKey=...;IngestionEndpoint=..."; + options.EnableQuickPulseMetricStream = true; + options.EnableDependencyTrackingTelemetryModule = true; + }); +}); +var host = builder.Build(); +await host.RunAsync(); +``` + +All three examples above work identically in 2.x and 3.x. + +## What changes under the hood + +Even though your code stays the same, 3.x brings these improvements automatically: + +- Telemetry is now collected via OpenTelemetry — better standards alignment and ecosystem compatibility. +- `TracesPerSecond` (effective default `5`) provides rate-limited sampling out of the box. No configuration needed. +- Logging is integrated automatically — `ILogger` output is exported to Application Insights without additional setup. +- Azure resource detection (App Service, VM) happens automatically. + +## See also + +- WorkerService 2.x to 3.x Code Migration(see in workerservice-2x-to-3x-code-migration.md) +- AddApplicationInsightsTelemetryWorkerService API reference(see in AddApplicationInsightsTelemetryWorkerService.md) diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Analysis.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Analysis.cs index b71b1ceed6..ad2f4b3625 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Analysis.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Analysis.cs @@ -22,6 +22,8 @@ public record ExistingInstrumentation { public InstrumentationType Type { get; init; } public string? Version { get; init; } + /// true when the detected SDK version is already the target (3.x for App Insights, any for Distro) + public bool IsTargetVersion { get; init; } public List Evidence { get; init; } = []; } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/AnalysisTemplate.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/AnalysisTemplate.cs index 60c5399050..7bd2dee007 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/AnalysisTemplate.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/AnalysisTemplate.cs @@ -7,6 +7,8 @@ public sealed record AnalysisTemplate public required ProcessorTemplate Processors { get; init; } public required ClientUsageTemplate ClientUsage { get; init; } public required SamplingTemplate Sampling { get; init; } + public required TelemetryPipelineTemplate TelemetryPipeline { get; init; } + public required LoggingTemplate Logging { get; init; } public static AnalysisTemplate CreateDefault() { @@ -14,8 +16,8 @@ public static AnalysisTemplate CreateDefault() { ServiceOptions = new ServiceOptionsTemplate { - EntryPointFile = "(string) File containing AddApplicationInsightsTelemetry, e.g. Program.cs", - SetupPattern = "(string) e.g. AddApplicationInsightsTelemetry", + EntryPointFile = "(string) File containing AddApplicationInsightsTelemetry or AddApplicationInsightsTelemetryWorkerService, e.g. Program.cs", + SetupPattern = "(string) e.g. AddApplicationInsightsTelemetry or AddApplicationInsightsTelemetryWorkerService", InstrumentationKey = "(string|null) options.InstrumentationKey value - REMOVED in 3.x", ConnectionString = "(string|null) options.ConnectionString value", EnableAdaptiveSampling = "(bool|null) - REMOVED in 3.x", @@ -25,26 +27,31 @@ public static AnalysisTemplate CreateDefault() EnableDebugLogger = "(bool|null) - REMOVED in 3.x", RequestCollectionOptions = "(string|null) - REMOVED in 3.x", DependencyCollectionOptions = "(string|null) - REMOVED in 3.x", + EnableEventCounterCollectionModule = "(bool|null) - REMOVED in 3.x (Worker Service only)", + EnableAppServicesHeartbeatTelemetryModule = "(bool|null) - REMOVED in 3.x (Worker Service only)", + EnableAzureInstanceMetadataTelemetryModule = "(bool|null) - REMOVED in 3.x (Worker Service only)", + EnableDiagnosticsTelemetryModule = "(bool|null) - REMOVED in 3.x (Worker Service only)", SamplingRatio = "(number|null) - New in 3.x, already correct if set", TracesPerSecond = "(number|null) - New in 3.x, already correct if set", EnableQuickPulseMetricStream = "(bool|null) - unchanged", UseApplicationInsights = "(bool) true if UseApplicationInsights() call found - REMOVED in 3.x", AddTelemetryProcessor = "(bool) true if AddApplicationInsightsTelemetryProcessor() found - REMOVED in 3.x", - ConfigureTelemetryModule = "(bool) true if ConfigureTelemetryModule() found - REMOVED in 3.x" + ConfigureTelemetryModule = "(bool) true if ConfigureTelemetryModule() found - REMOVED in 3.x", + UsesInstrumentationKeyOverload = "(bool) true if string overload e.g. AddApplicationInsightsTelemetry(\"ikey\") or AddApplicationInsightsTelemetryWorkerService(\"ikey\") found - REMOVED in 3.x" }, Initializers = new InitializerTemplate { - Found = "(bool) true if any ITelemetryInitializer implementations exist", + Found = "(bool) true if any ITelemetryInitializer or IConfigureOptions implementations exist", Implementations = [ new ImplementationTemplate { ClassName = "(string) class name", File = "(string) file path", - Purpose = "(string) what this initializer does" + Purpose = "(string) what this initializer does \u2014 for IConfigureOptions, mention if it calls SetAzureTokenCredential for AAD auth" } ], - Registrations = ["(string) the DI registration line, e.g. services.AddSingleton()"] + Registrations = ["(string) the DI registration line, e.g. services.AddSingleton() or services.AddSingleton, MyEnricher>()"] }, Processors = new ProcessorTemplate { @@ -69,7 +76,7 @@ public static AnalysisTemplate CreateDefault() { File = "(string) file path", Pattern = "(string) e.g. Constructor injection of TelemetryClient", - Methods = ["(string) method names called, e.g. TrackEvent, TrackException"] + Methods = ["(string) every Track*/GetMetric method name called, e.g. TrackEvent, TrackException, TrackPageView, TrackAvailability, TrackTrace, TrackMetric, TrackDependency, GetMetric"] } ] }, @@ -79,6 +86,22 @@ public static AnalysisTemplate CreateDefault() Type = "(string|null) e.g. adaptive, fixed-rate", Details = "(string|null) description of what was found", File = "(string|null) file where sampling is configured" + }, + TelemetryPipeline = new TelemetryPipelineTemplate + { + Found = "(bool) true if any custom ITelemetryChannel, TelemetryConfiguration.TelemetryChannel assignment, TelemetrySinks, or DefaultTelemetrySink usage exists", + HasCustomChannel = "(bool) true if custom ITelemetryChannel implementation or TelemetryChannel assignment found", + HasTelemetrySinks = "(bool) true if TelemetrySinks or DefaultTelemetrySink usage found", + ClassName = "(string|null) class name if custom channel implementation found", + File = "(string|null) file path", + Details = "(string|null) description of what was found" + }, + Logging = new LoggingTemplate + { + Found = "(bool) true if any explicit Application Insights logger provider configuration exists", + HasExplicitLoggerProvider = "(bool) true if AddApplicationInsights() on ILoggingBuilder found (e.g. loggingBuilder.AddApplicationInsights() or services.AddLogging(b => b.AddApplicationInsights(...)))", + LogFilters = ["(string) each AddFilter(...) line found"], + File = "(string|null) file where the logging configuration is located" } }; } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/BrownfieldFindings.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/BrownfieldFindings.cs index 321f1c19eb..f3cf83852e 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/BrownfieldFindings.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/BrownfieldFindings.cs @@ -12,6 +12,7 @@ public record BrownfieldFindings public ClientUsageFindings? ClientUsage { get; init; } public SamplingFindings? Sampling { get; init; } public TelemetryPipelineFindings? TelemetryPipeline { get; init; } + public LoggingFindings? Logging { get; init; } } /// @@ -132,3 +133,23 @@ public record TelemetryPipelineFindings public string? File { get; init; } public string? Details { get; init; } } + +/// +/// Findings from analyzing explicit Application Insights logger provider configuration. +/// In 3.x, ILogger output is exported to Application Insights automatically - explicit +/// AddApplicationInsights() calls and AddFilter<ApplicationInsightsLoggerProvider> must be removed. +/// +public record LoggingFindings +{ + /// true if any explicit Application Insights logger provider configuration exists + public bool Found { get; init; } + + /// true if AddApplicationInsights() on ILoggingBuilder is found + public bool HasExplicitLoggerProvider { get; init; } + + /// AddFilter<ApplicationInsightsLoggerProvider>(...) lines found + public List LogFilters { get; init; } = []; + + /// File containing the logging configuration + public string? File { get; init; } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Constants.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Constants.cs index e830a57f01..e105bdbb28 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Constants.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/Constants.cs @@ -20,6 +20,7 @@ public static class LearningResources // .NET public const string ConceptsOpenTelemetryPipelineDotNet = "learn://concepts/dotnet/opentelemetry-pipeline.md"; public const string ConceptsAzureMonitorDistro = "learn://concepts/dotnet/azure-monitor-distro.md"; + public const string ConceptsAppInsightsAspNetCore = "learn://concepts/dotnet/appinsights-aspnetcore.md"; public const string ApiUseAzureMonitor = "learn://api-reference/dotnet/UseAzureMonitor.md"; public const string ApiAddOpenTelemetry = "learn://api-reference/dotnet/AddOpenTelemetry.md"; public const string ApiOpenTelemetrySdkCreate = "learn://api-reference/dotnet/OpenTelemetrySdkCreate.md"; @@ -32,9 +33,17 @@ public static class LearningResources public const string ApiSampling = "learn://api-reference/dotnet/Sampling.md"; public const string ApiActivityProcessors = "learn://api-reference/dotnet/ActivityProcessors.md"; public const string ApiLogProcessors = "learn://api-reference/dotnet/LogProcessors.md"; + public const string ApiTelemetryClient = "learn://api-reference/dotnet/TelemetryClient.md"; + public const string ApiEntityFrameworkInstrumentation = "learn://api-reference/dotnet/EntityFrameworkInstrumentation.md"; + public const string ApiRedisInstrumentation = "learn://api-reference/dotnet/RedisInstrumentation.md"; + public const string ApiSqlClientInstrumentation = "learn://api-reference/dotnet/SqlClientInstrumentation.md"; + public const string ApiHttpInstrumentation = "learn://api-reference/dotnet/HttpInstrumentation.md"; + public const string ApiOtlpExporter = "learn://api-reference/dotnet/OtlpExporter.md"; + public const string ApiConsoleExporter = "learn://api-reference/dotnet/ConsoleExporter.md"; public const string ApiAzureMonitorExporter = "learn://api-reference/dotnet/AzureMonitorExporter.md"; public const string ApiUseAzureMonitorExporter = "learn://api-reference/dotnet/UseAzureMonitorExporter.md"; public const string ApiApplicationInsightsWeb = "learn://api-reference/dotnet/ApplicationInsightsWeb.md"; + public const string ApiConfigureOpenTelemetryBuilder = "learn://api-reference/dotnet/TelemetryConfigurationBuilder.md"; public const string ApiAddApplicationInsightsTelemetry = "learn://api-reference/dotnet/AddApplicationInsightsTelemetry.md"; public const string ApiAddApplicationInsightsTelemetryWorkerService = "learn://api-reference/dotnet/AddApplicationInsightsTelemetryWorkerService.md"; public const string ConceptsAspNetClassicAppInsights = "learn://concepts/dotnet/aspnet-classic-appinsights.md"; @@ -42,7 +51,12 @@ public static class LearningResources public const string MigrationAppInsights2xTo3xNoCodeChange = "learn://migration/dotnet/appinsights-2x-to-3x-no-code-change.md"; public const string MigrationWorkerService2xTo3xCode = "learn://migration/dotnet/workerservice-2x-to-3x-code-migration.md"; public const string MigrationWorkerService2xTo3xNoCodeChange = "learn://migration/dotnet/workerservice-2x-to-3x-no-code-change.md"; + public const string MigrationAspNetClassic2xTo3xCode = "learn://migration/dotnet/aspnet-classic-2x-to-3x-code-migration.md"; + public const string MigrationConsole2xTo3xCode = "learn://migration/dotnet/console-2x-to-3x-code-migration.md"; + public const string MigrationAadAuthentication = "learn://migration/dotnet/aad-authentication-migration.md"; + public const string MigrationILoggerMigration = "learn://migration/dotnet/ilogger-migration.md"; public const string ExampleAspNetCoreSetup = "learn://examples/dotnet/aspnetcore-setup.md"; + public const string ExampleAspNetCoreDistroSetup = "learn://examples/dotnet/aspnetcore-distro-setup.md"; public const string ExampleAspNetClassicSetup = "learn://examples/dotnet/aspnet-classic-setup.md"; public const string ExampleWorkerServiceSetup = "learn://examples/dotnet/workerservice-setup.md"; @@ -85,14 +99,29 @@ public static class Packages public const string ApplicationInsightsAspNetCore3x = "3.*"; public const string WorkerService = "Microsoft.ApplicationInsights.WorkerService"; public const string WorkerServiceVersion = "3.0.0-rc1"; + public const string WorkerService3x = "3.*"; + public const string ApplicationInsightsWeb = "Microsoft.ApplicationInsights.Web"; + public const string ApplicationInsightsWeb3x = "3.*"; + public const string ApplicationInsightsCore = "Microsoft.ApplicationInsights"; + public const string ApplicationInsightsCore3x = "3.*"; public const string PackageManagerNuGet = "nuget"; + public const string PackageManagerNuGetVS = "nuget-vs"; public const string LatestStableVersion = "latest-stable"; + + // Enhancement packages + public const string EntityFrameworkInstrumentation = "OpenTelemetry.Instrumentation.EntityFrameworkCore"; + public const string RedisInstrumentation = "OpenTelemetry.Instrumentation.StackExchangeRedis"; + public const string SqlClientInstrumentation = "OpenTelemetry.Instrumentation.SqlClient"; + public const string HttpInstrumentation = "OpenTelemetry.Instrumentation.Http"; + public const string AspNetCoreInstrumentation = "OpenTelemetry.Instrumentation.AspNetCore"; + public const string OtlpExporter = "OpenTelemetry.Exporter.OpenTelemetryProtocol"; + public const string ConsoleExporter = "OpenTelemetry.Exporter.Console"; } // Configuration public static class Config { - public const string AzureMonitorConnectionStringPath = "AzureMonitor.ConnectionString"; + public const string AzureMonitorConnectionStringPath = "AzureMonitor:ConnectionString"; public const string AppInsightsConnectionStringPath = "ApplicationInsights:ConnectionString"; public const string ConnectionStringEnvVar = "APPLICATIONINSIGHTS_CONNECTION_STRING"; public const string ConnectionStringPlaceholder = ""; @@ -119,6 +148,7 @@ public static class Intents { public const string Onboard = "onboard"; public const string Migrate = "migrate"; + public const string Enhance = "enhance"; public const string Error = "error"; public const string ClarificationNeeded = "clarification-needed"; public const string Unsupported = "unsupported"; diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/GeneratorConfigLoader.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/GeneratorConfigLoader.cs index fc9871163f..aec583032c 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/GeneratorConfigLoader.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/GeneratorConfigLoader.cs @@ -59,6 +59,10 @@ public class ActionConfig // Manual Step specific public string? Instructions { get; set; } public List? Links { get; set; } + + // Validate Install specific + public List? FilesToExist { get; set; } + public Dictionary>? FileContentChecks { get; set; } } /// diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/LoggingTemplate.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/LoggingTemplate.cs new file mode 100644 index 0000000000..c9cc209fef --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/LoggingTemplate.cs @@ -0,0 +1,9 @@ +namespace Azure.Mcp.Tools.Monitor.Models; + +public sealed record LoggingTemplate +{ + public required string Found { get; init; } + public required string HasExplicitLoggerProvider { get; init; } + public required List LogFilters { get; init; } + public required string File { get; init; } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/OnboardingSpecBuilder.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/OnboardingSpecBuilder.cs index 36ff2c8952..d1fd78910c 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/OnboardingSpecBuilder.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/OnboardingSpecBuilder.cs @@ -269,10 +269,11 @@ public OnboardingSpecBuilder AddActionsFromConfig( break; case "validate-install": + var validateInstructions = BuildValidateInstallInstructions(action, projectDir); AddManualStepAction( action.Id, action.Title, - action.Instructions ?? $"Verify that the installation step is complete: {action.Title}", + validateInstructions, action.Links); break; @@ -282,4 +283,36 @@ public OnboardingSpecBuilder AddActionsFromConfig( } return this; } + + private static string BuildValidateInstallInstructions(ActionConfig action, string projectDir) + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine(action.Instructions ?? $"Verify that the installation step is complete: {action.Title}"); + + if (action.FilesToExist is { Count: > 0 }) + { + sb.AppendLine(); + sb.AppendLine("Check that these files were created:"); + foreach (var file in action.FilesToExist) + { + sb.AppendLine($" - {Path.Combine(projectDir, file)}"); + } + } + + if (action.FileContentChecks is { Count: > 0 }) + { + sb.AppendLine(); + sb.AppendLine("Check that these files contain the expected content:"); + foreach (var (file, expectedStrings) in action.FileContentChecks) + { + sb.AppendLine($" {file} must contain:"); + foreach (var s in expectedStrings) + { + sb.AppendLine($" - \"{s}\""); + } + } + } + + return sb.ToString(); + } } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/ServiceOptionsTemplate.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/ServiceOptionsTemplate.cs index 304e0de2bb..f5216ecbbb 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/ServiceOptionsTemplate.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/ServiceOptionsTemplate.cs @@ -13,10 +13,15 @@ public sealed record ServiceOptionsTemplate public required string EnableDebugLogger { get; init; } public required string RequestCollectionOptions { get; init; } public required string DependencyCollectionOptions { get; init; } + public required string EnableEventCounterCollectionModule { get; init; } + public required string EnableAppServicesHeartbeatTelemetryModule { get; init; } + public required string EnableAzureInstanceMetadataTelemetryModule { get; init; } + public required string EnableDiagnosticsTelemetryModule { get; init; } public required string SamplingRatio { get; init; } public required string TracesPerSecond { get; init; } public required string EnableQuickPulseMetricStream { get; init; } public required string UseApplicationInsights { get; init; } public required string AddTelemetryProcessor { get; init; } public required string ConfigureTelemetryModule { get; init; } + public required string UsesInstrumentationKeyOverload { get; init; } } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/TelemetryPipelineTemplate.cs b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/TelemetryPipelineTemplate.cs new file mode 100644 index 0000000000..c5373fb062 --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Models/Instrumentation/TelemetryPipelineTemplate.cs @@ -0,0 +1,11 @@ +namespace Azure.Mcp.Tools.Monitor.Models; + +public sealed record TelemetryPipelineTemplate +{ + public required string Found { get; init; } + public required string HasCustomChannel { get; init; } + public required string HasTelemetrySinks { get; init; } + public required string ClassName { get; init; } + public required string File { get; init; } + public required string Details { get; init; } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/MonitorSetup.cs b/tools/Azure.Mcp.Tools.Monitor/src/MonitorSetup.cs index ad9005e107..c6b1aec2e6 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/MonitorSetup.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/MonitorSetup.cs @@ -35,15 +35,41 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -67,6 +93,7 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } public CommandGroup RegisterCommands(IServiceProvider serviceProvider) @@ -152,6 +179,8 @@ public CommandGroup RegisterCommands(IServiceProvider serviceProvider) instrumentation.AddCommand(orchestratorNext.Name, orchestratorNext); var sendBrownfieldAnalysis = serviceProvider.GetRequiredService(); instrumentation.AddCommand(sendBrownfieldAnalysis.Name, sendBrownfieldAnalysis); + var sendEnhancedSelection = serviceProvider.GetRequiredService(); + instrumentation.AddCommand(sendEnhancedSelection.Name, sendEnhancedSelection); return monitor; } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/CommandOptions.cs b/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/CommandOptions.cs index 795ea503bd..a69759cd81 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/CommandOptions.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/CommandOptions.cs @@ -26,3 +26,10 @@ public sealed class SendBrownfieldAnalysisOptions public string? FindingsJson { get; set; } } + +public sealed class SendEnhancedSelectionOptions +{ + public string? SessionId { get; set; } + + public string? EnhancementKeys { get; set; } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/MonitorInstrumentationOptionDefinitions.cs b/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/MonitorInstrumentationOptionDefinitions.cs index 30905e37e0..e0c2574128 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/MonitorInstrumentationOptionDefinitions.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Options/Instrumentation/MonitorInstrumentationOptionDefinitions.cs @@ -41,13 +41,21 @@ public static class MonitorInstrumentationOptionDefinitions Description = """ JSON object with brownfield analysis findings. Required properties: - serviceOptions: Service options findings from analyzing AddApplicationInsightsTelemetry() call. Null if not found. - - initializers: Telemetry initializer findings from analyzing ITelemetryInitializer implementations. Null if none found. + - initializers: Telemetry initializer findings from analyzing ITelemetryInitializer or IConfigureOptions implementations. Null if none found. - processors: Telemetry processor findings from analyzing ITelemetryProcessor implementations. Null if none found. - clientUsage: TelemetryClient usage findings from analyzing direct TelemetryClient usage. Null if not found. - sampling: Custom sampling configuration findings. Null if no custom sampling. - telemetryPipeline: Custom ITelemetryChannel or TelemetrySinks usage findings. Null if not found. + - logging: Explicit logger provider and filter findings. Null if not found. For sections that do not exist in the codebase, pass an empty/default object (e.g. found: false, hasCustomSampling: false) rather than null. """, Required = true }; + + public const string EnhancementKeysName = "enhancement-keys"; + public static readonly Option EnhancementKeys = new($"--{EnhancementKeysName}") + { + Description = "One or more enhancement keys, comma-separated (e.g. 'redis', 'redis,processors', 'entityframework,otlp').", + Required = true + }; } diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/OrchestratorTool.cs b/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/OrchestratorTool.cs index f0a2e9e53a..be34c3524d 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/OrchestratorTool.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/OrchestratorTool.cs @@ -1,21 +1,12 @@ using System.Collections.Concurrent; using System.Text.Json; +using Azure.Mcp.Tools.Monitor.Generators; using Azure.Mcp.Tools.Monitor.Models; using Azure.Mcp.Tools.Monitor.Pipeline; using static Azure.Mcp.Tools.Monitor.Models.OnboardingConstants; namespace Azure.Mcp.Tools.Monitor.Tools; -/// -/// Single entry point for Azure Monitor instrumentation. -/// Controls the entire workflow server-side, eliminating LLM decision randomness. -/// -/// Flow: -/// 1. LLM calls orchestrator_start → gets first action with explicit instructions -/// 2. LLM executes EXACTLY what's returned -/// 3. LLM calls orchestrator_next → gets next action (or completion) -/// 4. Repeat until complete -/// public class OrchestratorTool { private readonly WorkspaceAnalyzer _analyzer; @@ -27,10 +18,6 @@ public OrchestratorTool(WorkspaceAnalyzer analyzer) _analyzer = analyzer; } - /// - /// Removes expired sessions to prevent unbounded memory growth. - /// Called opportunistically on Start and Next operations. - /// private static void CleanupExpiredSessions() { var now = DateTime.UtcNow; @@ -47,9 +34,22 @@ private static void CleanupExpiredSessions() public string Start(string workspacePath) { - var spec = _analyzer.Analyze(workspacePath); + OnboardingSpec spec; + try + { + spec = _analyzer.Analyze(workspacePath); + } + catch (Exception ex) + { + return Respond(new OrchestratorResponse + { + Status = "error", + Message = $"Analysis failed: {ex.GetType().Name}: {ex.Message}", + Instruction = "Tell the user about this error. Do not proceed.", + Warnings = [ex.StackTrace ?? "No stack trace available"] + }); + } - // Handle error cases if (spec.Decision.Intent == Intents.Error) { return Respond(new OrchestratorResponse @@ -61,15 +61,19 @@ public string Start(string workspacePath) }); } - // Handle brownfield — intercept before unsupported fallback + if (spec.Decision.Intent == Intents.Enhance) + { + return HandleEnhancementOffer(workspacePath, spec); + } + if (spec.Decision.Intent == Intents.Unsupported && spec.Analysis.State == InstrumentationState.Brownfield - && spec.Analysis.ExistingInstrumentation?.Type == InstrumentationType.ApplicationInsightsSdk) + && spec.Analysis.ExistingInstrumentation?.Type == InstrumentationType.ApplicationInsightsSdk + && spec.Analysis.ExistingInstrumentation?.IsTargetVersion != true) { return HandleBrownfieldAnalysis(workspacePath, spec.Analysis); } - // Handle unsupported cases if (spec.Decision.Intent == Intents.Unsupported) { return Respond(new OrchestratorResponse @@ -81,7 +85,6 @@ public string Start(string workspacePath) }); } - // Handle clarification needed if (spec.Decision.Intent == Intents.ClarificationNeeded) { return Respond(new OrchestratorResponse @@ -93,7 +96,6 @@ public string Start(string workspacePath) }); } - // No actions to execute if (spec.Actions.Count == 0) { return Respond(new OrchestratorResponse @@ -104,7 +106,6 @@ public string Start(string workspacePath) }); } - // Create session CleanupExpiredSessions(); var session = new ExecutionSession { @@ -115,7 +116,6 @@ public string Start(string workspacePath) }; Sessions[workspacePath] = session; - // Return first action var firstAction = spec.Actions[0]; var primaryProject = spec.Analysis.Projects.FirstOrDefault(); var appTypeDescription = primaryProject?.AppType.ToString() ?? "unknown"; @@ -125,16 +125,9 @@ public string Start(string workspacePath) Status = "in_progress", SessionId = workspacePath, Message = $"Instrumentation started for {spec.Analysis.Language} {appTypeDescription} application.", - - // Tell LLM exactly what to do Instruction = BuildInstruction(firstAction, spec.AgentMustExecuteFirst), - - // Provide action details for execution CurrentAction = firstAction, - - // Progress info Progress = $"Step 1 of {spec.Actions.Count}", - Warnings = spec.Warnings }); } @@ -153,7 +146,6 @@ public string Next(string sessionId, string completionNote) }); } - // Guard: cannot call next while still awaiting brownfield analysis if (session.State == SessionState.AwaitingAnalysis) { return Respond(new OrchestratorResponse @@ -165,9 +157,19 @@ public string Next(string sessionId, string completionNote) }); } + if (session.State == SessionState.AwaitingEnhancementSelection) + { + return Respond(new OrchestratorResponse + { + Status = "error", + SessionId = sessionId, + Message = "Enhancement selection is pending. Send selection first.", + Instruction = "Call send_enhanced_selection with the chosen enhancement keys before calling orchestrator_next." + }); + } + var spec = session.Spec!; - // Record completion and advance atomically var completedIndex = session.AdvanceIndex(); if (completedIndex >= spec.Actions.Count) { @@ -187,7 +189,6 @@ public string Next(string sessionId, string completionNote) session.CompletedActions.Add(completedAction.Id); var nextIndex = completedIndex + 1; - // Check if all done if (nextIndex >= spec.Actions.Count) { Sessions.TryRemove(sessionId, out _); @@ -202,7 +203,6 @@ public string Next(string sessionId, string completionNote) }); } - // Return next action var nextAction = spec.Actions[nextIndex]; return Respond(new OrchestratorResponse @@ -217,10 +217,6 @@ public string Next(string sessionId, string completionNote) }); } - /// - /// Handle brownfield detection by creating an AwaitingAnalysis session - /// and returning an analysis template for the LLM to fill. - /// private string HandleBrownfieldAnalysis(string workspacePath, Analysis analysis) { CleanupExpiredSessions(); @@ -248,20 +244,70 @@ private string HandleBrownfieldAnalysis(string workspacePath, Analysis analysis) }); } + private string HandleEnhancementOffer(string workspacePath, OnboardingSpec spec) + { + CleanupExpiredSessions(); + var session = new ExecutionSession + { + WorkspacePath = workspacePath, + Analysis = spec.Analysis, + State = SessionState.AwaitingEnhancementSelection, + Spec = spec, + CreatedAt = DateTime.UtcNow + }; + Sessions[workspacePath] = session; + + var sdkLabel = spec.Analysis.ExistingInstrumentation?.Type == InstrumentationType.AzureMonitorDistro + ? "Azure Monitor Distro" + : "Application Insights 3.x"; + + var version = spec.Analysis.ExistingInstrumentation?.Version; + var versionSuffix = version != null ? $" (v{version})" : string.Empty; + + var options = DotNetEnhancementGenerator.SupportedEnhancements + .Select(kv => new EnhancementOptionInfo { Key = kv.Key, DisplayName = kv.Value.DisplayName }) + .ToList(); + + return Respond(new OrchestratorResponse + { + Status = "enhancement_available", + SessionId = workspacePath, + Message = $"Already on {sdkLabel}{versionSuffix}. No migration needed. Choose an enhancement to add:", + Instruction = BuildEnhancementInstruction(), + EnhancementOptions = options + }); + } + + private static string BuildEnhancementInstruction() + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine("ENHANCEMENT SELECTION"); + sb.AppendLine(); + sb.AppendLine("The application is already on the latest SDK version. No migration is needed."); + sb.AppendLine("Present the enhancement options to the user and ask what they'd like to add."); + sb.AppendLine("The user may select one or more options. They can describe what they want in natural language."); + sb.AppendLine(); + sb.AppendLine("When the user has chosen, call send_enhanced_selection with the sessionId and the selected option key(s) as a comma-separated string (e.g. 'redis,processors')."); + sb.AppendLine("If the user asks for something not in the list, inform them it is not currently supported through MCP."); + return sb.ToString(); + } + private static string BuildAnalysisInstruction() { var sb = new System.Text.StringBuilder(); sb.AppendLine("BROWNFIELD ANALYSIS REQUIRED"); sb.AppendLine(); sb.AppendLine("Scan the workspace source files and fill in the analysis template provided in the 'analysisTemplate' field."); - sb.AppendLine("The template has 5 sections. Set any section to null if the concern does not exist in the codebase."); + sb.AppendLine("The template has 7 sections. For sections that do not exist in the codebase, pass an empty/default object (e.g. found: false) rather than null."); sb.AppendLine(); sb.AppendLine("Sections to analyze:"); - sb.AppendLine("1. serviceOptions — Find the AddApplicationInsightsTelemetry() call and report which options are configured"); - sb.AppendLine("2. initializers — Find all classes implementing ITelemetryInitializer and describe each one"); + sb.AppendLine("1. serviceOptions — Find the AddApplicationInsightsTelemetry() or AddApplicationInsightsTelemetryWorkerService() call, or for Console apps find TelemetryConfiguration.CreateDefault() / TelemetryConfiguration.Active usage, and report which options are configured"); + sb.AppendLine("2. initializers — Find all classes implementing ITelemetryInitializer AND any classes implementing IConfigureOptions (e.g. classes that call SetAzureTokenCredential for AAD auth). Also report any direct config.SetAzureTokenCredential() calls found in entry points (e.g. Program.cs, Global.asax.cs) as an initializer entry with purpose mentioning 'SetAzureTokenCredential for AAD auth'. Report all types here — describe each one with its purpose"); sb.AppendLine("3. processors — Find all classes implementing ITelemetryProcessor and describe each one"); - sb.AppendLine("4. clientUsage — Find all files that use TelemetryClient directly (injection, instantiation, or method calls)"); - sb.AppendLine("5. sampling — Find any custom sampling configuration"); + sb.AppendLine("4. clientUsage — Find all files that use TelemetryClient directly OR access Application Insights telemetry types (RequestTelemetry, DependencyTelemetry, TraceTelemetry, EventTelemetry, MetricTelemetry, ExceptionTelemetry, AvailabilityTelemetry) from HttpContext.Features, HttpContext.GetRequestTelemetry(), or via Microsoft.ApplicationInsights.DataContracts. For each file, list every Track*/GetMetric method name called (e.g. TrackEvent, TrackException, TrackPageView, TrackAvailability, GetMetric) — several have removed overloads in 3.x. Also note: (a) Features.Get() or GetRequestTelemetry() usage — in 3.x use Activity.Current with SetTag(); (b) manual construction of telemetry objects (new DependencyTelemetry(), new RequestTelemetry(), etc.) — these types still exist in 3.x but some properties changed; (c) type checks like 'telemetry is RequestTelemetry' in custom code outside initializers/processors"); + sb.AppendLine("5. sampling — Find any custom sampling configuration (e.g. custom ISamplingProcessor, .SetSampler(), or explicit TelemetryProcessorChainBuilder sampling setup). Do NOT report EnableAdaptiveSampling here — that is a service option handled in section 1"); + sb.AppendLine("6. telemetryPipeline — Find any custom ITelemetryChannel implementations, TelemetryConfiguration.TelemetryChannel assignments, or TelemetrySinks/DefaultTelemetrySink usage — all removed in 3.x"); + sb.AppendLine("7. logging — Find any explicit loggingBuilder.AddApplicationInsights() or services.AddLogging(b => b.AddApplicationInsights(...)) calls, and any AddFilter(...) log filter registrations — ApplicationInsightsLoggerProvider is removed in 3.x and logging is now automatic"); sb.AppendLine(); sb.AppendLine("When done, call send_brownfield_analysis with the sessionId and your filled findings JSON."); return sb.ToString(); @@ -272,30 +318,21 @@ private static AnalysisTemplate BuildAnalysisTemplate() return AnalysisTemplate.CreateDefault(); } - /// - /// Build explicit, unambiguous instructions for the LLM. - /// This is the key to reducing hallucination - tell it EXACTLY what to do. - /// private string BuildInstruction(OnboardingAction action, string? preInstruction) { return BuildInstructionPublic(action, preInstruction); } - /// - /// Public accessor for BuildInstruction, used by SendBrownfieldAnalysisTool. - /// internal static string BuildInstructionPublic(OnboardingAction action, string? preInstruction) { var instruction = new System.Text.StringBuilder(); - // Pre-instruction (e.g., read docs first) if (!string.IsNullOrEmpty(preInstruction)) { instruction.AppendLine($"FIRST: {preInstruction}"); instruction.AppendLine(); } - // Action-specific instructions instruction.AppendLine($"ACTION: {action.Description}"); instruction.AppendLine(); @@ -315,16 +352,16 @@ internal static string BuildInstructionPublic(OnboardingAction action, string? p break; case ActionType.AddPackage: - var pkg = action.Details.GetValueOrDefault("package", "")?.ToString(); - var project = action.Details.GetValueOrDefault("targetProject", "")?.ToString(); - var version = action.Details.GetValueOrDefault("version", "")?.ToString(); - var packageManager = action.Details.GetValueOrDefault("packageManager", "")?.ToString(); + var pkg = action.Details.GetValueOrDefault("package", string.Empty)?.ToString(); + var project = action.Details.GetValueOrDefault("targetProject", string.Empty)?.ToString(); + var version = action.Details.GetValueOrDefault("version", string.Empty)?.ToString(); + var packageManager = action.Details.GetValueOrDefault("packageManager", string.Empty)?.ToString(); if (string.IsNullOrWhiteSpace(pkg) || string.IsNullOrWhiteSpace(project)) { instruction.AppendLine("ERROR: Missing package or project information. Cannot proceed with this action."); break; } - instruction.AppendLine($"EXECUTE: Run this exact command:"); + instruction.AppendLine("EXECUTE: Run this exact command:"); var installCommand = packageManager?.ToLowerInvariant() switch { "pip" => !string.IsNullOrWhiteSpace(version) && version != "latest-stable" @@ -333,20 +370,28 @@ internal static string BuildInstructionPublic(OnboardingAction action, string? p "npm" => !string.IsNullOrWhiteSpace(version) && version != "latest-stable" ? $" npm install {pkg}@{version}" : $" npm install {pkg}", + "nuget-vs" => $" Install-Package {pkg}", _ => !string.IsNullOrWhiteSpace(version) && version != "latest-stable" ? $" dotnet add \"{project}\" package {pkg} --version {version}" : $" dotnet add \"{project}\" package {pkg}" }; instruction.AppendLine(installCommand); + if (packageManager?.ToLowerInvariant() == "nuget-vs") + { + instruction.AppendLine(); + instruction.AppendLine("ASK THE USER to run this command in the Package Manager Console (View → Other Windows → Package Manager Console) or install via the NuGet Package Manager UI (right-click project → Manage NuGet Packages)."); + instruction.AppendLine("The agent cannot run this command — it requires the Package Manager Console which is separate from the developer terminal."); + instruction.AppendLine("Wait for the user to confirm the package is installed, then call orchestrator_next to continue."); + } instruction.AppendLine(); instruction.AppendLine("Wait for the command to complete successfully."); break; case ActionType.ModifyCode: - var file = action.Details.GetValueOrDefault("file", "")?.ToString(); - var snippet = action.Details.GetValueOrDefault("codeSnippet", "")?.ToString(); - var insertAfter = action.Details.GetValueOrDefault("insertAfter", "")?.ToString(); - var usingStmt = action.Details.GetValueOrDefault("requiredUsing", "")?.ToString(); + var file = action.Details.GetValueOrDefault("file", string.Empty)?.ToString(); + var snippet = action.Details.GetValueOrDefault("codeSnippet", string.Empty)?.ToString(); + var insertAfter = action.Details.GetValueOrDefault("insertAfter", string.Empty)?.ToString(); + var usingStmt = action.Details.GetValueOrDefault("requiredUsing", string.Empty)?.ToString(); if (string.IsNullOrWhiteSpace(file) || string.IsNullOrWhiteSpace(snippet)) { instruction.AppendLine("ERROR: Missing file path or code snippet. Cannot proceed with this action."); @@ -356,7 +401,7 @@ internal static string BuildInstructionPublic(OnboardingAction action, string? p instruction.AppendLine(); if (!string.IsNullOrWhiteSpace(usingStmt)) { - instruction.AppendLine($"1. Add this using statement at the top:"); + instruction.AppendLine("1. Add this using statement at the top:"); instruction.AppendLine($" using {usingStmt};"); instruction.AppendLine(); instruction.AppendLine($"2. Add this code IMMEDIATELY after the line containing '{insertAfter}':"); @@ -371,18 +416,28 @@ internal static string BuildInstructionPublic(OnboardingAction action, string? p break; case ActionType.AddConfig: - var configFile = action.Details.GetValueOrDefault("file", "")?.ToString(); - var jsonPath = action.Details.GetValueOrDefault("jsonPath", "")?.ToString(); - var value = action.Details.GetValueOrDefault("value", "")?.ToString(); - var envVar = action.Details.GetValueOrDefault("envVarAlternative", "")?.ToString(); + var configFile = action.Details.GetValueOrDefault("file", string.Empty)?.ToString(); + var jsonPath = action.Details.GetValueOrDefault("jsonPath", string.Empty)?.ToString(); + var value = action.Details.GetValueOrDefault("value", string.Empty)?.ToString(); + var envVar = action.Details.GetValueOrDefault("envVarAlternative", string.Empty)?.ToString(); if (string.IsNullOrWhiteSpace(configFile) || string.IsNullOrWhiteSpace(jsonPath)) { - instruction.AppendLine("ERROR: Missing configuration file or JSON path. Cannot proceed with this action."); + instruction.AppendLine("ERROR: Missing configuration file or config path. Cannot proceed with this action."); break; } instruction.AppendLine($"EXECUTE: Add configuration to {configFile}"); instruction.AppendLine(); - instruction.AppendLine($"Add this JSON property: \"{jsonPath}\": \"{value}\""); + if (configFile.EndsWith(".config", StringComparison.OrdinalIgnoreCase)) + { + // XML config (ApplicationInsights.config, Web.config) + instruction.AppendLine($"Set the <{jsonPath}> element value to \"{value}\" in the XML file."); + instruction.AppendLine($"Example: <{jsonPath}>{value}"); + } + else + { + // JSON config (appsettings.json) + instruction.AppendLine($"Add this JSON property: \"{jsonPath}\": \"{value}\""); + } if (!string.IsNullOrWhiteSpace(envVar)) { instruction.AppendLine(); @@ -391,7 +446,7 @@ internal static string BuildInstructionPublic(OnboardingAction action, string? p break; case ActionType.ManualStep: - var manualInstructions = action.Details.GetValueOrDefault("instructions", "")?.ToString(); + var manualInstructions = action.Details.GetValueOrDefault("instructions", string.Empty)?.ToString(); if (string.IsNullOrWhiteSpace(manualInstructions)) { instruction.AppendLine("ERROR: Missing manual step instructions. Cannot proceed with this action."); @@ -411,10 +466,6 @@ internal static string BuildInstructionPublic(OnboardingAction action, string? p return instruction.ToString(); } - /// - /// Build a dynamic completion instruction based on what was actually done, - /// instead of hardcoding .NET-specific text. - /// private static string BuildCompletionInstruction(OnboardingSpec spec) { var sb = new System.Text.StringBuilder(); @@ -459,9 +510,8 @@ private static string Respond(OrchestratorResponse response) internal enum SessionState { - /// Brownfield analysis template sent, waiting for LLM to submit findings AwaitingAnalysis, - /// Normal step-through of actions (greenfield or post-analysis brownfield) + AwaitingEnhancementSelection, Executing } @@ -478,56 +528,27 @@ internal class ExecutionSession public int CurrentActionIndex => _currentActionIndex; - /// - /// Atomically advances the action index. Returns the previous index. - /// public int AdvanceIndex() => Interlocked.Increment(ref _currentActionIndex) - 1; } +internal sealed record EnhancementOptionInfo +{ + public required string Key { get; init; } + public required string DisplayName { get; init; } +} + internal record OrchestratorResponse { - /// - /// Status: "in_progress", "complete", "error", "unsupported", "clarification_needed", "analysis_needed" - /// public required string Status { get; init; } - public string? SessionId { get; init; } - - /// - /// Human-readable message about current state - /// public required string Message { get; init; } - - /// - /// EXPLICIT instruction for what the LLM must do next. - /// This is the key to reducing hallucination - tell it exactly what to do. - /// public required string Instruction { get; init; } - - /// - /// The current action details (if in_progress) - /// public OnboardingAction? CurrentAction { get; init; } - - /// - /// Progress indicator: "Step 2 of 4" - /// public string? Progress { get; init; } - - /// - /// Actions already completed - /// public List? CompletedActions { get; init; } - - /// - /// Any warnings to relay to user - /// public List? Warnings { get; init; } - - /// - /// Brownfield analysis template (only present when status is "analysis_needed") - /// public AnalysisTemplate? AnalysisTemplate { get; init; } + public List? EnhancementOptions { get; init; } } #endregion diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendBrownfieldAnalysisTool.cs b/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendBrownfieldAnalysisTool.cs index fe0998ed18..45dd719616 100644 --- a/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendBrownfieldAnalysisTool.cs +++ b/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendBrownfieldAnalysisTool.cs @@ -24,7 +24,8 @@ public string Submit( ProcessorFindings? processors, ClientUsageFindings? clientUsage, SamplingFindings? sampling, - TelemetryPipelineFindings? telemetryPipeline) + TelemetryPipelineFindings? telemetryPipeline, + LoggingFindings? logging) { if (!OrchestratorTool.Sessions.TryGetValue(sessionId, out var session)) { @@ -54,7 +55,8 @@ public string Submit( Processors = processors, ClientUsage = clientUsage, Sampling = sampling, - TelemetryPipeline = telemetryPipeline + TelemetryPipeline = telemetryPipeline, + Logging = logging }; // Store findings and attach to analysis for generator diff --git a/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendEnhancedSelectionTool.cs b/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendEnhancedSelectionTool.cs new file mode 100644 index 0000000000..013659c3ce --- /dev/null +++ b/tools/Azure.Mcp.Tools.Monitor/src/Tools/Instrumentation/SendEnhancedSelectionTool.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using Azure.Mcp.Tools.Monitor.Generators; +using Azure.Mcp.Tools.Monitor.Models; + +namespace Azure.Mcp.Tools.Monitor.Tools; + +public class SendEnhancedSelectionTool +{ + public string Send(string sessionId, string enhancementKeys) + { + if (!OrchestratorTool.Sessions.TryGetValue(sessionId, out var session)) + { + return Respond(new OrchestratorResponse + { + Status = "error", + Message = "No active session. Call orchestrator_start first.", + Instruction = "Call orchestrator_start with the workspace path to begin." + }); + } + + if (session.State != SessionState.AwaitingEnhancementSelection) + { + return Respond(new OrchestratorResponse + { + Status = "error", + SessionId = sessionId, + Message = "Session is not awaiting enhancement selection. This tool is only valid after orchestrator_start returns 'enhancement_available'.", + Instruction = "Call orchestrator_next to continue with the current session." + }); + } + + var keys = enhancementKeys + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + var selections = new List<(string key, EnhancementOption option)>(); + foreach (var key in keys) + { + if (!DotNetEnhancementGenerator.SupportedEnhancements.TryGetValue(key, out var option)) + { + var validKeys = string.Join(", ", DotNetEnhancementGenerator.SupportedEnhancements.Keys); + return Respond(new OrchestratorResponse + { + Status = "error", + SessionId = sessionId, + Message = $"Unknown enhancement key: '{key}'. This enhancement is not currently supported through MCP.", + Instruction = $"Valid options are: {validKeys}. Ask the user to choose from the supported list." + }); + } + + selections.Add((key, option)); + } + + var spec = DotNetEnhancementGenerator.GenerateForSelections(session.Analysis, selections); + session.Spec = spec; + session.State = SessionState.Executing; + + if (spec.Actions.Count == 0) + { + var names = string.Join(", ", selections.Select(s => s.option.DisplayName)); + return Respond(new OrchestratorResponse + { + Status = "complete", + SessionId = sessionId, + Message = $"{names} - no actions needed.", + Instruction = "Inform the user." + }); + } + + var firstAction = spec.Actions[0]; + var displayNames = string.Join(" + ", selections.Select(s => s.option.DisplayName)); + + return Respond(new OrchestratorResponse + { + Status = "in_progress", + SessionId = sessionId, + Message = $"Enhancement plan generated: {displayNames}.", + Instruction = OrchestratorTool.BuildInstructionPublic(firstAction, spec.AgentMustExecuteFirst), + CurrentAction = firstAction, + Progress = $"Step 1 of {spec.Actions.Count}" + }); + } + + private static string Respond(OrchestratorResponse response) + { + return JsonSerializer.Serialize(response, OnboardingJsonContext.Default.OrchestratorResponse); + } +} diff --git a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.UnitTests/Instrumentation/Tools/SendBrownfieldAnalysisToolTests.cs b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.UnitTests/Instrumentation/Tools/SendBrownfieldAnalysisToolTests.cs index e5cfe5dbe2..eaaa888609 100644 --- a/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.UnitTests/Instrumentation/Tools/SendBrownfieldAnalysisToolTests.cs +++ b/tools/Azure.Mcp.Tools.Monitor/tests/Azure.Mcp.Tools.Monitor.UnitTests/Instrumentation/Tools/SendBrownfieldAnalysisToolTests.cs @@ -25,6 +25,7 @@ public void Submit_WithoutActiveSession_ReturnsError() null, null, null, + null, null)); // Assert @@ -54,6 +55,7 @@ public void Submit_WhenSessionIsNotAwaitingAnalysis_ReturnsError() null, null, null, + null, null)); // Assert @@ -83,6 +85,7 @@ public void Submit_WhenAwaitingAnalysisAndNoMatchingGenerator_ReturnsUnsupported null, null, null, + null, null)); // Assert @@ -112,7 +115,8 @@ public void Submit_WhenAwaitingAnalysisAndGeneratorMatches_ReturnsInProgress() new ProcessorFindings { Found = false }, new ClientUsageFindings { DirectUsage = false }, new SamplingFindings { HasCustomSampling = false }, - new TelemetryPipelineFindings { Found = false })); + new TelemetryPipelineFindings { Found = false }, + null)); // Assert Assert.Equal("in_progress", response.GetProperty("status").GetString());