diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000000..b0450e11dfe --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/dotnet/.devcontainer/base.Dockerfile + +ARG VARIANT="6.0-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:0-${VARIANT} + +RUN apt update && export DEBIAN_FRONTEND=noninteractive \ + && apt install -y --no-install-recommends cmake clang llvm + +ARG INSTALL_NODE="true" +ARG INSTALL_AZURITE="true" +ARG NODE_VERSION="lts/*" + +RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi +RUN if [ "${INSTALL_AZURITE}" = "true" ]; then npm install -g azurite; fi diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..1d2f3945fd2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,90 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "C# (.NET)", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "6.0-bullseye", + "INSTALL_NODE": "true", + "INSTALL_AZURITE": "true", + "NODE_VERSION": "lts/*" + } + }, + "hostRequirements": { + "cpus": 4 + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-dotnettools.csharp", + "EditorConfig.EditorConfig", + "ms-vscode.powershell", + "tintoy.msbuild-project-tools", + "streetsidesoftware.code-spell-checker" + ], + "settings": { + "files.associations": { + "*.csproj": "msbuild", + "*.targets": "msbuild", + "*.vbproj": "msbuild", + "*.props": "msbuild", + "*.resx": "xml" + }, + + // ms-dotnettools.csharp settings + "csharp.format.enable": true, + "csharp.semanticHighlighting.enabled": true, + + // ms-dotnettools.csharp settings + "omnisharp.path": "latest", + "omnisharp.defaultLaunchSolution": "dotnet-monitor.sln", + "omnisharp.disableMSBuildDiagnosticWarning": true, + "omnisharp.useModernNet": true, + "omnisharp.enableAsyncCompletion": true, + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableImportCompletion": true, + "omnisharp.enableRoslynAnalyzers": true, + "omnisharp.organizeImportsOnFormat": true, + "omnisharp.autoStart": true, + + // ms-vscode.powershell settings + "powershell.promptToUpdatePowerShell": false, + "powershell.integratedConsole.showOnStartup": false, + "powershell.startAutomatically": false + } + } + }, + "postCreateCommand": "bash -i ${containerWorkspaceFolder}/.devcontainer/scripts/container-creation.sh", + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/azure-cli:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/powershell:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/docker-in-docker:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { + "version": "latest" + } + }, + "remoteEnv": { + "PATH": "${containerWorkspaceFolder}/.dotnet:${containerEnv:PATH}", + "DOTNET_ROOT": "${containerWorkspaceFolder}/.dotnet", + "DOTNET_MULTILEVEL_LOOKUP": "0" + }, + "portsAttributes": { + "52323": { + "label": "dotnet-monitor" + }, + "52325": { + "label": "dotnet-monitor: metrics" + } + } +} diff --git a/.devcontainer/scripts/container-creation.sh b/.devcontainer/scripts/container-creation.sh new file mode 100755 index 00000000000..6d24765d6e9 --- /dev/null +++ b/.devcontainer/scripts/container-creation.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +# Install SDK and tool dependencies before container starts +# Also run the full restore on the repo so that go-to definition +# and other language features will be available in C# files +./restore.sh -ci + +# Add the .NET dev certs by default for dotnet-monitor's usage on launch. +# Do **NOT** do this in base images. +dotnet dev-certs https + +# The container creation script is executed in a new Bash instance +# so we exit at the end to avoid the creation process lingering. +exit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..57e5950d4c0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,239 @@ +root = true + +#### Default Core EditorConfig Options #### +[*] +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# C# files +[*.cs] + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +dotnet_diagnostic.IDE0005.severity = warning +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true +csharp_style_prefer_parameter_null_checking = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bff7aaf0579..1c89d970d51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,9 +1,7 @@ # Users referenced in this file will automatically be requested as reviewers for PRs that modify the given paths. # See https://help.github.com/articles/about-code-owners/ - -# We should just make everything require review of at least one team member. -# However, we need /eng/ and global.json excluded for automatic updates -# but there's no include syntax. At least make sure source and documentation -# have proper ownership. -/src/ @dotnet/dotnet-monitor -/documentation/ @dotnet/dotnet-monitor @IEvangelist +* @dotnet/dotnet-monitor +# We need /eng/ and global.json excluded for automatic updates +/eng/ +global.json +/eng/dependabot @dotnet/dotnet-monitor diff --git a/.github/actions/AppendToFile/action.yml b/.github/actions/AppendToFile/action.yml new file mode 100644 index 00000000000..c85ab137d7e --- /dev/null +++ b/.github/actions/AppendToFile/action.yml @@ -0,0 +1,15 @@ +name: 'AppendToFile Action' +description: 'Appends text to a file (if it is not already there)' +inputs: + textToSearch: + description: 'The required text in a file' + required: true + textToAdd: + description: 'The required text to add to a file (if not present)' + required: true + paths: + description: 'Paths to the changed files' + required: false +runs: + using: 'node16' + main: 'index.js' diff --git a/.github/actions/AppendToFile/index.js b/.github/actions/AppendToFile/index.js new file mode 100644 index 00000000000..ea491ac3ab9 --- /dev/null +++ b/.github/actions/AppendToFile/index.js @@ -0,0 +1,60 @@ +const util = require("util"); +const fs = require('fs'); +const path = require('path') + +async function main() { + + const jsExec = util.promisify(require("child_process").exec); + + console.log("Installing npm dependencies"); + const { stdout, stderr } = await jsExec("npm install @actions/core"); + console.log("npm-install stderr:\n\n" + stderr); + console.log("npm-install stdout:\n\n" + stdout); + console.log("Finished installing npm dependencies"); + + const core = require('@actions/core'); + + try { + const textToSearch = core.getInput('textToSearch', { required: true }); + const textToAdd = core.getInput('textToAdd', { required: true }); + const paths = core.getInput('paths', {required: false}); + + const insertFileNameParameter = "{insertFileName}"; + + if (paths === null || paths.trim() === "") + { + return; + } + + console.log("Paths: " + paths); + + for (const currPath of paths.split(',')) { + fs.readFile(currPath, (err, content) => { + if (err) + { + console.log(err); + } + + if (content && !content.includes(textToSearch)) + { + var updatedTextToAdd = textToAdd; + if (textToAdd.includes(insertFileNameParameter)) + { + const parsedPath = path.parse(currPath); + const encodedURIWithoutExtension = encodeURIComponent(path.join(parsedPath.dir, parsedPath.name)) + updatedTextToAdd = textToAdd.replace(insertFileNameParameter, encodedURIWithoutExtension); + } + + var contentStr = updatedTextToAdd + "\n\n" + content.toString(); + + fs.writeFile(currPath, contentStr, (err) => {}); + } + }); + } + } catch (error) { + core.setFailed(error.message); + } +} + +// Call the main function to run the action +main(); diff --git a/.github/actions/generate-release-notes/action.yml b/.github/actions/generate-release-notes/action.yml new file mode 100644 index 00000000000..4468f00cae8 --- /dev/null +++ b/.github/actions/generate-release-notes/action.yml @@ -0,0 +1,25 @@ +name: 'Generate release notes' +description: 'Generate release notes' +inputs: + auth_token: + description: 'The token used to authenticate to GitHub.' + required: true + output: + description: 'The output file to save the release notes to.' + required: true + build_description: + description: 'Build description.' + required: true + last_release_date: + description: 'Last release date in ISO 8601 format.' + required: true + branch_name: + description: 'The branch to generate release notes for.' + required: true + additional_branch: + description: "Include PRs from an additional branch" + required: false + +runs: + using: 'node16' + main: 'index.js' diff --git a/.github/actions/generate-release-notes/index.js b/.github/actions/generate-release-notes/index.js new file mode 100644 index 00000000000..16d29328967 --- /dev/null +++ b/.github/actions/generate-release-notes/index.js @@ -0,0 +1,171 @@ +const fs = require('fs'); +const path = require('path'); +const util = require('util'); +const jsExec = util.promisify(require("child_process").exec); +const readFile = (fileName) => util.promisify(fs.readFile)(fileName, 'utf8'); +const writeFile = (fileName, contents) => util.promisify(fs.writeFile)(fileName, contents); + +const UpdateReleaseNotesLabel = "update-release-notes"; +const BackportLabel = "backport"; + +async function run() { + console.log("Installing npm dependencies"); + const { stdout, stderr } = await jsExec("npm install @actions/core @actions/github"); + console.log("npm-install stderr:\n\n" + stderr); + console.log("npm-install stdout:\n\n" + stdout); + console.log("Finished installing npm dependencies"); + + const github = require('@actions/github'); + const core = require('@actions/core'); + + const octokit = github.getOctokit(core.getInput("auth_token", { required: true })); + + const output = core.getInput("output", { required: true }); + const buildDescription = core.getInput("build_description", { required: true }); + const lastReleaseDate = core.getInput("last_release_date", { required: true }); + const branch = core.getInput("branch_name", { required: true }); + const additional_branch = core.getInput("additional_branch", { required: false }); + + const repoOwner = github.context.payload.repository.owner.login; + const repoName = github.context.payload.repository.name; + + try { + const changelog = await generateChangelog(octokit, branch, additional_branch, repoOwner, repoName, lastReleaseDate, + [ + { + labelName: "breaking-change", + moniker: "⚠️" + }, + { + labelName: "experimental-feature", + moniker: "🔬" + } + ]); + + const releaseNotes = await generateReleaseNotes(path.join(__dirname, "releaseNotes.template.md"), buildDescription, changelog); + await writeFile(output, releaseNotes); + } catch (error) { + core.setFailed(error); + } +} + +async function generateChangelog(octokit, branchName, additionalBranch, repoOwner, repoName, minMergeDate, significantLabels) { + let prs = await getPRs(octokit, branchName, additionalBranch, repoOwner, repoName, minMergeDate, UpdateReleaseNotesLabel); + + // Resolve the backport PRs to their origin PRs + const maxRecursion = 3; + const backportPrs = await getPRs(octokit, branchName, additionalBranch, repoOwner, repoName, minMergeDate, BackportLabel); + for (const pr of backportPrs) { + const originPr = await resolveBackportPrToReleaseNotePr(octokit, pr, repoOwner, repoName, minMergeDate, maxRecursion); + if (originPr !== undefined) { + // Patch the origin PR information to have the backport PR number and URL + // so that the release notes links to the backport, but grabs the rest of + // the information from the origin PR. + originPr.number = pr.number; + originPr.html_url = pr.html_url; + prs.push(originPr); + } + } + + let changelog = []; + for (const pr of prs) { + let labelIndicesSeen = []; + + for (const label of pr.labels) + { + for(let i = 0; i < significantLabels.length; i++){ + if (label.name === significantLabels[i].labelName) { + labelIndicesSeen.push(i); + break; + } + } + } + + let entry = "-"; + for (const index of labelIndicesSeen) { + entry += ` ${significantLabels[index].moniker}`; + } + + const changelogRegex=/^###### Release Notes Entry\r?\n(?.*)/m + const userDefinedChangelogEntry = pr.body?.match(changelogRegex)?.groups?.releaseNotesEntry; + if (userDefinedChangelogEntry !== undefined) { + entry += ` ${userDefinedChangelogEntry}` + } else { + entry += ` ${pr.title}` + } + + entry += ` ([#${pr.number}](${pr.html_url}))` + + changelog.push(entry); + } + + return changelog.join("\n"); +} + +async function generateReleaseNotes(templatePath, buildDescription, changelog) { + let releaseNotes = await readFile(templatePath); + releaseNotes = releaseNotes.replace("${buildDescription}", buildDescription); + releaseNotes = releaseNotes.replace("${changelog}", changelog); + + return releaseNotes; +} + +async function getPRs(octokit, branchName, additionalBranch, repoOwner, repoName, minMergeDate, labelFilter) { + let searchQuery = `is:pr is:merged label:${labelFilter} repo:${repoOwner}/${repoName} base:${branchName} merged:>=${minMergeDate}`; + if (additionalBranch !== undefined) { + searchQuery += ` base:${additionalBranch}` + } + console.log(searchQuery); + + return await octokit.paginate(octokit.rest.search.issuesAndPullRequests, { + q: searchQuery, + sort: "created", + order: "desc" + }); +} + +async function resolveBackportPrToReleaseNotePr(octokit, pr, repoOwner, repoName, minMergeDate, maxRecursion) { + const backportRegex=/^Backport of #(?\d+) to/m + const backportOriginPrNumber = pr.body?.match(backportRegex)?.groups?.prNumber; + if (backportOriginPrNumber === undefined) { + console.log(`Unable to determine origin PR for backport: ${pr.html_url}`) + return undefined; + } + + const originPr = (await octokit.rest.pulls.get({ + owner: repoOwner, + repo: repoName, + pull_number: backportOriginPrNumber + }))?.data; + + if (originPr === undefined) { + console.log(`Unable to find origin PR for backport: ${pr.html_url}`); + return undefined; + } + + console.log(`Mapped PR #${pr.number} as a backport of #${originPr.number}`) + + let originIsBackport = false; + for (const label of originPr.labels) + { + if (label.name === UpdateReleaseNotesLabel) { + console.log(`--> Mentioning in release notes`) + return originPr; + } + + if (label.name === BackportLabel) { + originIsBackport = true; + // Keep searching incase there is also an update-release-notes label + } + } + + if (originIsBackport) { + if (maxRecursion > 0) { + return await resolveBackportPr(octokit, originPr, repoOwner, repoName, minMergeDate, maxRecursion - 1); + } + } + + return undefined; +} + +run(); diff --git a/.github/actions/generate-release-notes/releaseNotes.template.md b/.github/actions/generate-release-notes/releaseNotes.template.md new file mode 100644 index 00000000000..a53b3cf7d24 --- /dev/null +++ b/.github/actions/generate-release-notes/releaseNotes.template.md @@ -0,0 +1,6 @@ +Today we are releasing the ${buildDescription} of the `dotnet monitor` tool. This release includes: + +${changelog} + +\*🔬 **_indicates an experimental feature_** \ +\*⚠️ **_indicates a breaking change_** \ No newline at end of file diff --git a/.github/actions/open-pr/action.yml b/.github/actions/open-pr/action.yml new file mode 100644 index 00000000000..8f0beb52d78 --- /dev/null +++ b/.github/actions/open-pr/action.yml @@ -0,0 +1,59 @@ +name: 'Open PR' +description: 'Opens a PR targeting the currently checked out out branch.' +inputs: + branch_name: + description: 'The branch name to create. Will be prefixed with "bot/".' + required: true + title: + description: 'The PR title.' + required: true + files_to_commit: + description: 'The files to commit.' + required: true + commit_message: + description: 'The commit message.' + required: true + body: + description: 'The PR body.' + required: true + labels: + description: 'Labels to add to the PR, comma separated.' + required: false + fail_if_files_unchanged: + description: 'Fails the action if all of the specified files_to_commit are unchanged.' + required: false + auth_token: + description: 'The token used to authenticate to GitHub.' + required: true + +runs: + using: "composite" + steps: + - name: Open PR + run: | + current_branch_name=$(git symbolic-ref --short HEAD) + pr_branch_name=$(echo "bot/${{ inputs.branch_name }}" ) + + git config user.name "github-actions" + git config user.email "github-actions@github.com" + git checkout -b "$pr_branch_name" + git add ${{ inputs.files_to_commit }} + + are_files_changed="" + git diff --name-only --cached --exit-code || are_files_changed="1" + if [ "$are_files_changed" != "1" ]; then + echo "No files changed, nothing to do." + if [ "${{ inputs.fail_if_files_unchanged }}" == "true" ]; then + exit 1 + fi + + exit 0 + fi + + git commit -m "${{ inputs.commit_message }}" + git push --force --set-upstream origin "HEAD:$pr_branch_name" + + gh pr create --repo "${{ github.repository }}" --base "$current_branch_name" -t "${{ inputs.title }}" -b "${{ inputs.body }}" -l "${{ inputs.labels }}" + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.auth_token }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..ad5df8dc789 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,27 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + target-branch: "main" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + target-branch: "release/7.x" + - package-ecosystem: "nuget" + directory: "/eng/dependabot" + schedule: + interval: "daily" + target-branch: "main" + - package-ecosystem: "nuget" + directory: "/eng/dependabot" + schedule: + interval: "daily" + target-branch: "release/6.x" + - package-ecosystem: "nuget" + directory: "/eng/dependabot" + schedule: + interval: "daily" + target-branch: "release/7.x" diff --git a/.github/fabricbot.json b/.github/fabricbot.json new file mode 100644 index 00000000000..2d4a9c1643e --- /dev/null +++ b/.github/fabricbot.json @@ -0,0 +1,373 @@ +{ + "version": "1.0", + "tasks": [ + { + "taskType": "trigger", + "capabilityId": "AutoMerge", + "subCapability": "AutoMerge", + "version": "1.0", + "config": { + "taskName": "Automatically merge pull requests", + "label": "auto-merge", + "silentMode": false, + "minMinutesOpen": "1", + "mergeType": "squash", + "deleteBranches": true, + "allowAutoMergeInstructionsWithoutLabel": true, + "enforceDMPAsStatus": true, + "removeLabelOnPush": true, + "usePrDescriptionAsCommitMessage": false, + "conditionalMergeTypes": [], + "requireAllStatuses": true, + "requireSpecificCheckRuns": true, + "requireSpecificCheckRunsList": [ + "dotnet-monitor-ci" + ], + "minimumNumberOfStatuses": 1, + "minimumNumberOfCheckRuns": 1, + "requireAllStatuses_exemptList": [ + "codecov", + "Dependabot", + "DotNet Maestro", + "DotNet Maestro - Int" + ] + }, + "id": "wey6YCjmXB1" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "opened" + } + }, + { + "name": "titleContains", + "parameters": { + "titlePattern": "Update dependencies" + } + }, + { + "name": "isActivitySender", + "parameters": { + "user": "dotnet-maestro[bot]" + } + } + ] + }, + "eventType": "pull_request", + "eventNames": [ + "pull_request", + "issues", + "project_card" + ], + "taskName": "Auto approve dependencies", + "actions": [ + { + "name": "approvePullRequest", + "parameters": { + "comment": "Automatically approving dependency update." + } + }, + { + "name": "addLabel", + "parameters": { + "label": "automatic-pr" + } + } + ] + }, + "id": "PUcyzKaH6va" + }, + { + "taskType": "trigger", + "capabilityId": "PrAutoLabel", + "subCapability": "Path", + "version": "1.0", + "config": { + "taskName": "Add tags (paths)", + "configs": [ + { + "label": "dependencies", + "pathFilter": [ + "global.json", + "eng/Versions.props", + "eng/Version.Details.xml" + ] + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 1, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 2, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 3, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 4, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 5, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 6, + "hours": [ + 0 + ], + "timezoneOffset": -7 + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "isPr", + "parameters": {} + }, + { + "name": "noActivitySince", + "parameters": { + "days": 28 + } + }, + { + "name": "noLabel", + "parameters": { + "label": "no-recent-activity" + } + } + ], + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "no-recent-activity" + } + }, + { + "name": "addReply", + "parameters": { + "comment": "The 'no-recent-activity' label has been added to this pull request due to four weeks without any activity. If there is no activity in the next six weeks, this pull request will automatically be closed. You can learn more about our stale PR policy here: https://github.com/dotnet/dotnet-monitor/blob/main/CONTRIBUTING.md#stale-pr-policy" + } + } + ], + "taskName": "No Recent Activity PR Search" + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 1, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 2, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 3, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 4, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 5, + "hours": [ + 0 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 6, + "hours": [ + 0 + ], + "timezoneOffset": -7 + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "isPr", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "no-recent-activity" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 42 + } + } + ], + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ], + "taskName": "Close No Recent Activity PR" + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "hasLabel", + "parameters": { + "label": "no-recent-activity" + } + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "operator": "or", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "reopened" + } + }, + { + "name": "isAction", + "parameters": { + "action": "synchronize" + } + }, + { + "name": "isAction", + "parameters": { + "action": "edited" + } + }, + { + "name": "isAction", + "parameters": { + "action": "review_requested" + } + } + ] + }, + { + "operator": "not", + "operands": [ + { + "name": "isActivitySender", + "parameters": { + "user": "msftbot" + } + } + ] + } + ] + }, + "eventType": "pull_request", + "eventNames": [ + "pull_request", + "issues", + "project_card" + ], + "taskName": "Remove No Recent Activity Label", + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "no-recent-activity" + } + } + ] + } + } + ], + "userGroups": [] +} diff --git a/.github/linters/check-markdown-links-config.json b/.github/linters/check-markdown-links-config.json new file mode 100644 index 00000000000..7d95404386c --- /dev/null +++ b/.github/linters/check-markdown-links-config.json @@ -0,0 +1,18 @@ +{ + "ignorePatterns": [ + { + "pattern": "^https?://localhost:.*" + }, + { + "pattern": "^https://dev\\.azure\\.com/dnceng/internal/.*" + }, + { + "pattern": "^https://pkgs\\.dev\\.azure\\.com/dnceng/public/_packaging/.*" + } + ], + "aliveStatusCodes": [ + 0, + 200, + 203 + ] +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..310b36b3038 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +###### Summary + + + +###### Release Notes Entry diff --git a/.github/workflows/add-markdown-feedback.yml b/.github/workflows/add-markdown-feedback.yml new file mode 100644 index 00000000000..a59238758fc --- /dev/null +++ b/.github/workflows/add-markdown-feedback.yml @@ -0,0 +1,51 @@ +name: 'Add Markdown Feedback' +on: + pull_request: + paths: ['documentation/**.md'] + branches: ['main'] + +permissions: + pull-requests: read + +jobs: + add-markdown-feedback: + name: 'Add Markdown Feedback' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get base commit for the PR + run: | + git fetch origin ${{ github.base_ref }} + echo "base_sha=$(git rev-parse origin/${{ github.base_ref }})" >> $GITHUB_ENV + echo "Merging ${{ github.sha }} into ${{ github.base_ref }}" + + - name: Get changed files + run: | + echo "Files changed: '$(git diff-tree --no-commit-id --name-only -r ${{ env.base_sha }} ${{ github.sha }})'" + changed_source_files=$(git diff-tree --no-commit-id --name-only -r ${{ env.base_sha }} ${{ github.sha }} -- documentation ':!documentation/releaseNotes/*' | { grep "**.md$" || test $? = 1; }) + echo "Files to validate: '${changed_source_files}'" + changed_source_files=$(echo ${changed_source_files} | sed 's/ documentation/,documentation/g') + echo "updated_files=$(echo ${changed_source_files})" >> $GITHUB_ENV + + - name: Append To File + uses: ./.github/actions/AppendToFile + with: + textToSearch: 'DGDQWXH' + textToAdd: '### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src={insertFileName})' + paths: ${{ env.updated_files }} + + - name: Generate artifacts + run: | + mkdir -p ./pr + cp $GITHUB_EVENT_PATH ./pr/pr-event.json + echo -n $GITHUB_EVENT_NAME > ./pr/pr-event-name + git diff > ./pr/linter.diff + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: pr-linter + path: pr/ diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 7c778ffaceb..4aafe7d2ac2 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Extract backport target branch - uses: actions/github-script@v3 + uses: actions/github-script@v6.3.2 id: target-branch-extractor with: result-encoding: string @@ -28,18 +28,18 @@ jobs: return target_branch[1]; - name: Post backport started comment to pull request - uses: actions/github-script@v3 + uses: actions/github-script@v6.3.2 with: script: | const backport_start_body = `Started backporting to ${{ steps.target-branch-extractor.outputs.result }}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; - await github.issues.createComment({ + await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: backport_start_body }); - name: Checkout repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - name: Run backport @@ -47,3 +47,4 @@ jobs: with: target_branch: ${{ steps.target-branch-extractor.outputs.result }} auth_token: ${{ secrets.GITHUB_TOKEN }} + label: backport diff --git a/.github/workflows/buildkicker.yml b/.github/workflows/buildkicker.yml deleted file mode 100644 index 68ff7570048..00000000000 --- a/.github/workflows/buildkicker.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Push a PR through check gates -on: - issue_comment: - types: [created] - -permissions: - checks: write - issues: write - pull-requests: write - statuses: write - -jobs: - KickBuild: - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/kickbuild') - runs-on: ubuntu-20.04 - steps: - - name: Extract retries allowed - uses: actions/github-script@v3 - id: retries-extractor - with: - result-encoding: string - debug: true - script: | - if (context.eventName !== "issue_comment") throw "Error: This action only works on issue_comment events."; - - // extract the optional retry count attached to the end of the command - // The expected format for the command is one of 2 examples: - // 1. `/kickbuild` - // 2. `/kickbuild 32` Where "32" is a string of digits representing the number of retries allowed for each check-run. (note this is limited to 2 digits, so no more than 99 retries) - const regex = /\/kickbuild( ([\d]{1,2}))?/; - retries = regex.exec(context.payload.comment.body); - if (retries == null) throw "Error: Command does not match expected format: `/kickbuild XX`."; - - if (retries[2] != null) { - return Number.parseInt(retries[2]); - } - - return 3; - - name: Post acknowledgement to pull request - uses: actions/github-script@v3 - id: comment-poster - with: - result-encoding: string - debug: true - script: | - const retries = ${{ steps.retries-extractor.outputs.result }}; - const ack_bodytext = `### Build Kicker\n\nStarted kicking build, **Build Kicker** may re-request each check-run at most ${retries} time(s).
\nDetails\n\n**Build Kicker** is a github action defined at \`.github/workflows/buildkicker.yml\` used to automatically retry failed check-runs.\n\nThis instance was invoked by \`@${context.payload.comment.user.login}\` by adding a comment with the text \`/kickbuild\`. \n\n> Retrying tests until they suceed isn't a great engineering practice; so, use this command with caution.\n\n[GitHub Action Details](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID})
\n`; - let comment = await github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: ack_bodytext - }); - return comment.data.id - - name: Checkout repo - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Run BuildKicker - uses: ./eng/actions/buildkicker - with: - requiredSuccesses: 2 - pollInterval: 5 - commentId: ${{ steps.comment-poster.outputs.result }} - retries: ${{ steps.retries-extractor.outputs.result }} - auth_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/check-markdown-links.yml b/.github/workflows/check-markdown-links.yml new file mode 100644 index 00000000000..d481f76e300 --- /dev/null +++ b/.github/workflows/check-markdown-links.yml @@ -0,0 +1,23 @@ +name: 'Check markdown links' +on: + pull_request: + paths: ['**.md'] + +permissions: + pull-requests: read + +jobs: + check-markdown-links: + name: 'Check markdown links' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Check markdown links + uses: gaurav-nelson/github-action-markdown-link-check@v1 + with: + config-file: .github/linters/check-markdown-links-config.json + use-quiet-mode: 'yes' + use-verbose-mode: 'no' diff --git a/.github/workflows/generate-release-notes.yml b/.github/workflows/generate-release-notes.yml new file mode 100644 index 00000000000..6117966cef9 --- /dev/null +++ b/.github/workflows/generate-release-notes.yml @@ -0,0 +1,100 @@ +name: 'Generate release notes' + +on: + workflow_dispatch: + inputs: + include_main_prs: + description: 'Include PRs that were merged into main?' + required: true + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + generate-release-notes: + name: 'Generate release notes' + runs-on: ubuntu-latest + + steps: + - name: Checkout release branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch the entire repo for the below git commit graph operations + + - name: Calculate release information + run: | + git fetch --tags + + # Grab the latest tag from the current branch. If it doesn't exist, grab the latest tag across all branches. + last_release_tag=$(git describe --tags --abbrev=0 || git describe --tags $(git rev-list --tags --max-count=1)) + echo "Using tag: $last_release_tag" + echo "last_release_date=$(git log -1 --format=%aI ${last_release_tag})" >> $GITHUB_ENV + + versionFile="./eng/Versions.props" + release_version=$(perl -ne '/([^<]*)/ && print $1' $versionFile) + release_version_label=$(perl -ne '/([^<]*)/ && print $1' $versionFile) + major_minor_version=${release_version%.*} + + version_url="https://aka.ms/dotnet/diagnostics/monitor${major_minor_version}/release/dotnet-monitor.nupkg.version" + qualified_release_version=$(curl -sL $version_url) + # Check if the aka.ms url existed + if [[ "$qualified_release_version" =~ "> $GITHUB_ENV + echo "friendly_release_name=$(echo ${friendly_release_name})" >> $GITHUB_ENV + echo "qualified_release_version=$(echo ${qualified_release_version})" >> $GITHUB_ENV + + - name: Checkout main + uses: actions/checkout@v3 + with: + ref: main + + - name: Generate release notes + if: ${{ inputs.include_main_prs != true }} + uses: ./.github/actions/generate-release-notes + with: + output: ${{ env.release_note_path }} + last_release_date: ${{ env.last_release_date }} + build_description: ${{ env.friendly_release_name }} + auth_token: ${{ secrets.GITHUB_TOKEN }} + branch_name: ${{ github.ref_name }} + + - name: Generate release notes (main merged) + if: ${{ inputs.include_main_prs }} + uses: ./.github/actions/generate-release-notes + with: + output: ${{ env.release_note_path }} + last_release_date: ${{ env.last_release_date }} + build_description: ${{ env.friendly_release_name }} + auth_token: ${{ secrets.GITHUB_TOKEN }} + branch_name: ${{ github.ref_name }} + additional_branch: 'main' + + - name: Open PR + uses: ./.github/actions/open-pr + with: + files_to_commit: ${{ env.release_note_path }} + title: Add ${{ env.qualified_release_version }} release notes + commit_message: generate release notes + body: Add ${{ env.qualified_release_version }} release notes. This PR was auto generated and will not be automatically merged in. + branch_name: releaseNotes/${{ env.qualified_release_version }} + fail_if_files_unchanged: true + auth_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint-csharp.yml b/.github/workflows/lint-csharp.yml new file mode 100644 index 00000000000..2b670a6c2e7 --- /dev/null +++ b/.github/workflows/lint-csharp.yml @@ -0,0 +1,59 @@ +name: 'C# linting' +on: + pull_request: + paths: ['src/**.cs'] + branches: ['main', 'release/6.x', 'release/7.*'] + +permissions: + pull-requests: read + +jobs: + lint-csharp: + name: 'C# Linting' + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Get base commit for the PR + run: | + git fetch origin ${{ github.base_ref }} + echo "base_sha=$(git rev-parse origin/${{ github.base_ref }})" >> $GITHUB_ENV + echo "Merging ${{ github.sha }} into ${{ github.base_ref }}" + + - name: Get changed files + run: | + echo "Files changed: '$(git diff-tree --no-commit-id --name-only -r ${{ env.base_sha }} ${{ github.sha }})'" + changed_source_files=$(git diff-tree --no-commit-id --name-only -r ${{ env.base_sha }} ${{ github.sha }} | { grep "**.cs$" || test $? = 1; }) + echo "Files to validate: '${changed_source_files}'" + echo "updated_files=$(echo ${changed_source_files})" >> $GITHUB_ENV + + - name: Get dotnet version + run: echo 'dotnet_version='$(jq -r '.tools.dotnet' global.json) >> $GITHUB_ENV + + - name: Setup dotnet + uses: actions/setup-dotnet@v3.0.1 + with: + dotnet-version: ${{ env.dotnet_version }} + include-prerelease: true + + # Workaround for a bug in the dotnet-format build shipped in .NET 7.0 Preview 5. Ref: https://github.com/dotnet/core/blob/main/release-notes/7.0/known-issues.md#unhandled-exception-in-dotnet-format-app-in-net-70-preview-5 + - name: Setup dotnet format + run: dotnet tool install -g dotnet-format --version "7.*" --configfile ./NuGet.config + + - name: Run dotnet format + run: dotnet-format --include ${{ env.updated_files }} + + - name: Generate artifacts + run: | + mkdir -p ./pr + cp $GITHUB_EVENT_PATH ./pr/pr-event.json + echo -n $GITHUB_EVENT_NAME > ./pr/pr-event-name + git diff > ./pr/linter.diff + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: pr-linter + path: pr/ diff --git a/.github/workflows/submit-linter-suggestions.yml b/.github/workflows/submit-linter-suggestions.yml new file mode 100644 index 00000000000..fc79fa0f140 --- /dev/null +++ b/.github/workflows/submit-linter-suggestions.yml @@ -0,0 +1,70 @@ +name: 'Submit linter suggestions' + +on: + workflow_run: + workflows: ["C# linting", "Add Markdown Feedback"] + types: + - completed + +permissions: + pull-requests: write + +jobs: + submit-linter-suggestions: + name: 'Submit linter suggestions' + runs-on: ubuntu-latest + if: > + ${{ github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set C# Linting Env Vars + if: ${{ github.event.workflow_run.name == 'C# Linting' }} + run: | + echo 'reporter_name=dotnet format' >> $GITHUB_ENV + echo 'workflow_name=lint-csharp.yml' >> $GITHUB_ENV + + - name: Set Append Markdown Feedback Env Vars + if: ${{ github.event.workflow_run.name == 'Add Markdown Feedback' }} + run: | + echo 'reporter_name=Add Markdown Feedback' >> $GITHUB_ENV + echo 'workflow_name=add-markdown-feedback.yml' >> $GITHUB_ENV + # Download the artifact from the workflow that kicked off this one. + # The default artifact download action doesn't support cross-workflow + # artifacts, so use a 3rd party one. + - name: 'Download linting results' + uses: dawidd6/action-download-artifact@v2 + with: + workflow: ${{env.workflow_name}} + run_id: ${{github.event.workflow_run.id }} + name: pr-linter + path: ./pr-linter + + - name: 'Setup reviewdog' + uses: reviewdog/action-setup@v1 + + # Manually supply the triggering PR event information since when a PR is from a fork, + # this workflow running in the base repo will not be given information about it. + # + # Also patch the fork's owner id in the event file, since reviewdog has as fail-fast path that + # checks the head vs base repo owner id to determine if the PR is from a fork. + # If so, it assumes that it doesn't have permissions to write comments on the PR. + # + # This isn't the case in our setup since we are using two workflows (lint-csharp and this one) + # to enable write permissions on fork PRs. + - name: Submit formatting suggestions + run: | + new_event_file=${{github.workspace}}/reviewdog_event.json + new_event_name=$(cat ./pr-linter/pr-event-name) + jq -j ".${new_event_name}.head.repo.owner.id = .${new_event_name}.base.repo.owner.id" ./pr-linter/pr-event.json > ${new_event_file} + GITHUB_EVENT_NAME="${new_event_name}" GITHUB_EVENT_PATH="${new_event_file}" REVIEWDOG_GITHUB_API_TOKEN="${{ secrets.GITHUB_TOKEN }}" reviewdog \ + -name="${{env.reporter_name}}" \ + -f=diff \ + -f.diff.strip=1 \ + -reporter="github-pr-review" \ + -filter-mode="diff_context" \ + -fail-on-error="false" \ + -level="warning" \ + < "./pr-linter/linter.diff" diff --git a/.github/workflows/update-release-version.yml b/.github/workflows/update-release-version.yml new file mode 100644 index 00000000000..a0ff2781c2e --- /dev/null +++ b/.github/workflows/update-release-version.yml @@ -0,0 +1,103 @@ +name: 'Update release version' + +on: + workflow_dispatch: + inputs: + release_type: + description: 'Release type' + required: true + default: 'preview' + type: choice + options: + - rtm + - rc + - preview + - alpha + release_version: + description: 'Release version' + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + update-release-version: + name: 'Update release version' + runs-on: ubuntu-latest + + steps: + - name: Checkout branch + uses: actions/checkout@v3 + + - name: Update release information + run: | + # note: \d is not valid in POSIX regex + if [[ ! "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Unexpected release version, valid format is major.minor.patch without any extra labels." + exit 1 + fi + + versionFile="./eng/Versions.props" + + current_release_version=$(perl -ne '/([^<]*)/ && print $1' $versionFile) + current_release_version_type=$(perl -ne '/([^<]*)/ && print $1' $versionFile) + current_prerelease_version_iteration=$(perl -ne '/([^<]*)/ && print $1' $versionFile) + declare -i current_prerelease_version_iteration + + new_release_type="$release_type" + new_release_version="$release_version" + new_build_quality="daily" + new_prerelease_iteration=0 + declare -i new_prerelease_iteration + + current_branch_name=$(git symbolic-ref --short HEAD) + if [[ "$current_branch_name" =~ ^release\/* ]]; then + new_build_quality="release" + fi + + if [ "$release_type" == "rtm" ]; then + if [[ "$release_version" != *\.0 ]]; then + new_release_type="servicing" + fi + else + if [[ ! -z $current_prerelease_version_iteration ]] && [ "$release_type" == "$current_release_version_type" ] && [ "$release_version" == "$current_release_version" ]; then + new_prerelease_iteration=$current_prerelease_version_iteration+1 + else + new_prerelease_iteration=1 + fi + fi + + # Apply the new version information + sed -i "s/.*/${new_release_version}<\/VersionPrefix>/" $versionFile + sed -i "s/.*/${new_release_type}<\/PreReleaseVersionLabel>/" $versionFile + + sed -i "//d" $versionFile + if [ $new_prerelease_iteration != 0 ]; then + sed -i "/.*/a\ \ \ \ ${new_prerelease_iteration}<\/PreReleaseVersionIteration>" $versionFile + fi + + sed -i "s/.*/${new_build_quality}<\/BlobGroupBuildQuality>/" $versionFile + + sed -i "//d" $versionFile + # rtm on the release_type input covered both rtm and servicing + if [ "$release_type" == "rtm" ]; then + sed -i "/.*/a\ \ \ \ release<\/DotnetFinalVersionKind>" $versionFile + fi + + echo "release_version_file=$(echo $versionFile)" >> $GITHUB_ENV + env: + release_type: ${{ inputs.release_type }} + release_version: ${{ inputs.release_version }} + + - name: Open PR + uses: ./.github/actions/open-pr + with: + files_to_commit: ${{ env.release_version_file }} + title: '[${{ github.ref_name }}] Update release version' + commit_message: update release information + body: Update release version for ${{ inputs.release_type }} ${{ inputs.release_version }}. This PR was auto generated and will not be automatically merged in. + branch_name: releaseVersion/${{ inputs.release_type }}-${{ inputs.release_version }} + fail_if_files_unchanged: false + auth_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index cc2f220cf56..fb438b69cc3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ x64/ .dotnet-test/ .packages/ .tools/ -.vscode/ # Per-user project properties launchSettings.json @@ -97,6 +96,16 @@ ClientBin/ App_Data/*.mdf App_Data/*.ldf +# Azurite folders/files +**/__*storage__/ +**/__azurite* +.azurite + +# Github actions development +.github/actions/**/node_modules/ +.github/actions/**/package.json +.github/actions/**/package-lock.json + # ========================= # Windows detritus # ========================= diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..c5b3ac44e43 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,38 @@ +{ + "version": "0.2.0", + "inputs": [ + { + "id": "args", + "description": "Enter arguments for dotnet-monitor", + "default": "collect --no-auth", + "type": "promptString", + } + ], + "configurations": [ + { + "name": "Build & Launch", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build (Debug)", + "program": "${workspaceFolder}/artifacts/bin/dotnet-monitor/Debug/net7.0/dotnet-monitor.dll", + "args": "${input:args}", + "stopAtEntry": false, + "justMyCode": false + }, + { + "name": "Launch", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/artifacts/bin/dotnet-monitor/Debug/net7.0/dotnet-monitor.dll", + "args": "${input:args}", + "stopAtEntry": false, + "justMyCode": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "justMyCode": false + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000000..672de5b1292 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,122 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Build", + "type": "shell", + "command": "./build.sh", + "windows": { + "command": ".\\build.cmd" + }, + "args": [ + "-ci", + "-c", + "${input:configuration}" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ] + }, + { + "label": "Build (Debug)", + "type": "shell", + "command": "./build.sh", + "windows": { + "command": ".\\build.cmd" + }, + "args": [ + "-ci", + "-c", + "Debug" + ], + "group": { + "kind": "build", + }, + "problemMatcher": [ + "$msCompile" + ] + }, + { + "label": "Test", + "type": "shell", + "command": "./test.sh", + "windows": { + "command": ".\\test.cmd" + }, + "args": [ + "-ci", + "-c", + "${input:configuration}", + "/p:TestGroup=${input:testgroup}" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [] + }, + { + "label": "Regenerate schema.json", + "type": "shell", + "command": "./dotnet.sh", + "windows": { + "command": ".\\dotnet.cmd" + }, + "args": [ + "run", + "--project", + "./src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj", + "./documentation/schema.json" + ], + "problemMatcher": [ + "$msCompile" + ] + }, + { + "label": "Regenerate openapi.json", + "type": "shell", + "command": "./dotnet.sh", + "windows": { + "command": ".\\dotnet.cmd" + }, + "args": [ + "run", + "--project", + "./src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Microsoft.Diagnostics.Monitoring.OpenApiGen.csproj", + "./documentation/openapi.json" + ], + "problemMatcher": [ + "$msCompile" + ] + } + ], + "inputs": [ + { + "id": "configuration", + "type": "pickString", + "default": "Debug", + "description": "The build configuration", + "options": [ + "Debug", + "Release" + ] + }, + { + "id": "testgroup", + "type": "pickString", + "default": "PR", + "description": "The test group", + "options": [ + "CI", + "PR", + "All" + ] + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..9b42fa255ca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,105 @@ +# Contribution to .NET Monitor + +You can contribute to .NET Monitor with issues and PRs. Simply filing issues for problems you encounter is a great way to contribute. Contributing implementations is greatly appreciated. + +## Reporting Issues + +We always welcome bug reports, proposals and overall feedback. Here are a few tips on how you can make reporting your issue as effective as possible. + +### Finding Existing Issues + +Before filing a new issue, please search our [open issues](https://github.com/dotnet/dotnet-monitor/issues) to check if it already exists. + +If you do find an existing issue, please include your own feedback in the discussion. Do consider upvoting (👍 reaction) the original post, as this helps us prioritize popular issues in our backlog. + +### Writing a Good Bug Report + +Good bug reports make it easier for maintainers to verify and root cause the underlying problem. The better a bug report, the faster the problem will be resolved. Ideally, a bug report should contain the following information: + +* A high-level description of the problem. +* A _minimal reproduction_, i.e. the smallest size of code/configuration required to reproduce the wrong behavior. +* A description of the _expected behavior_, contrasted with the _actual behavior_ observed. +* Information on the environment: OS/distro, CPU arch, SDK version, etc. +* Additional information, e.g. is it a regression from previous versions? are there any known workarounds? + +### DOs and DON'Ts + +Please do: + +* **DO** follow our coding style. +* **DO** include tests when adding new features. When fixing bugs, start with + adding a test that highlights how the current behavior is broken. +* **DO** keep the discussions focused. When a new or related topic comes up + it's often better to create a new issue than to side track the discussion. +* **DO** feel free to blog, tweet, or share anywhere else about your contributions! + +Please do not: + +* **DON'T** make PRs for style changes. +* **DON'T** surprise us with big pull requests. For large changes, create + a new discussion so we can agree on a direction before you invest a large amount + of time. For bug fixes, create an issue. +* **DON'T** commit code that you didn't write. If you find code that you think is a good fit to add to .NET Monitor, file an issue and start a discussion before proceeding. +* **DON'T** submit PRs that alter licensing related files or headers. If you believe there's a problem with them, file an issue and we'll be happy to discuss it. + +### Suggested Workflow + +We use and recommend the following workflow: + +1. Create an issue for your work. + - You can skip this step for trivial changes. + - Reuse an existing issue on the topic, if there is one. + - Get agreement from the team and the community that your proposed change is a good one. + - Clearly state that you are going to take on implementing it, if that's the case. You can request that the issue be assigned to you. Note: The issue filer and the implementer don't have to be the same person. +2. Create a personal fork of the repository on GitHub (if you don't already have one). +3. In your fork, create a branch off of main (`git checkout -b mybranch`). + - Name the branch so that it clearly communicates your intentions, such as issue-123 or githubhandle-issue. + - Branches are useful since they isolate your changes from incoming changes from upstream. They also enable you to create multiple PRs from the same fork. +4. Make and commit your changes to your branch. +5. Add new tests corresponding to your change, if applicable. +6. Build the repository with your changes. + - Make sure that the builds are clean. + - Make sure that the tests are all passing, including your new tests. +7. Create a pull request (PR) against the dotnet/dotnet-monitor repository's **main** branch. + - State in the description what issue or improvement your change is addressing. + - Check if all the tests are passing. +8. Wait for feedback or approval of your changes from the team. +9. When the team has signed off, and all checks are green, your PR will be merged. + - The next official build will include your change. + - You can delete the branch you used for making the change. + +### Contributor License Agreement + +You must sign a [.NET Foundation Contribution License Agreement (CLA)](https://cla.dotnetfoundation.org) before your PR will be merged. This is a one-time requirement for projects in the .NET Foundation. You can read more about [Contribution License Agreements (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) on Wikipedia. + +The agreement: [net-foundation-contribution-license-agreement.pdf](https://github.com/dotnet/home/blob/master/guidance/net-foundation-contribution-license-agreement.pdf) + +You don't have to do this up-front. You can simply clone, fork, and submit your pull-request as usual. When your pull-request is created, it is classified by a CLA bot. If the change is trivial (for example, you just fixed a typo), then the PR is labelled with `cla-not-required`. Otherwise it's classified as `cla-required`. Once you signed a CLA, the current and all future pull-requests will be labelled as `cla-signed`. + +### File Headers + +Please use the following file header for new files. + +``` +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +``` + +### PR - CI Process + +The [dotnet continuous integration](https://dev.azure.com/dnceng/public/) (CI) system will automatically perform the required builds and run tests (including the ones you are expected to run) for PRs. Builds and test runs must be clean. + +If the CI build fails for any reason, the PR issue will be updated with a link that can be used to determine the cause of the failure. + +### PR Feedback + +Microsoft team and community members will provide feedback on your change. Community feedback is highly valued. You will often see the absence of team feedback if the community has already provided good review feedback. + +One or more Microsoft team members will review every PR prior to merge. That means that the PR will be merged once the feedback is resolved. "LGTM" == "looks good to me". + +There are lots of thoughts and approaches for how to efficiently discuss changes. It is best to be clear and explicit with your feedback. Please be patient with people who might not understand the finer details about your approach to feedback. + +### Stale PR Policy + +In an effort to prevent pull requests from becoming stale, the dotnet-monitor team will comment on pull requests that haven't had any activity in the last 4 weeks to ensure they are still under active development. After this point, if there are no updates on the pull request, the dotnet-monitor team will close the pull request 6 weeks after the notification. In the event that your pull request is closed, please feel free to re-open the pull request in the future to continue the review process, or open a new pull request with a link to the closed one. diff --git a/README.md b/README.md index be3242c17b1..18cca518221 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ There are many .NET related projects on GitHub. This project has adopted the code of conduct defined by the [Contributor Covenant](http://contributor-covenant.org/) to clarify expected behavior in our community. For more information, see the [.NET Foundation Code of Conduct](http://www.dotnetfoundation.org/code-of-conduct). -General .NET OSS discussions: [.NET Foundation forums](https://forums.dotnetfoundation.org) +General .NET OSS discussions: [.NET Foundation Discussions](https://github.com/dotnet-foundation/Home/discussions) ## License diff --git a/documentation/README.md b/documentation/README.md index 859253d8bc5..5426ccb54d5 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2FREADME) + # 📖 `dotnet monitor` documentation `dotnet monitor` is a tool that makes it easier to get access to diagnostics information in a dotnet process. @@ -24,6 +27,7 @@ When running a dotnet application, differences in diverse local and production e - [`/logs`](./api/logs.md) - [`/info`](./api/info.md) - [`/operations`](./api/operations.md) + - [`/collectionrules`](./api/collectionrules.md) - [Configuration](./configuration.md) - [JSON Schema](./schema.json) - [Authentication](./authentication.md) @@ -34,6 +38,7 @@ When running a dotnet application, differences in diverse local and production e - [Collection Rules examples](./collectionrules/collectionruleexamples.md) - [Trigger shortcut](./collectionrules/triggershortcuts.md) - [Egress Providers](./egress.md) +- [Breaking Changes by Version](./compatibility/README.md) - [Troubleshooting](./troubleshooting.md) - [Clone, build, and test the repo](./building.md) - [Official Build Instructions](./official-build-instructions.md) diff --git a/documentation/api-key-format.md b/documentation/api-key-format.md index 11979c16253..3c06d20d223 100644 --- a/documentation/api-key-format.md +++ b/documentation/api-key-format.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi-key-format) + # API Key Format API Keys or MonitorApiKeys used in `dotnet monitor` are JSON Web Tokens or JWTs as defined by [RFC 7519: JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519). > **Note:** Because the API Key is a `Bearer` token, it should be treated as a secret and always transmitted over `TLS` or another protected protocol. diff --git a/documentation/api-key-setup.md b/documentation/api-key-setup.md index 479c3c3eac6..91d43710b8f 100644 --- a/documentation/api-key-setup.md +++ b/documentation/api-key-setup.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi-key-setup) + # Configuring API Key Authentication The API Key you use to secure `dotnet monitor` is a secret Json Web Token (JWT), cryptographically signed by a public/private key algorithm. You can **[Recommended]** use the integrated command to generate a key or you can generate the key yourself following the [format, documented here](./api-key-format.md). This guide will use the integrated command. diff --git a/documentation/api/README.md b/documentation/api/README.md index 7b8da1a2673..8bfe1f9acb8 100644 --- a/documentation/api/README.md +++ b/documentation/api/README.md @@ -1,19 +1,26 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2FREADME) + # HTTP API Documentation The HTTP API enables on-demand extraction of diagnostic information and artifacts from discoverable processes. +>**NOTE:** Some features are [experimental](./../experimental.md) and are denoted as `**[Experimental]**` in this document. + The following are the root routes on the HTTP API surface. -| Route | Description | -|---|---| -| [`/processes`](processes.md) | Gets detailed information about discoverable processes. | -| [`/dump`](dump.md) | Captures managed dumps of processes without using a debugger. | -| [`/gcdump`](gcdump.md) | Captures GC dumps of processes. | -| [`/trace`](trace.md) | Captures traces of processes without using a profiler. | -| [`/metrics`](metrics.md) | Captures metrics of a process in the Prometheus exposition format. | -| [`/livemetrics`](livemetrics.md) | Captures live metrics of a process. | -| [`/logs`](logs.md) | Captures logs of processes. | -| [`/info`](info.md) | Gets info about Dotnet Monitor. | -| [`/operations`](operations.md) | Gets egress operation status or cancels operations. | +| Route | Description | Version Introduced | +|---|---|---| +| [`/processes`](processes.md) | Gets detailed information about discoverable processes. | 6.0 | +| [`/dump`](dump.md) | Captures managed dumps of processes without using a debugger. | 6.0 | +| [`/gcdump`](gcdump.md) | Captures GC dumps of processes. | 6.0 | +| [`/trace`](trace.md) | Captures traces of processes without using a profiler. | 6.0 | +| [`/metrics`](metrics.md) | Captures metrics of a process in the Prometheus exposition format. | 6.0 | +| [`/livemetrics`](livemetrics.md) | Captures live metrics of a process. | 6.0 | + [`/stacks`](stacks.md) | **[Experimental]** Gets the current callstacks of all .NET threads. | 7.0 | +| [`/logs`](logs.md) | Captures logs of processes. | 6.0 | +| [`/info`](info.md) | Gets info about `dotnet monitor`. | 6.0 | +| [`/operations`](operations.md) | Gets egress operation status or cancels operations. | 6.0 | +| [`/collectionrules`](collectionrules.md) | Gets the current state of collection rules. | 6.3 | The `dotnet monitor` tool is able to detect .NET Core 3.1 and .NET 5+ applications. When connecting to a .NET Core 3.1 application, some information may not be available and is called out in the documentation. diff --git a/documentation/api/collectionrules-get.md b/documentation/api/collectionrules-get.md new file mode 100644 index 00000000000..9aa049f5b62 --- /dev/null +++ b/documentation/api/collectionrules-get.md @@ -0,0 +1,97 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fcollectionrules-get) + +# Collection Rules - Get (6.3+) + +Get the detailed state of the specified collection rule for all processes or for the specified process. + +## HTTP Route + +```http +GET /collectionrules/{collectionrulename}?pid={pid}&uid={uid}&name={name} HTTP/1.1 +``` + +> **NOTE:** Process information (IDs, names, environment, etc) may change between invocations of these APIs. Processes may start or stop between API invocations, causing this information to change. + +## Host Address + +The default host address for these routes is `https://localhost:52323`. This route is only available on the addresses configured via the `--urls` command line parameter and the `DOTNETMONITOR_URLS` environment variable. + +## URI Parameters + +| Name | In | Required | Type | Description | +|---|---|---|---|---| +| `collectionrulename` | path | true | string | The name of the collection rule for which a detailed description should be provided. | +| `pid` | query | false | int | The ID of the process. | +| `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | +| `name` | query | false | string | The name of the process. | + +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. + +If none of `pid`, `uid`, or `name` are specified, the detailed description of the collection rule for the [default process](defaultprocess.md) will be provided. Attempting to get the detailed description from the default process when the default process cannot be resolved will fail. + +## Authentication + +Authentication is enforced for this route. See [Authentication](./../authentication.md) for further information. + +Allowed schemes: +- `Bearer` +- `Negotiate` (Windows only, running as unelevated) + +## Responses + +| Name | Type | Description | Content Type | +|---|---|---|---| +| 200 OK | [CollectionRuleDetailedDescription](definitions.md#collectionruledetaileddescription-63) | The detailed information about the current state of the specified collection rule. | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | + +## Examples + +### Sample Request + +```http +GET /collectionrules?pid=21632 HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +``` + +or + +```http +GET /collectionrules?uid=cd4da319-fa9e-4987-ac4e-e57b2aac248b HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +``` + +### Sample Response + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "state":"Running", + "stateReason":"This collection rule is active and waiting for its triggering conditions to be satisfied.", + "lifetimeOccurrences":0, + "slidingWindowOccurrences":0, + "actionCountLimit":2, + "actionCountSlidingWindowDurationLimit":"00:01:00", + "slidingWindowDurationCountdown":null, + "ruleFinishedCountdown":"00:03:00" +} +``` + +## Supported Runtimes + +| Operating System | Runtime Version | +|---|---| +| Windows | .NET 5+ | +| Linux | .NET 5+ | +| MacOS | .NET 5+ | + +## Additional Notes + +### When to use `pid` vs `uid` + +See [Process ID `pid` vs Unique ID `uid`](pidvsuid.md) for clarification on when it is best to use either parameter. diff --git a/documentation/api/collectionrules-list.md b/documentation/api/collectionrules-list.md new file mode 100644 index 00000000000..356c4f61ec8 --- /dev/null +++ b/documentation/api/collectionrules-list.md @@ -0,0 +1,92 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fcollectionrules-list) + +# Collection Rules - List (6.3+) + +Get the basic state of all collection rules for all processes or for the specified process. + +## HTTP Route + +```http +GET /collectionrules?pid={pid}&uid={uid}&name={name} HTTP/1.1 +``` + +> **NOTE:** Process information (IDs, names, environment, etc) may change between invocations of these APIs. Processes may start or stop between API invocations, causing this information to change. + +## Host Address + +The default host address for these routes is `https://localhost:52323`. This route is only available on the addresses configured via the `--urls` command line parameter and the `DOTNETMONITOR_URLS` environment variable. + +## URI Parameters + +| Name | In | Required | Type | Description | +|---|---|---|---|---| +| `pid` | query | false | int | The ID of the process. | +| `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | +| `name` | query | false | string | The name of the process. | + +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. + +If none of `pid`, `uid`, or `name` are specified, the state of collection rules for the [default process](defaultprocess.md) will be provided. Attempting to get the state of collection rules from the default process when the default process cannot be resolved will fail. + +## Authentication + +Authentication is enforced for this route. See [Authentication](./../authentication.md) for further information. + +Allowed schemes: +- `Bearer` +- `Negotiate` (Windows only, running as unelevated) + +## Responses + +| Name | Type | Description | Content Type | +|---|---|---|---| +| 200 OK | map (of [CollectionRuleDescription](definitions.md#collectionruledescription-63)) | The basic information about the current state of the configured collection rules. | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | + +## Examples + +### Sample Request + +```http +GET /collectionrules?pid=21632 HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +``` + +or + +```http +GET /collectionrules?uid=cd4da319-fa9e-4987-ac4e-e57b2aac248b HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +``` + +### Sample Response + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "MyCollectionRule": { + "state":"Running", + "stateReason":"This collection rule is active and waiting for its triggering conditions to be satisfied.", + } +} +``` + +## Supported Runtimes + +| Operating System | Runtime Version | +|---|---| +| Windows | .NET 5+ | +| Linux | .NET 5+ | +| MacOS | .NET 5+ | + +## Additional Notes + +### When to use `pid` vs `uid` + +See [Process ID `pid` vs Unique ID `uid`](pidvsuid.md) for clarification on when it is best to use either parameter. diff --git a/documentation/api/collectionrules.md b/documentation/api/collectionrules.md new file mode 100644 index 00000000000..03c33ef6353 --- /dev/null +++ b/documentation/api/collectionrules.md @@ -0,0 +1,13 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fcollectionrules) + +# Collection Rules (6.3+) + +The `/collectionrules` route reports the state of configured collection rules. + +> **NOTE:** Process information (IDs, names, environment, etc) may change between invocations of these APIs. Processes may start or stop between API invocations, causing this information to change. + +| Operation | Description | +|---|---| +| [Get Collection Rules](collectionrules-get.md) | Get the detailed state of the specified collection rule for all processes or for the specified process. | +| [List Collection Rules](collectionrules-list.md) | Get the basic state of all collection rules for all processes or for the specified process. | \ No newline at end of file diff --git a/documentation/api/defaultprocess.md b/documentation/api/defaultprocess.md index d0fd2840757..b2b5863f2ce 100644 --- a/documentation/api/defaultprocess.md +++ b/documentation/api/defaultprocess.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fdefaultprocess) + # Default Process When using APIs to capture diagnostic artifacts, typically a `pid`, `uid`, or `name` is provided to perform the operation on a specific process. However, these parameters may be omitted if `dotnet monitor` is able to resolve a default process. diff --git a/documentation/api/definitions.md b/documentation/api/definitions.md index 1aa98ea7453..913a4072660 100644 --- a/documentation/api/definitions.md +++ b/documentation/api/definitions.md @@ -1,5 +1,75 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fdefinitions) + # Definitions +>**NOTE:** Some features are [experimental](./../experimental.md) and are denoted as `**[Experimental]**` in this document. + +## **[Experimental]** CallStack (7.0+) + +| Name | Type | Description | +|---|---|---| +| `threadId` | int | The native thread id of the managed thread. | +| `frames` | [CallStackFrame](#experimental-callstackframe-70)[] | Managed frame for the thread at the time of collection. | + +## **[Experimental]** CallStackFormat (7.0+) + +Enumeration that describes the output format of the collected call stacks. + +| Name | Description | +|---|---| +| `Json` | Stacks are formatted in Json. See [CallStackResult](#experimental-callstackresult-70). | +| `PlainText` | Stacks are formatted in plain text. | + +## **[Experimental]** CallStackFrame (7.0+) + +| Name | Type | Description | +|---|---|---| +| `methodName` | string | Name of the method for this frame. This includes generic parameters. | +| `className` | string | Name of the class for this frame. This includes generic parameters. | +| `moduleName` | string | Name of the module for this frame. | + +## **[Experimental]** CallStackResult (7.0+) + +| Name | Type | Description | +|---|---|---| +| `stacks` | [CallStack](#experimental-callstack-70)[] | List of all managed stacks at the time of collection. | + +## CollectionRuleDescription (6.3+) + +Object describing the basic state of a collection rule for the executing instance of `dotnet monitor`. + +| Name | Type | Description | +|---|---|---| +| State | [CollectionRuleState](#collectionrulestate-63) | Indicates what state the collection rule is in for the current process. | +| StateReason | string | Human-readable explanation for the current state of the collection rule. | + +## CollectionRuleDetailedDescription (6.3+) + +Object describing the detailed state of a collection rule for the executing instance of `dotnet monitor`. + +| Name | Type | Description | +|---|---|---| +| State | [CollectionRuleState](#collectionrulestate-63) | Indicates what state the collection rule is in for the current process. | +| StateReason | string | Human-readable explanation for the current state of the collection rule. | +| LifetimeOccurrences | int | The number of times the trigger has executed for a process in its lifetime. | +| SlidingWindowOccurrences | int | The number of times the trigger has executed within the current sliding window. | +| ActionCountLimit | int | The number of times the action list may be executed before being throttled. | +| ActionCountSlidingWindowDurationLimit | TimeSpan? | The sliding window of time to consider whether the action list should be throttled based on the number of times the action list was executed. Executions that fall outside the window will not count toward the limit specified in the ActionCount setting. If not specified, all action list executions will be counted for the entire duration of the rule. | +| SlidingWindowDurationCountdown | TimeSpan? | The amount of time remaining before the collection rule will no longer be throttled. | +| RuleFinishedCountdown | TimeSpan? | The amount of time remaining before the rule will stop monitoring a process after it has been applied to a process. If not specified, the rule will monitor the process with the trigger indefinitely. | + +## CollectionRuleState (6.3+) + +Enumeration that describes the current state of the collection rule. + +| Name | Description | +|---|---| +| `Running` | Indicates that the collection rule is active and waiting for its triggering conditions to be satisfied. | +| `ActionExecuting` | Indicates that the collection has had its triggering conditions satisfied and is currently executing its action list. | +| `Throttled` | Indicates that the collection rule is temporarily throttled because the ActionCountLimit has been reached within the ActionCountSlidingWindowDuration. | +| `Finished` | Indicates that the collection rule has completed and will no longer trigger. | + ## DotnetMonitorInfo Object describing diagnostic/automation information about the executing instance of `dotnet monitor`. @@ -29,7 +99,7 @@ Describes custom metrics. | Name | Type | Description | |---|---|---| | `includeDefaultProviders` | bool | Determines if the default counter providers should be used (such as System.Runtime). | -| `providers` | [EventMetricsProvider](#EventMetricsProvider)[] | Array of providers for metrics to collect. | +| `providers` | [EventMetricsProvider](#eventmetricsprovider)[] | Array of providers for metrics to collect. | ## EventMetricsProvider @@ -55,7 +125,7 @@ Object describing the list of event providers, keywords, event levels, and addit | Name | Type | Description | |---|---|---| -| Providers | [EventProvider](#EventProvider)[] | List of event providers from which to capture events. At least one event provider must be specified. | +| Providers | [EventProvider](#eventprovider)[] | List of event providers from which to capture events. At least one event provider must be specified. | | RequestRundown | bool | The runtime may provide additional type information for certain types of events after the trace session is ended. This additional information is known as rundown events. Without this information, some events may not be parsable into useful information. Default is `true`. | | BufferSizeInMB | int | The size (in megabytes) of the event buffer used in the runtime. If the event buffer is filled, events produced by event providers may be dropped until the buffer is cleared. Increase the buffer size to mitigate this or pair down the list of event providers, keywords, and level to filter out extraneous events. Default is `256`. Min is `1`. Max is `1024`. | @@ -85,7 +155,7 @@ Object describing a log entry from a target process. | `Category` | string | The category of the log entry. | | `EventId` | string | The event name of the EventId of the log entry. | | `Exception` | string | If an exception is logged, this property contains the formatted message of the log entry. | -| `LogLevel` | string | The [LogLevel](#LogLevel) of the log entry. | +| `LogLevel` | string | The [LogLevel](#loglevel) of the log entry. | | `Message` | string | If an exception is NOT logged, this property contains the formatted message of the log entry. | | `Scopes` | map (of object) | The scope information associated with the log entry. | @@ -141,8 +211,8 @@ Object describing the default log level and filtering specifications for collect | Name | Type | Description | |---|---|---| -| `logLevel` | [LogLevel](#LogLevel) | The default log level at which logs are collected. Default is `Warning`. | -| `filterSpecs` | map (of [LogLevel](#LogLevel) or `null`) | A mapping of logger categories and the levels at which those categories should be collected. If level is set to `null`, collect category at the default level set in the `logLevel` property. | +| `logLevel` | [LogLevel](#loglevel) | The default log level at which logs are collected. Default is `Warning`. | +| `filterSpecs` | map (of [LogLevel](#loglevel) or `null`) | A mapping of logger categories and the levels at which those categories should be collected. If level is set to `null`, collect category at the default level set in the `logLevel` property. | | `useAppFilters` | bool | Collect logs for the categories and at the levels as specified by the [application-defined configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/#configure-logging). Default is `true`. | ### Example @@ -179,6 +249,16 @@ Object describing a metric from the application. | `code` | string | Error code representing the failure. | | `message` | string | Detailed error message. | +## OperationProcessInfo (6.3+) + +The process on which the egress operation is performed. + +| Name | Type | Description | +|---|---|---| +| `pid` | int | The ID of the process. | +| `uid` | guid | `.NET 5+` A value that uniquely identifies a runtime instance within a process.
`.NET Core 3.1` An empty value: `00000000-0000-0000-0000-000000000000` | +| `name` | string | The name of the process. | + ## OperationState Status of the egress operation. @@ -197,10 +277,10 @@ Detailed information about an operation. | Name | Type | Description | |---|---|---| | `resourceLocation` | string | Resource location of the egressed artifact. This can be Uri or path depending on the egress provider. | -| `error` | [OperationError](#OperationError) | Detailed error message if the operation is in a `Failed` state. | +| `error` | [OperationError](#operationerror) | Detailed error message if the operation is in a `Failed` state. | | `operationId` | guid | Unique identifier for the operation. | | `createdDateTime` | datetime string | UTC DateTime string of when the operation was created. | -| `status` | [OperationState](#OperationState) | The current status of operation. | +| `status` | [OperationState](#operationstate) | The current status of operation. | ### Example @@ -222,7 +302,8 @@ Summary state of an operation. |---|---|---| | `operationId` | guid | Unique identifier for the operation. | | `createdDateTime` | datetime string | UTC DateTime string of when the operation was created. | -| `status` | [OperationState](#OperationState) | The current status of operation. | +| `status` | [OperationState](#operationstate) | The current status of operation. | +| `process` | [OperationProcessInfo](#operationprocessinfo) | (6.3+) The process on which the operation is performed. | ### Example @@ -230,7 +311,12 @@ Summary state of an operation. { "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", - "status": "Succeeded" + "status": "Succeeded", + "process": { + "pid": 21632, + "uid": "cd4da319-fa9e-4987-ac4e-e57b2aac248b", + "name": "dotnet" + } } ``` @@ -286,6 +372,16 @@ The `uid` property is useful for uniquely identifying a process when it is runni "processArchitecture": "x64" } ``` +## TraceEventFilter + +Object describing a filter for trace events. + +| Name | Type | Description | +|---|---|---| +| `ProviderName` | string | The event provider that will produce the specified event. | +| `EventName` | string | The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name. | +| `PayloadFilter` | map (of string) | (Optional) A mapping of event payload field names to their expected value. A subset of the payload fields may be specified. | + ## TraceProfile diff --git a/documentation/api/dump.md b/documentation/api/dump.md index 19866fff346..ee6724c1292 100644 --- a/documentation/api/dump.md +++ b/documentation/api/dump.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fdump) + # Dump - Get Captures a managed dump of a specified process without using a debugger. @@ -23,10 +26,10 @@ The default host address for these routes is `https://localhost:52323`. This rou | `pid` | query | false | int | The ID of the process. | | `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | | `name` | query | false | string | The name of the process. | -| `type` | query | false | [DumpType](definitions.md#DumpType) | The type of dump to capture. Default value is `WithHeap` | +| `type` | query | false | [DumpType](definitions.md#dumptype) | The type of dump to capture. Default value is `WithHeap` | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected dump. When not specified, the dump is written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, a dump of the [default process](defaultprocess.md) will be captured. Attempting to capture a dump of the default process when the default process cannot be resolved will fail. @@ -44,7 +47,7 @@ Allowed schemes: |---|---|---|---| | 200 OK | stream | A managed dump of the process. | `application/octet-stream` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many dump requests at this time. Try to request a dump at a later time. | | diff --git a/documentation/api/gcdump.md b/documentation/api/gcdump.md index 4fb80b3306c..963ca7a2b1f 100644 --- a/documentation/api/gcdump.md +++ b/documentation/api/gcdump.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fgcdump) + # GC Dump - Get Captures a GC dump of a specified process. These dumps are useful for several scenarios: @@ -29,7 +32,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `name` | query | false | string | The name of the process. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected GC dump. When not specified, the GC dump is written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, a GC dump of the [default process](defaultprocess.md) will be captured. Attempting to capture a GC dump of the default process when the default process cannot be resolved will fail. @@ -47,7 +50,7 @@ Allowed schemes: |---|---|---|---| | 200 OK | stream | A GC dump of the process. | `application/octet-stream` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many GC dump requests at this time. Try to request a GC dump at a later time. | `application/problem+json` | diff --git a/documentation/api/info.md b/documentation/api/info.md index 73e7edfd750..acb35043b9c 100644 --- a/documentation/api/info.md +++ b/documentation/api/info.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Finfo) + # Info - Get Gets information about the `dotnet monitor` version, the runtime version, and the diagnostic port settings. @@ -21,7 +24,7 @@ Authentication is enforced for this route. See [Authentication](./../authenticat | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | | Information about `dotnet monitor` formatted as JSON. | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples diff --git a/documentation/api/livemetrics-custom.md b/documentation/api/livemetrics-custom.md index e867d541176..ad404d9b48c 100644 --- a/documentation/api/livemetrics-custom.md +++ b/documentation/api/livemetrics-custom.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Flivemetrics-custom) + # Livemetrics - Get Custom Captures metrics for a process, with the ability to specify custom metrics. @@ -24,7 +27,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `durationSeconds` | query | false | int | The duration of the metrics operation in seconds. Default is `30`. Min is `-1` (indefinite duration). Max is `2147483647`. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected metrics. When not specified, the metrics are written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, artifacts for the [default process](defaultprocess.md) will be captured. Attempting to capture artifacts of the default process when the default process cannot be resolved will fail. @@ -38,7 +41,7 @@ Allowed schemes: ## Request Body -A request body of type [EventMetricsConfiguration](definitions.md#EventMetricsConfiguration) is required. +A request body of type [EventMetricsConfiguration](definitions.md#eventmetricsconfiguration) is required. The expected content type is `application/json`. @@ -46,9 +49,9 @@ The expected content type is `application/json`. | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | [Metric](./definitions.md/#Metric) | The metrics from the process formatted as json sequence. Each JSON object is a [metrics object](./definitions.md/#Metric)| `application/json-seq` | +| 200 OK | [Metric](./definitions.md#metric) | The metrics from the process formatted as json sequence. Each JSON object is a [metrics object](./definitions.md#metric)| `application/json-seq` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many requests at this time. Try to request metrics at a later time. | `application/problem+json` | diff --git a/documentation/api/livemetrics-get.md b/documentation/api/livemetrics-get.md index 8677852d248..b7712403a9c 100644 --- a/documentation/api/livemetrics-get.md +++ b/documentation/api/livemetrics-get.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Flivemetrics-get) + # Livemetrics - Get Captures metrics for a chosen process. @@ -26,7 +29,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `durationSeconds` | query | false | int | The duration of the metrics operation in seconds. Default is `30`. Min is `-1` (indefinite duration). Max is `2147483647`. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected metrics. When not specified, the metrics are written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, artifacts for the [default process](defaultprocess.md) will be captured. Attempting to capture artifacts of the default process when the default process cannot be resolved will fail. @@ -42,9 +45,9 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | [Metric](./definitions.md/#Metric) | The metrics from the process formatted as json sequence. | `application/json-seq` | +| 200 OK | [Metric](./definitions.md#metric) | The metrics from the process formatted as json sequence. | `application/json-seq` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many requests at this time. Try to request metrics at a later time. | `application/problem+json` | diff --git a/documentation/api/livemetrics.md b/documentation/api/livemetrics.md index 711c97a35b6..f3172f986cc 100644 --- a/documentation/api/livemetrics.md +++ b/documentation/api/livemetrics.md @@ -1,6 +1,9 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Flivemetrics) + # Live Metrics | Operation | Description | |---|---| | [Live Metrics](livemetrics-get.md) | Captures metrics using the default metric providers. | -| [Live Custom Metrics](livemetrics-custom.md) | Captures metrics using custom metric providers. | \ No newline at end of file +| [Live Custom Metrics](livemetrics-custom.md) | Captures metrics using custom metric providers. | diff --git a/documentation/api/logs-custom.md b/documentation/api/logs-custom.md index d7e0cb5eebe..654b400c2de 100644 --- a/documentation/api/logs-custom.md +++ b/documentation/api/logs-custom.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Flogs-custom) + # Logs - Get Custom Captures log statements that are logged to the [ILogger<> infrastructure](https://docs.microsoft.com/aspnet/core/fundamentals/logging) within a specified process, as described in the settings specified in the request body. By default, logs are collected at the levels as specified by the [application-defined configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/#configure-logging). @@ -26,7 +29,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `durationSeconds` | query | false | int | The duration of the log collection operation in seconds. Default is `30`. Min is `-1` (indefinite duration). Max is `2147483647`. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected logs. When not specified, the logs are written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, logs for the [default process](defaultprocess.md) will be captured. Attempting to capture logs of the default process when the default process cannot be resolved will fail. @@ -40,7 +43,7 @@ Allowed schemes: ## Request Body -A request body of type [LogsConfiguration](definitions.md#LogsConfiguration) is required. +A request body of type [LogsConfiguration](definitions.md#logsconfiguration) is required. The expected content type is `application/json`. @@ -48,10 +51,10 @@ The expected content type is `application/json`. | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | | The logs from the process formatted as [newline delimited JSON](https://github.com/ndjson/ndjson-spec). Each JSON object is a [LogEntry](definitions.md#LogEntry) | `application/x-ndjson` | +| 200 OK | | The logs from the process formatted as [newline delimited JSON](https://github.com/ndjson/ndjson-spec). Each JSON object is a [LogEntry](definitions.md#logentry) | `application/x-ndjson` | | 200 OK | | The logs from the process formatted as plain text, similar to the output of the JSON console formatter. | `text/plain` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many logs requests at this time. Try to request logs at a later time. | `application/problem+json` | diff --git a/documentation/api/logs-get.md b/documentation/api/logs-get.md index 32a323da936..ed525f85875 100644 --- a/documentation/api/logs-get.md +++ b/documentation/api/logs-get.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Flogs-get) + # Logs - Get Captures log statements that are logged to the [ILogger<> infrastructure](https://docs.microsoft.com/aspnet/core/fundamentals/logging) within a specified process. By default, logs are collected at the levels as specified by the [application-defined configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/#configure-logging). @@ -23,11 +26,11 @@ The default host address for these routes is `https://localhost:52323`. This rou | `pid` | query | false | int | The ID of the process. | | `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | | `name` | query | false | string | The name of the process. | -| `level` | query | false | [LogLevel](definitions.md#LogLevel) | The name of the log level at which log events are collected. If not specified, logs are collected levels as specified by the [application-defined configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/#configure-logging). | +| `level` | query | false | [LogLevel](definitions.md#loglevel) | The name of the log level at which log events are collected. If not specified, logs are collected levels as specified by the [application-defined configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/#configure-logging). | | `durationSeconds` | query | false | int | The duration of the log collection operation in seconds. Default is `30`. Min is `-1` (indefinite duration). Max is `2147483647`. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected logs. When not specified, the logs are written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, logs for the [default process](defaultprocess.md) will be captured. Attempting to capture logs of the default process when the default process cannot be resolved will fail. @@ -43,10 +46,10 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | | The logs from the process formatted as [newline delimited JSON](https://github.com/ndjson/ndjson-spec). Each JSON object is a [LogEntry](definitions.md#LogEntry) | `application/x-ndjson` | +| 200 OK | | The logs from the process formatted as [newline delimited JSON](https://github.com/ndjson/ndjson-spec). Each JSON object is a [LogEntry](definitions.md#logentry) | `application/x-ndjson` | | 200 OK | | The logs from the process formatted as plain text, similar to the output of the JSON console formatter. | `text/plain` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many logs requests at this time. Try to request logs at a later time. | `application/problem+json` | diff --git a/documentation/api/logs.md b/documentation/api/logs.md index 678cf154f26..4be9cf67694 100644 --- a/documentation/api/logs.md +++ b/documentation/api/logs.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Flogs) + # Logs The Logs API enables collecting logs that are logged to the [ILogger<> infrastructure](https://docs.microsoft.com/aspnet/core/fundamentals/logging) within a specified process. @@ -9,4 +12,4 @@ The Logs API enables collecting logs that are logged to the [ILogger<> infrastru | Operation | Description | |---|---| | [Get Logs](logs-get.md) | Captures log statements from a process at a specified level or at the application-defined categories and levels. | -| [Get Custom Logs](logs-custom.md) | Captures log statements from a process using the settings specified in the request body. | \ No newline at end of file +| [Get Custom Logs](logs-custom.md) | Captures log statements from a process using the settings specified in the request body. | diff --git a/documentation/api/metrics.md b/documentation/api/metrics.md index 56d2df18b4f..ed987afd734 100644 --- a/documentation/api/metrics.md +++ b/documentation/api/metrics.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fmetrics) + # Metrics - Get Gets a snapshot of metrics in the Prometheus exposition format of a single process. @@ -9,7 +12,7 @@ The metrics are collected from the following providers by default: All of the counters for each of these providers are collected by default. -> **NOTE:** This route collects metrics only from a single process. If there are no processes or more than one process, the endpoint will not return information. In order to facilitate observing a single process, the tool can be configured to listen for connections from a target process; see [Default Process Configuration](<../configuration.md#Default-Process-Configuration>) and [Diagnostic Port Configuration](<../configuration.md#Diagnostic-Port-Configuration>) for more details. +> **NOTE:** This route collects metrics only from a single process. If there are no processes or more than one process, the endpoint will not return information. In order to facilitate observing a single process, the tool can be configured to listen for connections from a target process; see [Default Process Configuration](<../configuration.md#default-process-configuration>) and [Diagnostic Port Configuration](<../configuration.md#diagnostic-port-configuration>) for more details. ## HTTP Route @@ -32,7 +35,7 @@ Authentication is not enforced for this route. | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | | A list of metrics for a single process in the Prometheus exposition format. | `text/plain` | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples @@ -160,4 +163,4 @@ systemruntime_active_timer_count 0 1618889186313 ### Custom Metrics -The metrics providers and counter names to return from this route can be specified via configuration. See [Metrics Configuration](<../configuration.md#Metrics-Configuration>) for more information. +The metrics providers and counter names to return from this route can be specified via configuration. See [Metrics Configuration](<../configuration.md#metrics-configuration>) for more information. diff --git a/documentation/api/operations-delete.md b/documentation/api/operations-delete.md index 9f70041ab37..50cddb21105 100644 --- a/documentation/api/operations-delete.md +++ b/documentation/api/operations-delete.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Foperations-delete) + # Operations - Delete Cancel a running operation. Only valid against operations in the `Running` state. Transitions the operation to `Cancelled` state. @@ -25,7 +28,7 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | | The operation was successfully cancelled. | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples @@ -49,4 +52,4 @@ HTTP/1.1 200 OK |---|---| | Windows | .NET Core 3.1, .NET 5+ | | Linux | .NET Core 3.1, .NET 5+ | -| MacOS | .NET Core 3.1, .NET 5+ | \ No newline at end of file +| MacOS | .NET Core 3.1, .NET 5+ | diff --git a/documentation/api/operations-get.md b/documentation/api/operations-get.md index e55e84d6fa9..6f29c433829 100644 --- a/documentation/api/operations-get.md +++ b/documentation/api/operations-get.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Foperations-get) + # Operations - Get Gets detailed information about a specific operation. @@ -24,8 +27,8 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | [OperationStatus](./definitions.md#OperationStatus) | Detailed status of the operation | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](./definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 200 OK | [OperationStatus](./definitions.md#operationstatus) | Detailed status of the operation | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](./definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples @@ -49,7 +52,13 @@ Content-Type: application/json "error": null, "operationId": "67f07e40-5cca-4709-9062-26302c484f18", "createdDateTime": "2021-07-21T06:21:15.315861Z", - "status": "Succeeded" + "status": "Succeeded", + "process": + { + "pid":1, + "uid":"95b0202a-4ed3-44a6-98f1-767d270ec783", + "name":"dotnet-monitor-demo" + } } ``` @@ -59,4 +68,4 @@ Content-Type: application/json |---|---| | Windows | .NET Core 3.1, .NET 5+ | | Linux | .NET Core 3.1, .NET 5+ | -| MacOS | .NET Core 3.1, .NET 5+ | \ No newline at end of file +| MacOS | .NET Core 3.1, .NET 5+ | diff --git a/documentation/api/operations-list.md b/documentation/api/operations-list.md index 167fa43c24c..aa5bc67cdcc 100644 --- a/documentation/api/operations-list.md +++ b/documentation/api/operations-list.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Foperations-list) + # Operations - List Lists all operations that have been created, as well as their status. @@ -5,13 +8,27 @@ Lists all operations that have been created, as well as their status. ## HTTP Route ```http -GET /operations HTTP/1.1 +GET /operations?pid={pid}&uid={uid}&name={name} HTTP/1.1 ``` ## Host Address The default host address for these routes is `https://localhost:52323`. This route is only available on the addresses configured via the `--urls` command line parameter and the `DOTNETMONITOR_URLS` environment variable. +## URI Parameters + +| Name | In | Required | Type | Description | +|---|---|---|---|---| +| `pid` | query | false | int | (6.3+) The ID of the process. | +| `uid` | query | false | guid | (6.3+) A value that uniquely identifies a runtime instance within a process. | +| `name` | query | false | string | (6.3+) The name of the process. | + +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. + +If none of `pid`, `uid`, or `name` are specified, all operations will be listed. + +> **NOTE:** If multiple processes match the provided parameters (e.g., two processes named "MyProcess"), the operations for all matching processes will be listed. + ## Authentication Authentication is enforced for this route. See [Authentication](./../authentication.md) for further information. @@ -24,13 +41,13 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | [OperationSummary](./definitions.md#OperationSummary)[] | An array of operation objects. | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](./definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 200 OK | [OperationSummary](./definitions.md#operationsummary)[] | An array of operation objects. | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](./definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples -### Sample Request +### Sample Request 1 ```http GET /operations HTTP/1.1 @@ -38,7 +55,7 @@ Host: localhost:52323 Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= ``` -### Sample Response +### Sample Response 1 ```http HTTP/1.1 200 OK @@ -46,10 +63,55 @@ Content-Type: application/json [ { - "operationId": "67f07e40-5cca-4709-9062-26302c484f18","createdDateTime": "2021-07-21T06:21:15.315861Z","status": "Succeeded" + "operationId": "67f07e40-5cca-4709-9062-26302c484f18", + "createdDateTime": "2021-07-21T06:21:15.315861Z", + "status": "Succeeded", + "process": + { + "pid":1, + "uid":"95b0202a-4ed3-44a6-98f1-767d270ec783", + "name":"dotnet-monitor-demo" + } }, { - "operationId": "26e74e52-0a16-4e84-84bb-27f904bfaf85","createdDateTime": "2021-07-21T23:30:22.3058272Z","status": "Failed" + "operationId": "26e74e52-0a16-4e84-84bb-27f904bfaf85", + "createdDateTime": "2021-07-21T23:30:22.3058272Z", + "status": "Failed", + "process": + { + "pid":11782, + "uid":"23c289b3-b5ce-428a-aaa8-c864b3766bc2", + "name":"dotnet-monitor-demo2" + } + } +] +``` + +### Sample Request 2 + +```http +GET /operations?name=dotnet-monitor-demo HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +``` + +### Sample Response 2 + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +[ + { + "operationId": "67f07e40-5cca-4709-9062-26302c484f18", + "createdDateTime": "2021-07-21T06:21:15.315861Z", + "status": "Succeeded", + "process": + { + "pid":1, + "uid":"95b0202a-4ed3-44a6-98f1-767d270ec783", + "name":"dotnet-monitor-demo" + } } ] ``` @@ -60,4 +122,4 @@ Content-Type: application/json |---|---| | Windows | .NET Core 3.1, .NET 5+ | | Linux | .NET Core 3.1, .NET 5+ | -| MacOS | .NET Core 3.1, .NET 5+ | \ No newline at end of file +| MacOS | .NET Core 3.1, .NET 5+ | diff --git a/documentation/api/operations.md b/documentation/api/operations.md index ea03ccd732b..1241fb49662 100644 --- a/documentation/api/operations.md +++ b/documentation/api/operations.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Foperations) + # Operations Operations are used to track long running operations in dotnet-monitor, specifically egressing data via egressProviders instead of directly to the client. This api is very similiar to [Azure asynchronous operations](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/async-operations#url-to-monitor-status). diff --git a/documentation/api/pidvsuid.md b/documentation/api/pidvsuid.md index 832e94637bc..37cfdf90ca9 100644 --- a/documentation/api/pidvsuid.md +++ b/documentation/api/pidvsuid.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fpidvsuid) + # Process ID `pid` vs Unique ID `uid` Many of the HTTP routes allow specifying either the process ID `pid` or the unique ID `uid`. Which one to use depends on the target process and the environment in which the process is running. diff --git a/documentation/api/process-env.md b/documentation/api/process-env.md index e93cf195a4e..e5023f43f40 100644 --- a/documentation/api/process-env.md +++ b/documentation/api/process-env.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fprocess-env) + # Processes - Get Environment Gets the environment block of a specified process. @@ -22,7 +25,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | | `name` | query | false | string | The name of the process. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, the environment block of the [default process](defaultprocess.md) will be provided. Attempting to get the environment block of the default process when the default process cannot be resolved will fail. @@ -39,7 +42,7 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| | 200 OK | map (of string) | The environment block of the specified process. | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples diff --git a/documentation/api/process-get.md b/documentation/api/process-get.md index 32b737f204b..70ee5a5c9a6 100644 --- a/documentation/api/process-get.md +++ b/documentation/api/process-get.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fprocess-get) + # Processes - Get Gets detailed information about a specified process. @@ -22,7 +25,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | | `name` | query | false | string | The name of the process. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, information about the [default process](defaultprocess.md) will be provided. Attempting to get information from the default process when the default process cannot be resolved will fail. @@ -38,8 +41,8 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | [ProcessInfo](definitions.md#ProcessInfo) | The detailed information about the specified process. | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 200 OK | [ProcessInfo](definitions.md#processinfo) | The detailed information about the specified process. | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples diff --git a/documentation/api/processes-list.md b/documentation/api/processes-list.md index d5726b0b80a..e1ae9cabc8f 100644 --- a/documentation/api/processes-list.md +++ b/documentation/api/processes-list.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fprocesses-list) + # Processes - List Lists the processes that are available from which diagnostic information can be obtained. @@ -26,8 +29,8 @@ Allowed schemes: | Name | Type | Description | Content Type | |---|---|---|---| -| 200 OK | [ProcessIdentifier](definitions.md#ProcessIdentifier)[] | An array of process identifier objects. | `application/json` | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 200 OK | [ProcessIdentifier](definitions.md#processidentifier)[] | An array of process identifier objects. | `application/json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | ## Examples @@ -68,4 +71,4 @@ Content-Type: application/json |---|---| | Windows | .NET Core 3.1, .NET 5+ | | Linux | .NET Core 3.1, .NET 5+ | -| MacOS | .NET Core 3.1, .NET 5+ | \ No newline at end of file +| MacOS | .NET Core 3.1, .NET 5+ | diff --git a/documentation/api/processes.md b/documentation/api/processes.md index c6b21bbcaf7..9913bab262f 100644 --- a/documentation/api/processes.md +++ b/documentation/api/processes.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fprocesses) + # Processes The Processes API enables enumeration of the processes that `dotnet monitor` can detect and allows for obtaining their metadata (such as their names and environment variables). @@ -10,4 +13,4 @@ The Processes API enables enumeration of the processes that `dotnet monitor` can | [Get Process Environment](process-env.md) | `.NET 5+` Gets the environment block of a specified process.
`.NET Core 3.1` Not supported. | | [List Processes](processes-list.md) | Lists the processes that are available from which diagnostic information can be obtained. | -The `dotnet monitor` tool is able to detect .NET Core 3.1 and .NET 5+ applications. When connecting to a .NET Core 3.1 application, some information may not be available and is called out in the documentation. \ No newline at end of file +The `dotnet monitor` tool is able to detect .NET Core 3.1 and .NET 5+ applications. When connecting to a .NET Core 3.1 application, some information may not be available and is called out in the documentation. diff --git a/documentation/api/stacks.md b/documentation/api/stacks.md new file mode 100644 index 00000000000..862f7056588 --- /dev/null +++ b/documentation/api/stacks.md @@ -0,0 +1,130 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Fstacks) + +# Stacks - Get + +Captures the call stacks of the currently running process. Note that only managed frames are collected. + +>**NOTE:** This feature is [experimental](./../experimental.md). To enable this feature, set `DotnetMonitor_Experimental_Feature_CallStacks` to `true` as an environment variable on the `dotnet monitor` process or container. Additionally, the [in-process features](./../configuration.md#experimental-in-process-features-configuration-70) must be enabled since the call stacks feature uses shared libraries loaded into the target application for collecting the call stack information. + +## HTTP Route + +```http +GET /stacks?pid={pid}&uid={uid}&name={name}&egressProvider={egressProvider} HTTP/1.1 +``` + +> **NOTE:** Process information (IDs, names, environment, etc) may change between invocations of these APIs. Processes may start or stop between API invocations, causing this information to change. + +## Host Address + +The default host address for these routes is `https://localhost:52323`. This route is only available on the addresses configured via the `--urls` command line parameter and the `DOTNETMONITOR_URLS` environment variable. + +## URI Parameters + +| Name | In | Required | Type | Description | +|---|---|---|---|---| +| `pid` | query | false | int | The ID of the process. | +| `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | +| `name` | query | false | string | The name of the process. | +| `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected stacks. When not specified, the stacks are written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | + +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. + +If none of `pid`, `uid`, or `name` are specified, a stack of the [default process](defaultprocess.md) will be captured. Attempting to capture a stack of the default process when the default process cannot be resolved will fail. + +## Authentication + +Authentication is enforced for this route. See [Authentication](./../authentication.md) for further information. + +Allowed schemes: +- `Bearer` +- `Negotiate` (Windows only, running as unelevated) + +## Responses + +| Name | Type | Description | Content Type | +|---|---|---|---| +| 200 OK | [CallStackResult](definitions.md#experimental-callstackresult-70) | Callstacks for all managed threads in the process. | `application/json` | +| 200 OK | text | Text representation of callstacks in the process. | `text/plain` | +| 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | +| 429 Too Many Requests | | There are too many stack requests at this time. Try to request a stack at a later time. | `application/problem+json` | + +## Examples + +### Sample Request + +```http +GET /stack?pid=21632 HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +Accept: application/json +``` + +### Sample Response + +```http +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "threadId": 30860, + "frames": [ + { + "methodName": "GetQueuedCompletionStatus", + "className": "Interop\u002BKernel32", + "moduleName": "System.Private.CoreLib.dll", + }, + { + "methodName": "WaitForSignal", + "className": "System.Threading.LowLevelLifoSemaphore", + "moduleName": "System.Private.CoreLib.dll", + }, + { + "methodName": "Wait", + "className": "System.Threading.LowLevelLifoSemaphore", + "moduleName": "System.Private.CoreLib.dll", + } +} +``` + +### Sample Request + +```http +GET /stack?pid=21632 HTTP/1.1 +Host: localhost:52323 +Authorization: Bearer fffffffffffffffffffffffffffffffffffffffffff= +Accept: text/plain +``` + +### Sample Response + +```http +HTTP/1.1 200 OK +Content-Type: text/plain + +Thread: (0x68C0) + System.Private.CoreLib.dll!System.Threading.Monitor.Wait + System.Private.CoreLib.dll!System.Threading.ManualResetEventSlim.Wait + System.Private.CoreLib.dll!System.Threading.Tasks.Task.SpinThenBlockingWait + System.Private.CoreLib.dll!System.Threading.Tasks.Task.InternalWaitCore + System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification + System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter.GetResult + Microsoft.Extensions.Hosting.Abstractions.dll!Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run + WebApplication5.dll!WebApplication5.Program.Main +``` + +## Supported Runtimes + +| Operating System | Runtime Version | +|---|---| +| Windows | .NET 6+ | +| Linux | .NET 6+ | +| MacOS | .NET 6+ | + +## Additional Notes + +### When to use `pid` vs `uid` + +See [Process ID `pid` vs Unique ID `uid`](pidvsuid.md) for clarification on when it is best to use either parameter. diff --git a/documentation/api/trace-custom.md b/documentation/api/trace-custom.md index bc277a747de..302215323e4 100644 --- a/documentation/api/trace-custom.md +++ b/documentation/api/trace-custom.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=trace-customdocumentation%2Fapi%2F) + # Trace - Get Custom Captures a diagnostic trace of a process using the given set of event providers specified in the request body. @@ -24,7 +27,7 @@ The default host address for these routes is `https://localhost:52323`. This rou | `durationSeconds` | query | false | int | The duration of the trace operation in seconds. Default is `30`. Min is `-1` (indefinite duration). Max is `2147483647`. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected trace. When not specified, the trace is written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, a trace of the [default process](defaultprocess.md) will be captured. Attempting to capture a trace of the default process when the default process cannot be resolved will fail. @@ -38,7 +41,7 @@ Allowed schemes: ## Request Body -A request body of type [EventProvidersConfiguration](definitions.md#EventProvidersConfiguration) is required. +A request body of type [EventProvidersConfiguration](definitions.md#eventprovidersconfiguration) is required. The expected content type is `application/json`. @@ -48,7 +51,7 @@ The expected content type is `application/json`. |---|---|---|---| | 200 OK | stream | A trace of the process. | `application/octet-stream` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many trace requests at this time. Try to request a trace at a later time. | `application/problem+json` | diff --git a/documentation/api/trace-get.md b/documentation/api/trace-get.md index ce353b6fcba..40c68261e9c 100644 --- a/documentation/api/trace-get.md +++ b/documentation/api/trace-get.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Ftrace-get) + # Trace - Get Captures a diagnostic trace of a process based on a predefined set of trace profiles. @@ -21,11 +24,11 @@ The default host address for these routes is `https://localhost:52323`. This rou | `pid` | query | false | int | The ID of the process. | | `uid` | query | false | guid | A value that uniquely identifies a runtime instance within a process. | | `name` | query | false | string | The name of the process. | -| `profile` | query | false | [TraceProfile](definitions.md#TraceProfile) | The name of the profile(s) used to collect events. See [TraceProfile](definitions.md#TraceProfile) for details on the list of event providers, levels, and keywords each profile represents. Multiple profiles may be specified by separating them with commas. Default is `Cpu,Http,Metrics` | +| `profile` | query | false | [TraceProfile](definitions.md#traceprofile) | The name of the profile(s) used to collect events. See [TraceProfile](definitions.md#traceprofile) for details on the list of event providers, levels, and keywords each profile represents. Multiple profiles may be specified by separating them with commas. Default is `Cpu,Http,Metrics` | | `durationSeconds` | query | false | int | The duration of the trace operation in seconds. Default is `30`. Min is `-1` (indefinite duration). Max is `2147483647`. | | `egressProvider` | query | false | string | If specified, uses the named egress provider for egressing the collected trace. When not specified, the trace is written to the HTTP response stream. See [Egress Providers](../egress.md) for more details. | -See [ProcessIdentifier](definitions.md#ProcessIdentifier) for more details about the `pid`, `uid`, and `name` parameters. +See [ProcessIdentifier](definitions.md#processidentifier) for more details about the `pid`, `uid`, and `name` parameters. If none of `pid`, `uid`, or `name` are specified, a trace of the [default process](defaultprocess.md) will be captured. Attempting to capture a trace of the default process when the default process cannot be resolved will fail. @@ -43,7 +46,7 @@ Allowed schemes: |---|---|---|---| | 200 OK | stream | A trace of the process. | `application/octet-stream` | | 202 Accepted | | When an egress provider is specified, the Location header containers the URI of the operation for querying the egress status. | | -| 400 Bad Request | [ValidationProblemDetails](definitions.md#ValidationProblemDetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | +| 400 Bad Request | [ValidationProblemDetails](definitions.md#validationproblemdetails) | An error occurred due to invalid input. The response body describes the specific problem(s). | `application/problem+json` | | 401 Unauthorized | | Authentication is required to complete the request. See [Authentication](./../authentication.md) for further information. | | | 429 Too Many Requests | | There are too many trace requests at this time. Try to request a trace at a later time. | `application/problem+json` | diff --git a/documentation/api/trace.md b/documentation/api/trace.md index 25c7d6ba8b4..6d87b583e9a 100644 --- a/documentation/api/trace.md +++ b/documentation/api/trace.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fapi%2Ftrace) + # Traces The Traces API enables collecting `.nettrace` formatted traces without using a profiler. @@ -7,4 +10,4 @@ The Traces API enables collecting `.nettrace` formatted traces without using a p | Operation | Description | |---|---| | [Get Trace](trace-get.md) | Captures a diagnostic trace of a process based on a predefined set of trace profiles. | -| [Get Custom Trace](trace-custom.md) | Captures a diagnostic trace of a process using the given set of event providers specified in the request body. | \ No newline at end of file +| [Get Custom Trace](trace-custom.md) | Captures a diagnostic trace of a process using the given set of event providers specified in the request body. | diff --git a/documentation/authentication.md b/documentation/authentication.md index 57204e65b9b..ac15b06250f 100644 --- a/documentation/authentication.md +++ b/documentation/authentication.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fauthentication) + # Authentication Authenticated requests to `dotnet monitor` help protect sensitive diagnostic artifacts from unauthorized users and lower privileged processes. `dotnet monitor` can be configured to use either [Windows Authentication](#windows-authentication) or via an [API Key](#api-key-authentication). It is possible, although strongly not recommended, to [disable authentication](#disabling-authentication). diff --git a/documentation/building.md b/documentation/building.md index a5145f00fc7..d075bc128e1 100644 --- a/documentation/building.md +++ b/documentation/building.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fbuilding) + # Clone, build and test the repo ------------------------------ diff --git a/documentation/collectionrules/collectionruleexamples.md b/documentation/collectionrules/collectionruleexamples.md index 8f7f7a5bb91..6a1efc1cd78 100644 --- a/documentation/collectionrules/collectionruleexamples.md +++ b/documentation/collectionrules/collectionruleexamples.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fcollectionrules%2Fcollectionruleexamples) + # Collection Rule Examples The following examples provide sample scenarios for using a collection rule. These templates can be copied directly into your configuration file with minimal adjustments to work with your application (for more information on configuring an egress provider, see [egress providers](./../configuration.md#egress-configuration)), or they can be adjusted for your specific use-case. [Learn more about configuring collection rules](collectionrules.md). diff --git a/documentation/collectionrules/collectionrules.md b/documentation/collectionrules/collectionrules.md index d61f0c41b9e..c9e30007678 100644 --- a/documentation/collectionrules/collectionrules.md +++ b/documentation/collectionrules/collectionrules.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fcollectionrules%2Fcollectionrules) + # Collection Rules `dotnet monitor` can be [configured](./../configuration.md#collection-rule-configuration) to automatically collect diagnostic artifacts based on conditions within the discovered processes. @@ -54,7 +57,9 @@ The following are the currently available actions: |---|---| | [CollectDump](./../configuration.md#collectdump-action) | Collects a memory dump of the target process. | | [CollectGCDump](./../configuration.md#collectgcdump-action) | Collects a gcdump of the target process. | +| [CollectLiveMetrics](./../configuration.md#collectlivemetrics-action) | Collects live metrics from the target process. | | [CollectLogs](./../configuration.md#collectlogs-action) | Collects logs from the target process. | +| [CollectStacks](./../configuration.md#collectstacks-action) | Collects call stacks from the target process. | | [CollectTrace](./../configuration.md#collecttrace-action) | Collects an event trace of the target process. | | [Execute](./../configuration.md#execute-action) | Executes an external executable with command line parameters. | | [LoadProfiler](./../configuration.md#loadprofiler-action) | Loads an ICorProfilerCallback implementation into the target process. | diff --git a/documentation/collectionrules/templates.md b/documentation/collectionrules/templates.md index 1958130e03e..5481759fa10 100644 --- a/documentation/collectionrules/templates.md +++ b/documentation/collectionrules/templates.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fcollectionrules%2Ftemplates) + # Templates Templates allow users to design reusable collection rule components to decrease configuration verbosity, reduce duplication between rules, and speed up the process of writing complex scenarios. @@ -291,4 +294,4 @@ The following example creates a template trigger named "HighRequestCount", two t - name: DotnetMonitor_CollectionRules__TraceWhenHighCPU__Filters__0__ProcessId value: "12345" ``` - \ No newline at end of file + diff --git a/documentation/collectionrules/triggershortcuts.md b/documentation/collectionrules/triggershortcuts.md index 285947d13ad..c594872ead0 100644 --- a/documentation/collectionrules/triggershortcuts.md +++ b/documentation/collectionrules/triggershortcuts.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fcollectionrules%2Ftriggershortcuts) + # Trigger Shortcuts These triggers simplify configuration for several common trigger use-cases. All of these shortcuts can be expressed as `EventCounter` triggers; however, these shortcuts provide improved defaults, range validation, and a simpler syntax. There are currently three built-in default triggers; additional trigger shortcuts may be added in future versions of `dotnet monitor`. diff --git a/documentation/compatibility/7.0/README.md b/documentation/compatibility/7.0/README.md new file mode 100644 index 00000000000..27ff46e385a --- /dev/null +++ b/documentation/compatibility/7.0/README.md @@ -0,0 +1,14 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fcompatibility%2F7.0%2FREADME) + +# Breaking Changes in 7.0 + +If you are migrating your usage to `dotnet monitor` 7.0, the following changes might affect you. Changes are grouped together by areas within the tool. + +## Installation + +| Area | Title | Introduced | +|--|--|--| +| Deployment | The tool will not run on .NET Core 3.1 or .NET 5 due to removal of `netcoreapp3.1` target framework; **NOTE:** The tool will still be able to monitor applications running these .NET versions. | Preview 1 | +| Docker | Docker container entrypoint has been split among entrypoint and cmd instructions | Preview 3 | +| Egress | Built-in metadata keys for Azure Blob egress now prefixed with `DotnetMonitor_` | Preview 8 | diff --git a/documentation/compatibility/README.md b/documentation/compatibility/README.md new file mode 100644 index 00000000000..ed4165388df --- /dev/null +++ b/documentation/compatibility/README.md @@ -0,0 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fcompatibility%2FREADME) + +# Breaking Changes + +- [Breaking Changes in 7.0](./7.0/README.md) diff --git a/documentation/configuration.md b/documentation/configuration.md index 19d6cbffd34..aa9fda95786 100644 --- a/documentation/configuration.md +++ b/documentation/configuration.md @@ -1,10 +1,15 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fconfiguration) + # Configuration `dotnet monitor` has extensive configuration to control various aspects of its behavior. Ordinarily, you are not required to specify most of this configuration and only exists if you wish to change the default behavior in `dotnet monitor`. +>**NOTE:** Some features are [experimental](./experimental.md) and are denoted as `**[Experimental]**` in this document. + ## Configuration Sources -`dotnet monitor` can read and combine configuration from multiple sources. The configuration sources are listed below in the order in which they are read (Environment variables are highest precedence) : +`dotnet monitor` can read and combine configuration from multiple sources. The configuration sources are listed below in the order in which they are read (User-specified json file is highest precedence) : - Command line parameters - User settings path @@ -16,6 +21,8 @@ - On \*nix, `/etc/dotnet-monitor` - Environment variables +- User-Specified json file + - (6.3+) Use the `--configuration-file-path` flag from the command line to specify your own configuration file (using its full path). ### Translating configuration between providers @@ -312,8 +319,50 @@ When operating in `Listen` mode, you can also specify the maximum number of inco ## Storage Configuration +Some diagnostic features (e.g. memory dumps, stack traces) require that a directory is shared between the `dotnet monitor` tool and the target applications. The `Storage` configuration section allows specifying these directories to facilitate this sharing. + +### Default Shared Path (7.0+) + +The default shared path option (`DefaultSharedPath`) can be set, which allows artifacts to be shared automatically without requiring additional configuration for each artifact type. By setting this property with an appropriate value, the following become available: +- dumps are temporarily stored in this directory or in a subdirectory. +- **[Experimental]** shared libraries are shared from `dotnet monitor` to target applications in this directory or in a subdirectory. +- **[Experimental]** in-process diagnostics share files back to `dotnet monitor` in this directory or in a subdirectory. + +
+ JSON + + ```json + { + "Storage": { + "DefaultSharedPath": "/diag" + } + } + ``` +
+ +
+ Kubernetes ConfigMap + + ```yaml + Storage__DefaultSharedPath: "/diag" + ``` +
+ +
+ Kubernetes Environment Variables + + ```yaml + - name: DotnetMonitor_Storage__DefaultSharedPath + value: "/diag" + ``` +
+ +### Dumps Path + Unlike the other diagnostic artifacts (for example, traces), memory dumps aren't streamed back from the target process to `dotnet monitor`. Instead, they are written directly to disk by the runtime. After successful collection of a process dump, `dotnet monitor` will read the process dump directly from disk. In the default configuration, the directory that the runtime writes its process dump to is the temp directory (`%TMP%` on Windows, `/tmp` on \*nix). It is possible to change to the ephemeral directory that these dump files get written to via the following configuration: +>**Note:** This option is optional if `dotnet monitor` is running in the same process namespace as the target processes or if `DefaultSharedPath` is specified. +
JSON @@ -343,6 +392,41 @@ Unlike the other diagnostic artifacts (for example, traces), memory dumps aren't ```
+### **[Experimental]** Shared Library Path (7.0+) + +The shared library path option (`SharedLibraryPath`) allows specifying the path to where shared libraries are copied from the `dotnet monitor` installation to make them available to target applications for in-process diagnostics scenarios, such as call stack collection. + +>**Note:** This option is not required if `DefaultSharedPath` is specified. This option provides an alternative directory path compared to the behavior of specifying `DefaultSharedPath`. + +
+ JSON + + ```json + { + "Storage": { + "SharedLibraryPath": "/diag/libs/" + } + } + ``` +
+ +
+ Kubernetes ConfigMap + + ```yaml + Storage__SharedLibraryPath: "/diag/libs/" + ``` +
+ +
+ Kubernetes Environment Variables + + ```yaml + - name: DotnetMonitor_Storage__SharedLibraryPath + value: "/diag/libs/" + ``` +
+ ## Default Process Configuration Default process configuration is used to determine which process is used for metrics and in situations where the process is not specified in the query to retrieve an artifact. A process must match all the specified filters. If a `Key` is not specified, the default is `ProcessId`. @@ -620,7 +704,7 @@ You can customize the number of data points stored per metric via the following ``` -See [Global Counter Interval](#Global-Counter-Interval) to change the metrics frequency. +See [Global Counter Interval](#global-counter-interval) to change the metrics frequency. ### Custom Metrics @@ -717,12 +801,17 @@ In addition to enabling custom providers, `dotnet monitor` also allows you to di | blobPrefix | string | false | Optional path prefix for the artifacts to egress.| | copyBufferSize | string | false | The buffer size to use when copying data from the original artifact to the blob stream.| | accountKey | string | false | The account key used to access the Azure blob storage account; must be specified if `accountKeyName` is not specified.| -| sharedAccessSignature | string | false | The shared access signature (SAS) used to access the azure blob storage account; if using SAS, must be specified if `sharedAccessSignatureName` is not specified.| +| sharedAccessSignature | string | false | The shared access signature (SAS) used to access the Azure blob and optionally queue storage accounts; if using SAS, must be specified if `sharedAccessSignatureName` is not specified.| | accountKeyName | string | false | Name of the property in the Properties section that will contain the account key; must be specified if `accountKey` is not specified.| | managedIdentityClientId | string | false | The ClientId of the ManagedIdentity that can be used to authorize egress. Note this identity must be used by the hosting environment (such as Kubernetes) and must also have a Storage role with appropriate permissions. | | sharedAccessSignatureName | string | false | Name of the property in the Properties section that will contain the SAS token; if using SAS, must be specified if `sharedAccessSignature` is not specified.| | queueName | string | false | The name of the queue to which a message will be dispatched upon writing to a blob.| -| queueAccountUri | string | false | The URI of the Azure queue account.| +| queueAccountUri | string | false | The URI of the Azure queue storage account.| +| queueSharedAccessSignature | string | false | (6.3+) The shared access signature (SAS) used to access the Azure queue storage account; if using SAS, must be specified if `queueSharedAccessSignatureName` is not specified.| +| queueSharedAccessSignatureName | string | false | (6.3+) Name of the property in the Properties section that will contain the queue SAS token; if using SAS, must be specified if `queueSharedAccessSignature` is not specified.| +| metadata | Dictionary | false | A mapping of metadata keys to environment variable names. The values of the environment variables will be added as metadata for egressed artifacts.| + +***Note:*** Starting with `dotnet monitor` 7.0, all built-in metadata keys are prefixed with `DotnetMonitor_`; to avoid metadata naming conflicts, avoid prefixing your metadata keys with `DotnetMonitor_`. ### Example azureBlobStorage provider @@ -993,7 +1082,7 @@ The following is a collection rule that collects a 1 minute CPU trace and egress ### Filters -Each collection rule can specify a set of process filters to select which processes the rule should be applied. The filter criteria are the same as those used for the [default process](#Default-Process-Configuration) configuration. +Each collection rule can specify a set of process filters to select which processes the rule should be applied. The filter criteria are the same as those used for the [default process](#default-process-configuration) configuration. #### Example @@ -1309,7 +1398,7 @@ An action that collects a dump of the process that the collection rule is target | Name | Type | Required | Description | Default Value | |---|---|---|---|---| -| `Type` | [DumpType](api/definitions.md#DumpType) | false | The type of dump to collect | `WithHeap` | +| `Type` | [DumpType](api/definitions.md#dumptype) | false | The type of dump to collect | `WithHeap` | | `Egress` | string | true | The named [egress provider](egress.md) for egressing the collected dump. | | ##### Outputs @@ -1408,12 +1497,13 @@ An action that collects a trace of the process that the collection rule is targe | Name | Type | Required | Description | Default Value | Min Value | Max Value | |---|---|---|---|---|---|---| -| `Profile` | [TraceProfile](api/definitions.md#TraceProfile)? | false | The name of the profile(s) used to collect events. See [TraceProfile](api/definitions.md#TraceProfile) for details on the list of event providers, levels, and keywords each profile represents. Multiple profiles may be specified by separating them with commas. Either `Profile` or `Providers` must be specified, but not both. | `null` | | | -| `Providers` | [EventProvider](api/definitions.md#EventProvider)[] | false | List of event providers from which to capture events. Either `Profile` or `Providers` must be specified, but not both. | `null` | | | +| `Profile` | [TraceProfile](api/definitions.md#traceprofile)? | false | The name of the profile(s) used to collect events. See [TraceProfile](api/definitions.md#traceprofile) for details on the list of event providers, levels, and keywords each profile represents. Multiple profiles may be specified by separating them with commas. Either `Profile` or `Providers` must be specified, but not both. | `null` | | | +| `Providers` | [EventProvider](api/definitions.md#eventprovider)[] | false | List of event providers from which to capture events. Either `Profile` or `Providers` must be specified, but not both. | `null` | | | | `RequestRundown` | bool | false | The runtime may provide additional type information for certain types of events after the trace session is ended. This additional information is known as rundown events. Without this information, some events may not be parsable into useful information. Only applies when `Providers` is specified. | `true` | | | | `BufferSizeMegabytes` | int | false | The size (in megabytes) of the event buffer used in the runtime. If the event buffer is filled, events produced by event providers may be dropped until the buffer is cleared. Increase the buffer size to mitigate this or pair down the list of event providers, keywords, and level to filter out extraneous events. Only applies when `Providers` is specified. | `256` | `1` | `1024` | | `Duration` | TimeSpan? | false | The duration of the trace operation. | `"00:00:30"` (30 seconds) | `"00:00:01"` (1 second) | `"1.00:00:00"` (1 day) | | `Egress` | string | true | The named [egress provider](egress.md) for egressing the collected trace. | | | | +| `StoppingEvent` | [TraceEventFilter](api/definitions.md#traceeventfilter)? | false | The event to watch for while collecting the trace, and once either the event is hit or the `Duration` is reached the trace will be stopped. This can only be specified if `Providers` is set. | `null` | | | ##### Outputs @@ -1456,6 +1546,103 @@ Usage that collects a CPU trace for 30 seconds and egresses it to a provider nam ``` +#### `CollectLiveMetrics` Action + +An action that collects live metrics for the process that the collection rule is targeting. + +##### Properties + +| Name | Type | Required | Description | Default Value | Min Value | Max Value | +|---|---|---|---|---|---|---| +| `IncludeDefaultProviders` | bool | false | Determines if the default counter providers should be used. | `true` | | | +| `Providers` | [EventMetricsProvider](api/definitions.md#eventmetricsprovider)[] | false | The array of providers for metrics to collect. | | | | +| `Duration` | TimeSpan? | false | The duration of the live metrics operation. | `"00:00:30"` (30 seconds) | `"00:00:01"` (1 second) | `"1.00:00:00"` (1 day) | +| `Egress` | string | true | The named [egress provider](egress.md) for egressing the collected live metrics. | | | | + +##### Outputs + +| Name | Description | +|---|---| +| `EgressPath` | The path of the file that was egressed using the specified egress provider. | + +##### Example + +Usage that collects live metrics with the default providers for 30 seconds and egresses it to a provider named "TmpDir". + +
+ JSON + + ```json + { + "Egress": "TmpDir" + } + ``` +
+ +
+ Kubernetes ConfigMap + + ```yaml + CollectionRules__RuleName__Actions__0__Settings__Egress: "TmpDir" + ``` +
+ +
+ Kubernetes Environment Variables + + ```yaml + - name: DotnetMonitor_CollectionRules__RuleName__Actions__0__Settings__Egress + value: "TmpDir" + ``` +
+ +##### Example + +Usage that collects live metrics for the `cpu-usage` counter on `System.Runtime` for 20 seconds and egresses it to a provider named "TmpDir". + +
+ JSON + + ```json + { + "UseDefaultProviders": false, + "Providers": [ + { + "ProviderName": "System.Runtime", + "CounterNames": [ "cpu-usage" ] + } + ], + "Egress": "TmpDir" + } + ``` +
+ +
+ Kubernetes ConfigMap + + ```yaml + CollectionRules__RuleName__Actions__0__Settings__UseDefaultProviders: "false" + CollectionRules__RuleName__Actions__0__Settings__Providers__0__ProviderName: "System.Runtime" + CollectionRules__RuleName__Actions__0__Settings__Providers__0__CounterNames__0: "cpu-usage" + CollectionRules__RuleName__Actions__0__Settings__Egress: "TmpDir" + ``` +
+ +
+ Kubernetes Environment Variables + + ```yaml + - name: DotnetMonitor_CollectionRules__RuleName__Actions__0__Settings__UseDefaultProviders + value: "false" + - name: DotnetMonitor_CollectionRules__RuleName__Actions__0__Settings__Providers__0__ProviderName + value: "System.Runtime" + - name: DotnetMonitor_CollectionRules__RuleName__Actions__0__Settings__Providers__0__CounterNames__0 + value: "cpu-usage" + - name: DotnetMonitor_CollectionRules__RuleName__Actions__0__Settings__Egress + value: "TmpDir" + ``` +
+ #### `CollectLogs` Action An action that collects logs for the process that the collection rule is targeting. @@ -1464,10 +1651,10 @@ An action that collects logs for the process that the collection rule is targeti | Name | Type | Required | Description | Default Value | Min Value | Max Value | |---|---|---|---|---|---|---| -| `DefaultLevel` | [LogLevel](api/definitions.md#LogLevel)? | false | The default log level at which logs are collected for entries in the FilterSpecs that do not have a specified LogLevel value. | `LogLevel.Warning` | | | -| `FilterSpecs` | Dictionary | false | A custom mapping of logger categories to log levels that describes at what level a log statement that matches one of the given categories should be captured. | `null` | | | +| `DefaultLevel` | [LogLevel](api/definitions.md#loglevel)? | false | The default log level at which logs are collected for entries in the FilterSpecs that do not have a specified LogLevel value. | `LogLevel.Warning` | | | +| `FilterSpecs` | Dictionary | false | A custom mapping of logger categories to log levels that describes at what level a log statement that matches one of the given categories should be captured. | `null` | | | | `UseAppFilters` | bool | false | Specifies whether to capture log statements at the levels as specified in the application-defined filters. | `true` | | | -| `Format` | [LogFormat](api/definitions.md#LogFormat)? | false | The format of the logs artifact. | `PlainText` | | | +| `Format` | [LogFormat](api/definitions.md#logformat)? | false | The format of the logs artifact. | `PlainText` | | | | `Duration` | TimeSpan? | false | The duration of the logs operation. | `"00:00:30"` (30 seconds) | `"00:00:01"` (1 second) | `"1.00:00:00"` (1 day) | | `Egress` | string | true | The named [egress provider](egress.md) for egressing the collected logs. | | | | @@ -1569,6 +1756,19 @@ Usage that executes a .NET executable named "myapp.dll" using `dotnet`. ``` +#### **[Experimental]** `CollectStacks` Action (7.0+) + +Collect call stacks from the target process. + +>**NOTE:** This feature is [experimental](./experimental.md). To enable this feature, set `DotnetMonitor_Experimental_Feature_CallStacks` to `true` as an environment variable on the `dotnet monitor` process or container. Additionally, the [in-process features](#experimental-in-process-features-configuration-70) must be enabled since the call stacks feature uses shared libraries loaded into the target application for collecting the call stack information. + +##### Properties + +| Name | Type | Required | Description | Default Value | +|---|---|---|---|---| +| `Format` | [CallStackFormat](api/definitions.md#experimental-callstackformat-70) | false | The format of the collected call stack. | `Json` | +| `Egress` | string | true | The named [egress provider](egress.md) for egressing the collected stacks. | | + #### `LoadProfiler` Action An action that loads an [ICorProfilerCallback](https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/icorprofilercallback-interface) implementation into a target process as a startup profiler. This action must be used in a collection rule with a `Startup` trigger. @@ -1819,7 +2019,7 @@ Collection rule defaults are specified in configuration as a named item under th | Name | Section | Type | Applies To | |---|---|---|---| -| `Egress` | `Actions` | string | [CollectDump](#collectdump-action), [CollectGCDump](#collectgcdump-action), [CollectTrace](#collecttrace-action), [CollectLogs](#collectlogs-action) | +| `Egress` | `Actions` | string | [CollectDump](#collectdump-action), [CollectGCDump](#collectgcdump-action), [CollectTrace](#collecttrace-action), [CollectLiveMetrics](#collectlivemetrics-action), [CollectLogs](#collectlogs-action) | | `SlidingWindowDuration` | `Triggers` | TimeSpan? | [AspNetRequestCount](#aspnetrequestcount-trigger), [AspNetRequestDuration](#aspnetrequestduration-trigger), [AspNetResponseStatus](#aspnetresponsestatus-trigger), [EventCounter](#eventcounter-trigger) | | `RequestCount` | `Triggers` | int | [AspNetRequestCount](#aspnetrequestcount-trigger), [AspNetRequestDuration](#aspnetrequestduration-trigger) | | `ResponseCount` | `Triggers` | int | [AspNetResponseStatus](#aspnetresponsestatus-trigger) | @@ -1938,3 +2138,44 @@ The following example includes a default egress provider that corresponds to the value: "monitorBlob" ``` + +## **[Experimental]** In-Process Features Configuration (7.0+) + +Some features of `dotnet monitor` require loading libraries into target applications. These libraries ship with `dotnet monitor` and are provisioned to be available to target applications using the `DefaultSharedPath` option in the [storage configuration](#storage-configuration) section. The following features require these in-process libraries to be used: + +- Call stack collection + +Because these libraries are loaded into the target application (they are not loaded into `dotnet monitor`), they may have performance impact on memory and CPU utilization in the target application. These features are off by default and may be enabled via the `InProcessFeatures` configuration section. + +### Example + +To enable in-process features, such as call stack collection, use the following configuration: + +
+ JSON + + ```json + { + "InProcessFeatures": { + "Enabled": true + } + } + ``` +
+ +
+ Kubernetes ConfigMap + + ```yaml + InProcessFeatures__Enabled: "true" + ``` +
+ +
+ Kubernetes Environment Variables + + ```yaml + - name: DotnetMonitor_InProcessFeatures__Enabled + value: "true" + ``` +
diff --git a/documentation/docker.md b/documentation/docker.md index 419d8f7e86e..25e6d7c4adc 100644 --- a/documentation/docker.md +++ b/documentation/docker.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fdocker) + # Running in Docker In addition to its availability as a .NET CLI tool, the `dotnet monitor` tool is available as a prebuilt Docker image that can be run in container runtimes and orchestrators. @@ -8,21 +11,27 @@ In addition to its availability as a .NET CLI tool, the `dotnet monitor` tool is | Version | Platform | Architecture | Link | |---|---|---|---| -| 7.0 (Preview) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/7.0/alpine/amd64/Dockerfile | -| 7.0 (Preview) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/7.0/alpine/arm64v8/Dockerfile | -| 6.1 (Current, Latest) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/6.1/alpine/amd64/Dockerfile | -| 6.0 | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/6.0/alpine/amd64/Dockerfile | +| 7.0 (RC) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/7.0/alpine/amd64/Dockerfile | +| 7.0 (RC) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/7.0/alpine/arm64v8/Dockerfile | +| 6.3 (Current, Latest) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/6.3/alpine/amd64/Dockerfile | +| 6.3 (Current, Latest) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/6.3/alpine/arm64v8/Dockerfile | +| 6.2 | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/6.2/alpine/amd64/Dockerfile | +| 6.2 | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/main/src/monitor/6.2/alpine/arm64v8/Dockerfile | ### Nightly Dockerfiles | Version | Platform | Architecture | Link | |---|---|---|---| -| 7.0 (Preview, Latest) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/7.0/alpine/amd64/Dockerfile | -| 7.0 (Preview, Latest) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/7.0/alpine/arm64v8/Dockerfile | -| 6.2 (Preview) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.2/alpine/amd64/Dockerfile | -| 6.2 (Preview) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.2/alpine/arm64v8/Dockerfile | -| 6.1 (Current) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.1/alpine/amd64/Dockerfile | -| 6.0 | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.0/alpine/amd64/Dockerfile | +| 7.0 (RC, Latest) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/7.0/alpine/amd64/Dockerfile | +| 7.0 (RC, Latest) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/7.0/alpine/arm64v8/Dockerfile | +| 7.0 (RC, Latest) | Linux (Ubuntu, Chiseled) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/7.0/ubuntu-chiseled/amd64/Dockerfile | +| 7.0 (RC, Latest) | Linux (Ubuntu, Chiseled) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/7.0/ubuntu-chiseled/arm64v8/Dockerfile | +| 6.3 (Current) | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.3/alpine/amd64/Dockerfile | +| 6.3 (Current) | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.3/alpine/arm64v8/Dockerfile | +| 6.3 (Current) | Linux (Ubuntu, Chiseled) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.3/ubuntu-chiseled/amd64/Dockerfile | +| 6.3 (Current) | Linux (Ubuntu, Chiseled) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.3/ubuntu-chiseled/arm64v8/Dockerfile | +| 6.2 | Linux (Alpine) | amd64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.2/alpine/amd64/Dockerfile | +| 6.2 | Linux (Alpine) | arm64 | https://github.com/dotnet/dotnet-docker/blob/nightly/src/monitor/6.2/alpine/arm64v8/Dockerfile | ## Image Repositories diff --git a/documentation/egress.md b/documentation/egress.md index 7e0df06044c..8c4c92a223f 100644 --- a/documentation/egress.md +++ b/documentation/egress.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fegress) + # Egress Providers `dotnet monitor` supports configuration of [egress providers](./configuration.md#egress-configuration) that can be used to egress artifacts externally, instead of to the client. This is supported for dumps, gcdumps, traces, logs, and live metrics. Currently supported providers are Azure blob storage and filesystem. diff --git a/documentation/experimental.md b/documentation/experimental.md new file mode 100644 index 00000000000..0699badaa68 --- /dev/null +++ b/documentation/experimental.md @@ -0,0 +1,9 @@ +# Experimental Features + +Some features are offered as experimental, meaning that they are not supported however they can be enabled to evaluate their current state. These features may or may not ship with full support in future releases and can be redesigned or removed in any future release. + +The following are the current set of experimental features: + +| Name | Description | First Available Version | How to Enable | +|---|---|---|---| +| Call Stacks | Collect call stacks from target processes as a diagnostic artifact using either the `/stacks` route or the `CollectStacks` collection rule action. | 7.0 RC 1 | Set `DotnetMonitor_Experimental_Feature_CallStacks` to `true` as an environment variable on the `dotnet monitor` process or container. | diff --git a/documentation/kubernetes.md b/documentation/kubernetes.md index e56d829b7ad..08aba0e9ae8 100644 --- a/documentation/kubernetes.md +++ b/documentation/kubernetes.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fkubernetes) + # Running in Kubernetes In addition to its availability as a .NET CLI tool, the `dotnet monitor` tool is available as a prebuilt Docker image that can be run in container runtimes and orchestrators, such as Kubernetes. @@ -113,6 +116,6 @@ resources: How much memory and CPU is consumed by dotnet-monitor is dependent on which scenarios are being executed: - Metrics consume a negligible amount of resources, although using custom metrics can affect this. - Operations such as traces and logs may require memory in the main application container that will automatically be allocated by the runtime. -- Resource consumption by trace operations is also dependent on which providers are enabled, as well as the [buffer size](./api/definitions.md#EventProvidersConfiguration) allocated in the runtime. -- It is not recommended to use highly verbose [log levels](./api/definitions.md#LogLevel) while under load. This causes a lot of CPU usage in the dotnet-monitor container and more memory pressure in the main application container. +- Resource consumption by trace operations is also dependent on which providers are enabled, as well as the [buffer size](./api/definitions.md#eventprovidersconfiguration) allocated in the runtime. +- It is not recommended to use highly verbose [log levels](./api/definitions.md#loglevel) while under load. This causes a lot of CPU usage in the dotnet-monitor container and more memory pressure in the main application container. - Dumps also temporarily increase the amount of memory consumed by the application container. diff --git a/documentation/localmachine.md b/documentation/localmachine.md index 0a11ceebee2..2e124cc1ed3 100644 --- a/documentation/localmachine.md +++ b/documentation/localmachine.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Flocalmachine) + # Running on a local machine `dotnet monitor` can be installed as a global tool providing observability, diagnostics artifact collection, and triggering in local development and testing scenarios. You can run `dotnet tool install -g dotnet-monitor` to install the latest version, see the full details [here](./setup.md#net-core-global-tool). @@ -10,7 +13,7 @@ There are a number of local development scenarios that are much more efficient w ### Local configuration -To monitor a specific local process you can use the settings file to define a default process. This ensures `dotnet monitor` automatically collects artifacts and logs based on a process criteria you have identified (e.g. process name, processs id, etc.). +To monitor a specific local process you can use the settings file to define a default process. This ensures `dotnet monitor` automatically collects artifacts and logs based on a process criteria you have identified (e.g., process name, process id, etc.). Defining a default process on Windows requires creating a settings file in the user path (`%USERPROFILE%\.dotnet-monitor\settings.json`). In the following example the default process has a process name of __BuggyDemoWeb__. @@ -28,7 +31,7 @@ Defining a default process on Windows requires creating a settings file in the u ### dotnet monitor collection -To start using `dotnet monitor`, run the following command from a Powershell or Command prompt: +To start using `dotnet monitor`, run the following command from a PowerShell or Command prompt: ```cmd dotnet-monitor collect diff --git a/documentation/official-build-instructions.md b/documentation/official-build-instructions.md index 4c88fb8c77a..a9696f4bec3 100644 --- a/documentation/official-build-instructions.md +++ b/documentation/official-build-instructions.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fofficial-build-instructions) + # Official Build Instructions > *WARNING*: These instructions will only work internally at Microsoft. @@ -9,4 +12,4 @@ This signs and publishes the following packages to the [dotnet-tools](https://pk The packages are only published to the feed from builds of the `main` and `release/*` branches. -The release process is documented at [Release Process](./release-process.md). \ No newline at end of file +The release process is documented at [Release Process](./release-process.md). diff --git a/documentation/openapi.json b/documentation/openapi.json index 3dfe5bcdac4..fa32bedb132 100644 --- a/documentation/openapi.json +++ b/documentation/openapi.json @@ -49,9 +49,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -60,9 +58,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -70,9 +66,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } } ], @@ -110,9 +104,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -121,9 +113,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -131,9 +121,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } } ], @@ -174,9 +162,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -185,9 +171,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -195,9 +179,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -213,9 +195,7 @@ "in": "query", "description": "The egress provider to which the dump is saved.", "schema": { - "type": "string", - "description": "The egress provider to which the dump is saved.", - "nullable": true + "type": "string" } } ], @@ -241,7 +221,7 @@ } }, "202": { - "description": "Success" + "description": "Accepted" } } } @@ -260,9 +240,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -271,9 +249,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -281,9 +257,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -291,9 +265,7 @@ "in": "query", "description": "The egress provider to which the GC dump is saved.", "schema": { - "type": "string", - "description": "The egress provider to which the GC dump is saved.", - "nullable": true + "type": "string" } } ], @@ -319,7 +291,7 @@ } }, "202": { - "description": "Success" + "description": "Accepted" } } } @@ -338,9 +310,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -349,9 +319,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -359,9 +327,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -380,7 +346,6 @@ "maximum": 2147483647, "minimum": -1, "type": "integer", - "description": "The duration of the trace session (in seconds).", "format": "int32", "default": 30 } @@ -390,9 +355,7 @@ "in": "query", "description": "The egress provider to which the trace is saved.", "schema": { - "type": "string", - "description": "The egress provider to which the trace is saved.", - "nullable": true + "type": "string" } } ], @@ -418,7 +381,7 @@ } }, "202": { - "description": "Success" + "description": "Accepted" } } }, @@ -435,9 +398,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -446,9 +407,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -456,9 +415,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -469,7 +426,6 @@ "maximum": 2147483647, "minimum": -1, "type": "integer", - "description": "The duration of the trace session (in seconds).", "format": "int32", "default": 30 } @@ -479,9 +435,7 @@ "in": "query", "description": "The egress provider to which the trace is saved.", "schema": { - "type": "string", - "description": "The egress provider to which the trace is saved.", - "nullable": true + "type": "string" } } ], @@ -528,7 +482,7 @@ } }, "202": { - "description": "Success" + "description": "Accepted" } } } @@ -547,9 +501,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -558,9 +510,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -568,9 +518,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -581,7 +529,6 @@ "maximum": 2147483647, "minimum": -1, "type": "integer", - "description": "The duration of the logs session (in seconds).", "format": "int32", "default": 30 } @@ -599,9 +546,7 @@ "in": "query", "description": "The egress provider to which the logs are saved.", "schema": { - "type": "string", - "description": "The egress provider to which the logs are saved.", - "nullable": true + "type": "string" } } ], @@ -636,7 +581,7 @@ } }, "202": { - "description": "Success" + "description": "Accepted" } } }, @@ -653,9 +598,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -664,9 +607,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -674,9 +615,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -687,7 +626,6 @@ "maximum": 2147483647, "minimum": -1, "type": "integer", - "description": "The duration of the logs session (in seconds).", "format": "int32", "default": 30 } @@ -697,9 +635,7 @@ "in": "query", "description": "The egress provider to which the logs are saved.", "schema": { - "type": "string", - "description": "The egress provider to which the logs are saved.", - "nullable": true + "type": "string" } } ], @@ -754,7 +690,7 @@ } }, "202": { - "description": "Success" + "description": "Accepted" } } } @@ -786,6 +722,197 @@ } } }, + "/collectionrules": { + "get": { + "tags": [ + "Diag" + ], + "summary": "Gets a brief summary about the current state of the collection rules.", + "operationId": "GetCollectionRulesDescription", + "parameters": [ + { + "name": "pid", + "in": "query", + "description": "Process ID used to identify the target process.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "uid", + "in": "query", + "description": "The Runtime instance cookie used to identify the target process.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "name", + "in": "query", + "description": "Process name used to identify the target process.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "$ref": "#/components/responses/BadRequestResponse" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedResponse" + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CollectionRuleDescription" + } + } + } + } + } + } + } + }, + "/collectionrules/{collectionRuleName}": { + "get": { + "tags": [ + "Diag" + ], + "summary": "Gets detailed information about the current state of the specified collection rule.", + "operationId": "GetCollectionRuleDetailedDescription", + "parameters": [ + { + "name": "collectionRuleName", + "in": "path", + "description": "The name of the collection rule for which a detailed description should be provided.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pid", + "in": "query", + "description": "Process ID used to identify the target process.", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "uid", + "in": "query", + "description": "The Runtime instance cookie used to identify the target process.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "name", + "in": "query", + "description": "Process name used to identify the target process.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "$ref": "#/components/responses/BadRequestResponse" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedResponse" + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CollectionRuleDetailedDescription" + } + } + } + } + } + } + }, + "/stacks": { + "get": { + "tags": [ + "Diag" + ], + "operationId": "CaptureStacks", + "parameters": [ + { + "name": "pid", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "uid", + "in": "query", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "egressProvider", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "$ref": "#/components/responses/BadRequestResponse" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedResponse" + }, + "429": { + "$ref": "#/components/responses/TooManyRequestsResponse" + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "202": { + "description": "Accepted" + } + } + } + }, "/livemetrics": { "get": { "tags": [ @@ -800,9 +927,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -811,9 +936,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -821,9 +944,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -834,7 +955,6 @@ "maximum": 2147483647, "minimum": -1, "type": "integer", - "description": "The duration of the metrics session (in seconds).", "format": "int32", "default": 30 } @@ -844,9 +964,7 @@ "in": "query", "description": "The egress provider to which the metrics are saved.", "schema": { - "type": "string", - "description": "The egress provider to which the metrics are saved.", - "nullable": true + "type": "string" } } ], @@ -861,7 +979,7 @@ "$ref": "#/components/responses/TooManyRequestsResponse" }, "202": { - "description": "Success" + "description": "Accepted" } } }, @@ -878,9 +996,7 @@ "description": "Process ID used to identify the target process.", "schema": { "type": "integer", - "description": "Process ID used to identify the target process.", - "format": "int32", - "nullable": true + "format": "int32" } }, { @@ -889,9 +1005,7 @@ "description": "The Runtime instance cookie used to identify the target process.", "schema": { "type": "string", - "description": "The Runtime instance cookie used to identify the target process.", - "format": "uuid", - "nullable": true + "format": "uuid" } }, { @@ -899,9 +1013,7 @@ "in": "query", "description": "Process name used to identify the target process.", "schema": { - "type": "string", - "description": "Process name used to identify the target process.", - "nullable": true + "type": "string" } }, { @@ -912,7 +1024,6 @@ "maximum": 2147483647, "minimum": -1, "type": "integer", - "description": "The duration of the metrics session (in seconds).", "format": "int32", "default": 30 } @@ -922,9 +1033,7 @@ "in": "query", "description": "The egress provider to which the metrics are saved.", "schema": { - "type": "string", - "description": "The egress provider to which the metrics are saved.", - "nullable": true + "type": "string" } } ], @@ -960,7 +1069,7 @@ "$ref": "#/components/responses/TooManyRequestsResponse" }, "202": { - "description": "Success" + "description": "Accepted" } } } @@ -994,16 +1103,45 @@ "tags": [ "Operations" ], - "responses": { - "401": { - "$ref": "#/components/responses/UnauthorizedResponse" + "summary": "Gets the operations list for the specified process (or all processes if left unspecified).", + "parameters": [ + { + "name": "pid", + "in": "query", + "description": "Process ID used to identify the target process.", + "schema": { + "type": "integer", + "format": "int32" + } }, - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "array", + { + "name": "uid", + "in": "query", + "description": "The Runtime instance cookie used to identify the target process.", + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "name", + "in": "query", + "description": "Process name used to identify the target process.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "401": { + "$ref": "#/components/responses/UnauthorizedResponse" + }, + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "array", "items": { "$ref": "#/components/schemas/OperationSummary" } @@ -1035,7 +1173,7 @@ "$ref": "#/components/responses/UnauthorizedResponse" }, "201": { - "description": "Success", + "description": "Created", "content": { "application/json": { "schema": { @@ -1084,137 +1222,104 @@ }, "components": { "schemas": { - "ValidationProblemDetails": { + "CollectionRuleDescription": { "type": "object", "properties": { - "type": { - "type": "string", - "nullable": true + "state": { + "$ref": "#/components/schemas/CollectionRuleState" }, - "title": { + "stateReason": { "type": "string", + "description": "Human-readable explanation for the current state of the collection rule.", "nullable": true - }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true - }, - "instance": { - "type": "string", - "nullable": true - }, - "errors": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "nullable": true, - "readOnly": true } }, - "additionalProperties": { } + "additionalProperties": false }, - "ProcessIdentifier": { + "CollectionRuleDetailedDescription": { "type": "object", "properties": { - "pid": { - "type": "integer", - "format": "int32" + "state": { + "$ref": "#/components/schemas/CollectionRuleState" }, - "uid": { - "type": "string", - "format": "uuid" - }, - "name": { + "stateReason": { "type": "string", + "description": "Human-readable explanation for the current state of the collection rule.", "nullable": true }, - "isDefault": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "ProcessInfo": { - "type": "object", - "properties": { - "pid": { + "lifetimeOccurrences": { "type": "integer", + "description": "The number of times the trigger has executed for a process in its lifetime.", "format": "int32" }, - "uid": { - "type": "string", - "format": "uuid" + "slidingWindowOccurrences": { + "type": "integer", + "description": "The number of times the trigger has executed for a process in the current sliding window duration (as defined by Limits).", + "format": "int32" }, - "name": { - "type": "string", - "nullable": true + "actionCountLimit": { + "type": "integer", + "description": "The number of times the trigger can execute for a process before being limited (as defined by Limits).", + "format": "int32" }, - "commandLine": { - "type": "string", - "nullable": true + "actionCountSlidingWindowDurationLimit": { + "$ref": "#/components/schemas/TimeSpan" }, - "operatingSystem": { - "type": "string", - "nullable": true + "slidingWindowDurationCountdown": { + "$ref": "#/components/schemas/TimeSpan" }, - "processArchitecture": { - "type": "string", - "nullable": true + "ruleFinishedCountdown": { + "$ref": "#/components/schemas/TimeSpan" } }, "additionalProperties": false }, - "DumpType": { + "CollectionRuleState": { "enum": [ - "Full", - "Mini", - "WithHeap", - "Triage" + "Running", + "ActionExecuting", + "Throttled", + "Finished" ], "type": "string" }, - "ProblemDetails": { + "DiagnosticPortConnectionMode": { + "enum": [ + "Connect", + "Listen" + ], + "type": "string" + }, + "DotnetMonitorInfo": { "type": "object", "properties": { - "type": { + "version": { "type": "string", + "description": "The dotnet monitor version.", "nullable": true }, - "title": { + "runtimeVersion": { "type": "string", + "description": "The dotnet runtime version.", "nullable": true }, - "status": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "detail": { - "type": "string", - "nullable": true + "diagnosticPortMode": { + "$ref": "#/components/schemas/DiagnosticPortConnectionMode" }, - "instance": { + "diagnosticPortName": { "type": "string", + "description": "The name of the named pipe or unix domain socket to use for connecting to the diagnostic server.", "nullable": true } }, - "additionalProperties": { } + "additionalProperties": false }, - "TraceProfile": { + "DumpType": { "enum": [ - "Cpu", - "Http", - "Logs", - "Metrics" + "Full", + "Mini", + "WithHeap", + "Triage" ], "type": "string" }, @@ -1229,25 +1334,35 @@ ], "type": "string" }, - "EventPipeProvider": { + "EventMetricsConfiguration": { + "type": "object", + "properties": { + "includeDefaultProviders": { + "type": "boolean" + }, + "providers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventMetricsProvider" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "EventMetricsProvider": { "required": [ - "name" + "providerName" ], "type": "object", "properties": { - "name": { + "providerName": { + "minLength": 1, "type": "string" }, - "keywords": { - "type": "string", - "nullable": true - }, - "eventLevel": { - "$ref": "#/components/schemas/EventLevel" - }, - "arguments": { - "type": "object", - "additionalProperties": { + "counterNames": { + "type": "array", + "items": { "type": "string" }, "nullable": true @@ -1280,6 +1395,33 @@ }, "additionalProperties": false }, + "EventPipeProvider": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "minLength": 1, + "type": "string" + }, + "keywords": { + "type": "string", + "nullable": true + }, + "eventLevel": { + "$ref": "#/components/schemas/EventLevel" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "LogLevel": { "enum": [ "Trace", @@ -1316,71 +1458,38 @@ }, "additionalProperties": false }, - "DiagnosticPortConnectionMode": { - "enum": [ - "Connect", - "Listen" - ], - "type": "string" - }, - "DotnetMonitorInfo": { + "OperationError": { "type": "object", "properties": { - "version": { - "type": "string", - "description": "The dotnet monitor version.", - "nullable": true - }, - "runtimeVersion": { + "code": { "type": "string", - "description": "The dotnet runtime version.", "nullable": true }, - "diagnosticPortMode": { - "$ref": "#/components/schemas/DiagnosticPortConnectionMode" - }, - "diagnosticPortName": { + "message": { "type": "string", - "description": "The name of the named pipe or unix domain socket to use for connecting to the diagnostic server.", "nullable": true } }, "additionalProperties": false }, - "EventMetricsProvider": { - "required": [ - "providerName" - ], + "OperationProcessInfo": { "type": "object", "properties": { - "providerName": { - "type": "string" + "pid": { + "type": "integer", + "format": "int32" }, - "counterNames": { - "type": "array", - "items": { - "type": "string" - }, - "nullable": true - } - }, - "additionalProperties": false - }, - "EventMetricsConfiguration": { - "type": "object", - "properties": { - "includeDefaultProviders": { - "type": "boolean" + "uid": { + "type": "string", + "format": "uuid" }, - "providers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EventMetricsProvider" - }, + "name": { + "type": "string", "nullable": true } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Represents the details of a given process used in an operation." }, "OperationState": { "enum": [ @@ -1391,6 +1500,34 @@ ], "type": "string" }, + "OperationStatus": { + "type": "object", + "properties": { + "operationId": { + "type": "string", + "format": "uuid" + }, + "createdDateTime": { + "type": "string", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/OperationState" + }, + "process": { + "$ref": "#/components/schemas/OperationProcessInfo" + }, + "resourceLocation": { + "type": "string", + "nullable": true + }, + "error": { + "$ref": "#/components/schemas/OperationError" + } + }, + "additionalProperties": false, + "description": "Represents the state of a long running operation. Used for all types of results, including successes and failures." + }, "OperationSummary": { "type": "object", "properties": { @@ -1404,49 +1541,198 @@ }, "status": { "$ref": "#/components/schemas/OperationState" + }, + "process": { + "$ref": "#/components/schemas/OperationProcessInfo" } }, "additionalProperties": false, "description": "Represents a partial model when enumerating all operations." }, - "OperationError": { + "ProblemDetails": { "type": "object", "properties": { - "code": { + "type": { "type": "string", "nullable": true }, - "message": { + "title": { "type": "string", "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": { } + }, + "ProcessIdentifier": { + "type": "object", + "properties": { + "pid": { + "type": "integer", + "format": "int32" + }, + "uid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "isDefault": { + "type": "boolean" } }, "additionalProperties": false }, - "OperationStatus": { + "ProcessInfo": { "type": "object", "properties": { - "operationId": { + "pid": { + "type": "integer", + "format": "int32" + }, + "uid": { "type": "string", "format": "uuid" }, - "createdDateTime": { + "name": { "type": "string", - "format": "date-time" + "nullable": true + }, + "commandLine": { + "type": "string", + "nullable": true + }, + "operatingSystem": { + "type": "string", + "nullable": true + }, + "processArchitecture": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "TimeSpan": { + "type": "object", + "properties": { + "ticks": { + "type": "integer", + "format": "int64" + }, + "days": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "hours": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "milliseconds": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "minutes": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "seconds": { + "type": "integer", + "format": "int32", + "readOnly": true + }, + "totalDays": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalHours": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalMilliseconds": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalMinutes": { + "type": "number", + "format": "double", + "readOnly": true + }, + "totalSeconds": { + "type": "number", + "format": "double", + "readOnly": true + } + }, + "additionalProperties": false + }, + "TraceProfile": { + "enum": [ + "Cpu", + "Http", + "Logs", + "Metrics" + ], + "type": "string" + }, + "ValidationProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true }, "status": { - "$ref": "#/components/schemas/OperationState" + "type": "integer", + "format": "int32", + "nullable": true }, - "resourceLocation": { + "detail": { "type": "string", "nullable": true }, - "error": { - "$ref": "#/components/schemas/OperationError" + "instance": { + "type": "string", + "nullable": true + }, + "errors": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "nullable": true, + "readOnly": true } }, - "additionalProperties": false, - "description": "Represents the state of a long running operation. Used for all types of results, including successes and failures." + "additionalProperties": { } } }, "responses": { diff --git a/documentation/release-process.md b/documentation/release-process.md index 40193b5092c..c417edbe22e 100644 --- a/documentation/release-process.md +++ b/documentation/release-process.md @@ -1,13 +1,16 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Frelease-process) + # Release Process -## Merge to Release Branch +## Prepare the release branch -1. Merge from the `main` branch to the appropriate release branch (e.g. `release/5.0`). Note that for patch releases, fixes should be made directly to the appropriate release branch and we do not merge from the `main` branch. -1. In `/eng/Versions.props`, update `dotnet/diagnostics` dependencies to versions from the corresponding release of the `dotnet/diagnostics` repo. -1. In `/eng/Version.props`, ensure that `` is set appropriately. See the documentation next to this setting for the appropriate values. In release branches, its value should be `release`. This setting, in combination with the version settings, determine for which 'channel' the aks.ms links are created. +1. Merge from the `main` branch to the appropriate release branch (e.g. `release/5.0`). Note that for patch releases, fixes should be made directly to the appropriate release branch and we do not merge from the `main` branch. Note that it is acceptable to use a release/major.x branch. Alternatively, you can create a new release branch for the minor version. See [additional branch steps](#additional-steps-when-creating-a-new-release-branch) below. +1. Review and merge in any outstanding dependabot PRs for the release branch. +1. Run the [Update release version](https://github.com/dotnet/dotnet-monitor/actions/workflows/update-release-version.yml) workflow, setting `Use workflow from` to the release branch and correctly setting the `Release type` and `Release version` options. (*NOTE:* Release version should include only major.minor.patch, without any extra labels). Review and merge in the PR created by this workflow. +1. If you merged from `main` in step 1, repeat the above step for the `main` branch with the appropriate `Release type` and `Release version`. 1. Complete at least one successful [release build](#build-release-branch). 1. [Update dotnet-docker pipeline variables](#update-pipeline-variable-for-release) to pick up builds from the release branch. -1. Bump the version number in the `main` branch and reset release notes. [Example Pull Request](https://github.com/dotnet/dotnet-monitor/pull/1560). ## Additional steps when creating a new release branch @@ -26,6 +29,7 @@ The official build will not automatically trigger for release branches. Each tim 1. Wait for changes to be mirrored from [GitHub repository](https://github.com/dotnet/dotnet-monitor) to the [internal repository](https://dev.azure.com/dnceng/internal/_git/dotnet-dotnet-monitor). 1. Invoke the [internal pipeline](https://dev.azure.com/dnceng/internal/_build?definitionId=954) for the release branch. +1. Bump the versions across feature branches. See https://github.com/dotnet/dotnet-monitor/pull/1973/files for an example. The result of the successful build pushes packages to the [dotnet-tools](https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json) feed, pushes symbols to symbol feeds, and generates aka.ms links for the following: - `aka.ms/dotnet/diagnostics/monitor{channel}/dotnet-monitor.nupkg.version` @@ -33,7 +37,6 @@ The result of the successful build pushes packages to the [dotnet-tools](https:/ The `channel` value is used by the `dotnet-docker` repository to consume the correct latest version. This value is: - `{major}.{minor}/daily` for builds from non-release branches. For example, `channel` is `5.0/daily` for the `main` branch. -- `{major}.{minor}/{preReleaseVersionLabel}.{preReleaseVersionIteration}` for non-final releases in release branches. For example, `channel` is `5.0/preview.5` for the `release/5.0` branch. - `{majorVersion}.{minorVersion}/release` for final release in release branches. For example, `channel` is `5.0/release` for the `release/5.0` if its `` is set to `release`. ## Update Nightly Docker Ingestion @@ -42,20 +45,41 @@ The `channel` value is used by the `dotnet-docker` repository to consume the cor The `dotnet-docker` repository runs an update process each day that detects the latest version of a given `dotnet-monitor` channel. During the stabilization/testing/release period for a release of `dotnet-monitor`, the update process should be changed to pick up builds for the release branch. +**Known issues** +* You may not have permissions to change these variables. +* Currently docker only supports updating one minor version for each major version. We have to manually update any additional versions. See [instructions](#manually-updating-docker-versions) for manually updating. + The following variables for [dotnet-docker-update-dependencies](https://dev.azure.com/dnceng/internal/_build?definitionId=470) need to be updated for release: * `monitorXMinorVersion`: Make sure these are set to the correct values. * `monitorXQuality`: Normally this is daily, but should be set to release. -* `monitorXStableBranding`: Normally this is false, but should be set to true when the package version is stable e.g. `dotnet-monitor.8.0.0.nupkg` (does not have a prerelease label on it such as `-preview.X` or `-rtm.X`. * `update-monitor-enabled`: Make sure this is true. * `update-dotnet-enabled`: When doing an ad-hoc run, make sure to **disable** this. +### Updating tags + +If you are releasing a new minor version, you may need to update the current/preview tags as well as the shared tag pool. +1. Update https://github.com/dotnet/dotnet-docker/blob/nightly/eng/mcr-tags-metadata-templates/monitor-tags.yml. +1. Update https://github.com/dotnet/dotnet-docker/blob/nightly/manifest.json. +1. Run update-dependencies as described [here](#manually-updating-docker-versions). +1. See https://github.com/dotnet/dotnet-docker/pull/3830/files for an example. + ### Revert Pipeline Variable After Release After the release has been completed, this pipeline variable should be changed to the appropriate daily channel (e.g. `6.0/daily`). +### Manually updating docker versions +1. Run `\eng\Set-DotnetVersions.ps1`. Example: +``` powershell +.\Set-DotnetVersions.ps1 6.1 -MonitorVersion 6.1.2-servicing.22306.3 +.\Set-DotnetVersions.ps1 6.2 -MonitorVersion 6.2.0-rtm.22306.2 +.\Set-DotnetVersions.ps1 7.0 -MonitorVersion 7.0.0-preview.5.22306.5 +``` +1. See https://github.com/dotnet/dotnet-docker/pull/3828 for sample result. + ### Updating dependencies -If necessary, update dependencies in the release branch. Most commonly this means picking up a new version of diagnostics packages. +If necessary, update dependencies in the release branch. +>**Note:** This is no longer needed for the diagnostics packages. They are kept up-to-date by dependabot. 1. For new branches only, you need to setup a subscription using darc: `darc add-subscription --channel ".NET Core Tooling Release" --source-repo https://github.com/dotnet/diagnostics --target-repo https://github.com/dotnet/dotnet-monitor --target-branch release/8.x --update-frequency None --standard-automerge` 1. Use `darc get-subscriptions --target-repo monitor` to see existing subscriptions. @@ -70,16 +94,15 @@ The nightly image is `mcr.microsoft.com/dotnet/nightly/monitor`. The tag list is ## Stabilization 1. Fix issues for the release in the release branch. Backport fixes to `main` branch and other prior release branches as needed. -1. Invoke [build](<#Build Release Branch>) pipeline as needed. +1. Invoke [build](#build-release-branch) pipeline as needed. 1. After successful build, test changes from [dotnet-tools](https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json) feed. Images from the `nightly` branch of the `dotnet-docker` repository will be recreated the next day after the successful build of the release branch. ## Release to nuget.org and Add GitHub Release -1. Grab the file [/documentation/releaseNotes/releaseNotes.md](https://github.com/dotnet/dotnet-monitor/blob/release/6.0/documentation/releaseNotes/releaseNotes.md) from the release branch and check this file into [main](https://github.com/dotnet/dotnet-monitor/tree/main) as [/documentation/releaseNotes/releaseNotes.v{NugetVersionNumber}.md](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v6.0.0-preview.8.21503.3.md). ->**Note:** this file comes from the **release** branch and is checked into the **main** branch. -2. Start [release pipeline](https://dev.azure.com/dnceng/internal/_release?_a=releases&view=mine&definitionId=105). Allow the stages to trigger automatically (do not check the boxes in the associated dropdown). During creation of the release you must select the dotnet-monitor build to release from the list of available builds. This must be a build with the tag `MonitorRelease` and the associated `MonitorRelease` artifact (set `dotnet-monitor_build` to the pipeline run of `dotnet monitor` that is being released; set `dotnet-monitor_source` to the latest commit from `main`). -3. The release will start the stage "Pre-Release Verification"; this will check that the above steps were done as expected. The name of the release will be updated automatically. -4. Approve the sign-off step the day before the release after 8:45 AM PDT, when ready to publish. +1. Run the [Generate release notes](https://github.com/dotnet/dotnet-monitor/actions/workflows/generate-release-notes.yml) workflow, setting `Use workflow from` to the release branch and checking `Include PRs that were merged into main?` if you merged `main` into the release branch. Review and merge in the PR created by this workflow. +1. Start [release pipeline](https://dev.azure.com/dnceng/internal/_release?_a=releases&view=mine&definitionId=105). Allow the stages to trigger automatically (do not check the boxes in the associated dropdown). During creation of the release you must select the dotnet-monitor build to release from the list of available builds. This must be a build with the tag `MonitorRelease` and the associated `MonitorRelease` artifact (set `dotnet-monitor_build` to the pipeline run of `dotnet monitor` that is being released; set `dotnet-monitor_source` to the latest commit from `main`). +1. The release will start the stage "Pre-Release Verification"; this will check that the above steps were done as expected. The name of the release will be updated automatically. +1. Approve the sign-off step the day before the release after 8:45 AM PDT, when ready to publish. >**Note:** After sign-off of the "Pre-Release Verification" environment the NuGet and GitHub release steps will automatically wait until 8:45 AM PDT the next day to correspond with the typical docker release time of 9:00 AM PDT. The remainder of the release will automatically push NuGet packages to nuget.org, [tag](https://github.com/dotnet/dotnet-monitor/tags) the commit from the build with the release version, and add a new [GitHub release](https://github.com/dotnet/dotnet-monitor/releases). @@ -92,7 +115,7 @@ The remainder of the release will automatically push NuGet packages to nuget.org ## Release Docker Images 1. Contact `dotnet-docker` team with final version that should be released. This version should be latest version in the `nightly` branch. -1. Docker image build from main branch requires assets to be published to `dotnetcli` and `dotnetclichecksums` storage accounts. See [Release to Storage Accounts](#Release-to-Storage-Accounts). +1. Docker image build from main branch requires assets to be published to `dotnetcli` and `dotnetclichecksums` storage accounts. See [Release to Storage Accounts](#release-to-storage-accounts). 1. The `dotnet-docker` team will merge from `nightly` branch to `main` branch and wait for `dotnet-monitor` team approval. Typically, these changes are completed the day before the release date. 1. The `dotnet-docker` team will start the build ahead of the release and wait for the all-clear from `dotnet-monitor` team before publishing the images. @@ -103,3 +126,5 @@ The release image is `mcr.microsoft.com/dotnet/monitor`. The tag list is https:/ 1. Update [releases.md](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releases.md) with the latest version. 1. When necessary, update [docker.md](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/docker.md). 1. When necessary, update this document if its instructions were unclear or incorrect. +1. When releasing a new minor version, include an announcement that the previous version will soon be out of support. For example, https://github.com/dotnet/dotnet-monitor/discussions/1871 +1. Make sure you [Revert](#revert-pipeline-variable-after-release) the nightly build pipeline. diff --git a/documentation/releaseNotes/README.md b/documentation/releaseNotes/README.md index b975e818d08..f91d3df3168 100644 --- a/documentation/releaseNotes/README.md +++ b/documentation/releaseNotes/README.md @@ -1,19 +1,8 @@ # Release Notes This folder contains the release notes that appear here: [https://github.com/dotnet/dotnet-monitor/releases](https://github.com/dotnet/dotnet-monitor/releases). -During development, notes for current features can be added to [`releaseNotes.md`](https://github.com/dotnet/dotnet-monitor/tree/main/documentation/releaseNotes/releaseNotes.md) +## Creation +Release notes can be created by running the [Generate release notes](https://github.com/dotnet/dotnet-monitor/actions/workflows/generate-release-notes.yml) workflow. This workflow will generate release notes for a given branch, corretly name it according to the format described in [File Naming](#file-naming), and open a PR with the new file. ## File Naming -Release notes will be named one of 3 different things based on their use: - -- `releaseNotes.[fullVersionNumber].md` for a released version. This is set to archive release notes from a version that has been released. `fullVersionNumber` should be the version assigned on the github release page, for example `releaseNotes.v5.0.0-preview.6.21370.3.md` would be the name of the Preview 6 release notes (if they were archived). -- [`releaseNotes.md`](https://github.com/dotnet/dotnet-monitor/tree/main/documentation/releaseNotes/releaseNotes.md) represents release notes for the code in the current branch that hasn't been released yet. -- [`releaseNotes.template.md`](https://github.com/dotnet/dotnet-monitor/tree/main/documentation/releaseNotes/releaseNotes.template.md) is the template to be used to create new `releaseNotes.md` in new versions. - -## Rotation -These are the steps during a release describing when and how release notes should be modified: - -- The `main` branch will merge into `release/*.*`. -- [`releaseNotes.md`](https://github.com/dotnet/dotnet-monitor/tree/main/documentation/releaseNotes/releaseNotes.md) (in `main`) can be overwritten by [`releaseNotes.template.md`](https://github.com/dotnet/dotnet-monitor/tree/main/documentation/releaseNotes/releaseNotes.template.md). -- A final build should be produced from the internal official release pipeline. This will have a version like `v5.0.0-preview.6.21370.3` -- Release notes from [`releaseNotes.md`](https://github.com/dotnet/dotnet-monitor/tree/release/5.0/documentation/releaseNotes/releaseNotes.md) (in `release/5.0` or the current release branch) should be copied into `main` as `releaseNotes.[fullVersionNumber].md` where `fullVersionNumber` is the same as the source tag that will be created during release, like `v5.0.0-preview.6.21370.3`. +Release notes are named in the format `releaseNotes.v[fullVersionNumber].md` for a released version. This is set to archive release notes from a version that has been released. `fullVersionNumber` should be the version assigned on the github release page, for example `releaseNotes.v5.0.0-preview.6.21370.3.md` would be the name of the 5.0 Preview 6 release notes (if they were archived). diff --git a/documentation/releaseNotes/releaseNotes.md b/documentation/releaseNotes/releaseNotes.md deleted file mode 100644 index 86c9c6bb0d3..00000000000 --- a/documentation/releaseNotes/releaseNotes.md +++ /dev/null @@ -1,6 +0,0 @@ -Today we are releasing the next official preview of the `dotnet monitor` tool. This release includes: - -- ⚠️ [Here is a breaking change we did and its work item] (#737) -- [Here is a new feature we added and its work item] (#737) - -\*⚠️ **_indicates a breaking change_** \ No newline at end of file diff --git a/documentation/releaseNotes/releaseNotes.template.md b/documentation/releaseNotes/releaseNotes.template.md deleted file mode 100644 index 7267ef4b08b..00000000000 --- a/documentation/releaseNotes/releaseNotes.template.md +++ /dev/null @@ -1,6 +0,0 @@ -Today we are releasing the next official preview of the `dotnet monitor` tool. This release includes: - -- ⚠️ [Here is a breaking change we did and it's work item] (#737) -- [Here is a new feature we added and it's work item] (#737) - -\*⚠️ **_indicates a breaking change_** diff --git a/documentation/releaseNotes/releaseNotes.v5.0.0-preview.7.21425.3.md b/documentation/releaseNotes/releaseNotes.v5.0.0-preview.7.21425.3.md index c4ee7b56eae..904bb7c99ac 100644 --- a/documentation/releaseNotes/releaseNotes.v5.0.0-preview.7.21425.3.md +++ b/documentation/releaseNotes/releaseNotes.v5.0.0-preview.7.21425.3.md @@ -6,4 +6,4 @@ Today we are releasing the next official preview of the `dotnet-monitor` tool. T - The `generatekey` command has one new command line parameter, `--output`. With this parameter, the output format of the Authentication configuration may be specified to be one of: `Json`, `Text`, `Cmd`, `PowerShell` or `Shell`. See [Generating an API Key](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/authentication.md#generating-an-api-key) for details. (#589) - ⚠️ The `generatekey` command line parameters `--hash-algorithm` and `--key-length` have been removed. (#247) -\*⚠️ **_indicates a breaking change_** \ No newline at end of file +\*⚠️ **_indicates a breaking change_** diff --git a/documentation/releaseNotes/releaseNotes.v6.0.0-preview.8.21503.3.md b/documentation/releaseNotes/releaseNotes.v6.0.0-preview.8.21503.3.md index 931c3f1746a..d95bbb5e3be 100644 --- a/documentation/releaseNotes/releaseNotes.v6.0.0-preview.8.21503.3.md +++ b/documentation/releaseNotes/releaseNotes.v6.0.0-preview.8.21503.3.md @@ -1,9 +1,9 @@ Today we are releasing the next official preview of the `dotnet-monitor` tool. This release includes: - Added a new HTTP route (`/livemetrics`) to collect metrics on demand. (#68) -- Added collection rules for automated collection of diagnostic artifacts based on trigger conditions in target applications. See [Collection Rules](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/collectionrules.md) for more details. +- Added collection rules for automated collection of diagnostic artifacts based on trigger conditions in target applications. See [Collection Rules](https://github.com/dotnet/dotnet-monitor/blob/v6.0.0-preview.8.21503.3/documentation/collectionrules.md) for more details. - Updated process detection to cancel waiting on unresponsive processes. -- Documented recommended container limits. See [Running in Kubernetes](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/kubernetes.md) for more details. +- Documented recommended container limits. See [Running in Kubernetes](https://github.com/dotnet/dotnet-monitor/blob/v6.0.0-preview.8.21503.3/documentation/kubernetes.md) for more details. - ⚠️ Upgraded runtime framework dependency from .NET Core 3.1 to .NET 6 - ⚠️ Reversioned from 5.0.0 to 6.0.0 - ⚠️ Fix all counter intervals to single global option (#923) diff --git a/documentation/releaseNotes/releaseNotes.v6.1.2.md b/documentation/releaseNotes/releaseNotes.v6.1.2.md new file mode 100644 index 00000000000..19a921133fe --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.1.2.md @@ -0,0 +1,6 @@ +Today we are releasing the 6.1.2 build of the `dotnet monitor` tool. This release includes: + +- Ensure unexpected egress failures complete operations (#1935) +- Prevent process enumeration from pruning processes that are capturing gcdumps (#1933) +- Prevent auth configuration warnings when using `--no-auth` switch (#1851) +- Show warning when using collection rules in `connect` mode (#1852) diff --git a/documentation/releaseNotes/releaseNotes.v6.1.3.md b/documentation/releaseNotes/releaseNotes.v6.1.3.md new file mode 100644 index 00000000000..c336f771ec8 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.1.3.md @@ -0,0 +1,4 @@ +Today we are releasing the 6.1.3 build of the `dotnet monitor` tool. This release includes: + +- Better exception handling and logging for certain scenarios (#2053) +- Updated Newtonsoft.Json to 13.0.1 (#2065) diff --git a/documentation/releaseNotes/releaseNotes.v6.1.4.md b/documentation/releaseNotes/releaseNotes.v6.1.4.md new file mode 100644 index 00000000000..51f4081e542 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.1.4.md @@ -0,0 +1,4 @@ +Today we are releasing the 6.1.4 build of the `dotnet monitor` tool. This release includes: + +- Update Azure.Storage.Blobs to 12.13.0 (#2220) +- Update Azure.Storage.Queues to 12.11.0 (#2220) diff --git a/documentation/releaseNotes/releaseNotes.v6.2.0.md b/documentation/releaseNotes/releaseNotes.v6.2.0.md new file mode 100644 index 00000000000..7565544eb30 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.2.0.md @@ -0,0 +1,14 @@ +Today we are releasing the 6.2.0 build of the `dotnet monitor` tool. This release includes: + +- Allow for pushing a message to a queue when writing to Azure egress (#163) +- Allow for simplified process filter configuration (#636) +- Allow `config show` command to list configuration sources (#277) +- Added collection rule defaults (#1595) +- Added collection rule templates (#1921) +- Added CPU usage, GC heap size, and threadpool queue length triggers (#1911) +- Added Managed Service Identity support for egress (#1884) +- Added Process.RuntimeId token for collection rule action properties (#1870) +- Fix egress operations to report failed egress to operation service (#1884) +- Prevent process enumeration from pruning processes that are capturing gcdumps (#1927) +- Show warning when using collection rules in `connect` mode (#1844) +- Prevent auth configuration warnings when using `--no-auth` switch (#1845) diff --git a/documentation/releaseNotes/releaseNotes.v6.2.1.md b/documentation/releaseNotes/releaseNotes.v6.2.1.md new file mode 100644 index 00000000000..3b8f0968630 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.2.1.md @@ -0,0 +1,4 @@ +Today we are releasing the 6.2.1 build of the `dotnet monitor` tool. This release includes: + +- Better exception handling and logging for certain scenarios (#2051) +- Updated Newtonsoft.Json to 13.0.1 (#2064) diff --git a/documentation/releaseNotes/releaseNotes.v6.2.2.md b/documentation/releaseNotes/releaseNotes.v6.2.2.md new file mode 100644 index 00000000000..6882b8705a6 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.2.2.md @@ -0,0 +1,7 @@ +Today we are releasing the 6.2.2 build of the `dotnet monitor` tool. This release includes: + +- Delete endpoint on startup (#2183) +- Fixed duplication of metrics (#2177) +- Fixed memory leak in authentication handler (#2172) +- Update Azure.Storage.Blobs to 12.13.0 (#2219) +- Update Azure.Storage.Queues to 12.11.0 (#2219) diff --git a/documentation/releaseNotes/releaseNotes.v6.3.0.md b/documentation/releaseNotes/releaseNotes.v6.3.0.md new file mode 100644 index 00000000000..0310a909df7 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v6.3.0.md @@ -0,0 +1,9 @@ +Today we are releasing the 6.3.0 build of the `dotnet monitor` tool. This release includes: + +- Add metadata for Azure egress (#2371) +- Support restrictive SAS tokens for Azure egress (#2325, #2394) +- Avoid Kestrel address override warning (#2576) +- Check read permissions for JSON configuration files (#2272) +- Improve `operations` Api to show RuntimeInstanceId and support filtering (#2265, #2360) +- Fixed an issue (#2526) where the `config` command could throw an exception (#2551) +- Fixed an issue where running `config show --show-sources` could incorrectly report `ChainedConfigurationProvider` as the source (#2553) \ No newline at end of file diff --git a/documentation/releaseNotes/releaseNotes.v7.0.0-preview.5.22306.5.md b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.5.22306.5.md new file mode 100644 index 00000000000..7313420c424 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.5.22306.5.md @@ -0,0 +1,10 @@ +Today we are releasing the next official preview of the `dotnet monitor` tool. This release includes: + +- Added collection rule templates (#1797) +- Added CPU usage, GC heap size, and threadpool queue length triggers (#1751) +- Added Managed Service Identity support for egress (#1928) +- Added Process.RuntimeId token for collection rule action properties (#1947) +- Fix egress operations to report failed egress to operation service (#1928) +- Prevent process enumeration from pruning processes that are capturing gcdumps (#1932) +- Show warning when using collection rules in `connect` mode (#1830) +- Prevent auth configuration warnings when using `--no-auth` switch (#1838) diff --git a/documentation/releaseNotes/releaseNotes.v7.0.0-preview.6.22330.7.md b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.6.22330.7.md new file mode 100644 index 00000000000..2c2aba93cd5 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.6.22330.7.md @@ -0,0 +1,6 @@ +Today we are releasing the next official preview of the `dotnet monitor` tool. This release includes: + +- Added HTTP route `/collectionrules` to inspect collection rule state (#1829) +- Allow specifying JSON configuration file via command line (#1990) +- Better exception handling and logging for certain scenarios (#2022) +- Updated Newtonsoft.Json to 13.0.1 (#2055) diff --git a/documentation/releaseNotes/releaseNotes.v7.0.0-preview.7.22401.1.md b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.7.22401.1.md new file mode 100644 index 00000000000..a73a1315df6 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.7.22401.1.md @@ -0,0 +1,8 @@ +Today we are releasing the next official preview of the `dotnet monitor` tool. This release includes: + +- Delete endpoint on startup (#2178) +- Fixed memory leak in authentication handler (#2165) +- Fixed duplication of metrics (#2149) +- Add `CollectLiveMetrics` collection rule action (#2119) +- Update Azure.Storage.Blobs to 12.13.0 (#2218) +- Update Azure.Storage.Queues to 12.11.0 (#2218) diff --git a/documentation/releaseNotes/releaseNotes.v7.0.0-preview.8.22457.4.md b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.8.22457.4.md new file mode 100644 index 00000000000..87037299d3a --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v7.0.0-preview.8.22457.4.md @@ -0,0 +1,6 @@ +Today we are releasing the next official preview of the `dotnet monitor` tool. This release includes: + +- Add metadata for Azure egress (#2290) +- Support restrictive SAS tokens for Azure egress (#2322) +- Improve `operations` Api to show RuntimeInstanceId and support filtering (#2136, #2312) +- Check read permissions for JSON configuration files (#2232) diff --git a/documentation/releaseNotes/releaseNotes.v7.0.0-rc.1.22479.8.md b/documentation/releaseNotes/releaseNotes.v7.0.0-rc.1.22479.8.md new file mode 100644 index 00000000000..abd087228f9 --- /dev/null +++ b/documentation/releaseNotes/releaseNotes.v7.0.0-rc.1.22479.8.md @@ -0,0 +1,10 @@ +Today we are releasing the official 7.0 Release Candidate of the `dotnet monitor` tool. This release includes: + +- With this release, support for experimental features is added. Users can now enable an experimental feature through configuration flags when launching `dotnet monitor`. Experimental features are still under development and not yet fully production-ready. (#2506) +- 🔬 Add collect stacks api and action (#2512, #2525) +- Avoid Kestrel address override warning (#2535) +- Create default diagnostic port under default shared path when running in `listen` mode (#2471, #2523) +- Fixed an issue (#2526) where the `config` command could throw an exception (#2530) +- Fixed an issue where running `config show --show-sources` could incorrectly report `ChainedConfigurationProvider` as the source (#2550) + +\*🔬 **_indicates an experimental feature_** \ No newline at end of file diff --git a/documentation/releases.md b/documentation/releases.md index 8e592eaae03..65ddfd355bb 100644 --- a/documentation/releases.md +++ b/documentation/releases.md @@ -1,14 +1,24 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Freleases) + # Releases -## Supported Versions +## Supported versions + +| Version | Original Release Date | Latest Patch Version | Patch Release Date | End of Support | Runtime Frameworks | +|---|---|---|---|---|---| +| 6.3 | October 11, 2022 | [6.3.0](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v6.3.0.md) | October 11, 2022 | | .NET Core 3.1 (with major roll forward)
.NET 6 | +| 6.2 | June 14, 2022 | [6.2.2](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v6.2.2.md) | August 9, 2022 | January 11, 2023 | .NET Core 3.1 (with major roll forward)
.NET 6 | + +## Out of support versions | Version | Original Release Date | Latest Patch Version | Patch Release Date | End of Support | Runtime Frameworks | |---|---|---|---|---|---| -| 6.1 | February 17, 2022 | [6.1.1](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v6.1.1.md) | April 12, 2022 | | .NET Core 3.1 (with major roll forward)
.NET 6 | +| 6.1 | February 17, 2022 | [6.1.4](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v6.1.4.md) | August 9, 2022 | September 14, 2022 | .NET Core 3.1 (with major roll forward)
.NET 6 | | 6.0 | November 8, 2021 | [6.0.2](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v6.0.2.md) | February 8, 2022 | May 17, 2022 | .NET Core 3.1 (with major roll forward)
.NET 6 | -## Preview Versions +## Preview releases | Version | Latest Version | Release Date | Runtime Frameworks | |---|---|---|---| -| 7.0 | [7.0.0 Preview 4](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v7.0.0-preview.4.22227.4.md) | May 10, 2022 | .NET 6 (with major roll forward)
.NET 7 | +| 7.0 | [7.0.0 RC 1](https://github.com/dotnet/dotnet-monitor/blob/main/documentation/releaseNotes/releaseNotes.v7.0.0-rc.1.22479.8.md) | October 11, 2022 | .NET 6
.NET 7 | diff --git a/documentation/schema.json b/documentation/schema.json index 252561e608e..4520f815e5a 100644 --- a/documentation/schema.json +++ b/documentation/schema.json @@ -48,6 +48,18 @@ } ] }, + "InProcessFeatures": { + "description": "[Experimental]", + "default": {}, + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/InProcessFeaturesOptions" + } + ] + }, "CorsConfiguration": { "default": {}, "oneOf": [ @@ -724,6 +736,16 @@ } } }, + { + "properties": { + "Type": { + "const": "CollectLiveMetrics" + }, + "Settings": { + "$ref": "#/definitions/CollectLiveMetricsOptions" + } + } + }, { "properties": { "Type": { @@ -734,6 +756,16 @@ } } }, + { + "properties": { + "Type": { + "const": "CollectStacks" + }, + "Settings": { + "$ref": "#/definitions/CollectStacksOptions" + } + } + }, { "properties": { "Type": { @@ -819,7 +851,7 @@ "string" ], "description": "The sliding window of time to consider whether the action list should be throttled based on the number of times the action list was executed. Executions that fall outside the window will not count toward the limit specified in the ActionCount setting.", - "format": "time-span" + "format": "duration" }, "RuleDuration": { "type": [ @@ -827,7 +859,7 @@ "string" ], "description": "The amount of time before the rule will stop monitoring a process after it has been applied to a process. If not specified, the rule will monitor the process with the trigger indefinitely.", - "format": "time-span" + "format": "duration" } } }, @@ -848,6 +880,20 @@ } } }, + "InProcessFeaturesOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "Enabled": { + "type": [ + "boolean", + "null" + ], + "description": "[Experimental] Allows features that require diagnostic components to be loaded into target processes to be enabled. These features may have minimal performance impact on target processes.", + "default": false + } + } + }, "CorsConfigurationOptions": { "type": "object", "additionalProperties": false, @@ -892,6 +938,14 @@ ], "description": "In 'Listen' mode, the maximum amount of connections to accept.", "format": "int32" + }, + "DeleteEndpointOnStartup": { + "type": [ + "boolean", + "null" + ], + "description": "In 'Listen' connection mode, deletes the domain socket file used for diagnostic port communication.", + "default": false } } }, @@ -979,7 +1033,7 @@ "null", "string" ], - "description": "The shared access signature (SAS) used to access the azure blob storage account." + "description": "The shared access signature (SAS) used to access the Azure blob and optionally queue storage accounts." }, "SharedAccessSignatureName": { "type": [ @@ -1027,8 +1081,32 @@ "null", "string" ], - "description": "The URI of the Azure queue account.", + "description": "The URI of the Azure queue storage account.", "format": "uri" + }, + "QueueSharedAccessSignature": { + "type": [ + "null", + "string" + ], + "description": "The shared access signature (SAS) used to access the Azure queue storage account." + }, + "QueueSharedAccessSignatureName": { + "type": [ + "null", + "string" + ], + "description": "The name of the queue shared access signature (SAS) used to look up the value from the Egress options Properties map." + }, + "Metadata": { + "type": [ + "null", + "object" + ], + "description": "A mapping of metadata keys to environment variable names. The values of the environment variables will be added as metadata for egressed artifacts.", + "additionalProperties": { + "type": "string" + } } } }, @@ -1141,12 +1219,26 @@ "type": "object", "additionalProperties": false, "properties": { + "DefaultSharedPath": { + "type": [ + "null", + "string" + ], + "description": "The default path where assets will be shared between dotnet-monitor and target processes. Dumps are temporarily stored under this path or in a sub folder unless DumpTempFolder is specified. Shared libraries are stored under this path or in a sub folder unless SharedLibraryPath is specified. On non-Windows platforms, a server diagnostic port is created with the name of 'dotnet-monitor.sock' immediately under this path if running in listen mode unless the diagnostic port is specified on the command line or the DiagnosticPort options are specified." + }, "DumpTempFolder": { "type": [ "null", "string" ], "description": "The location for temporary dump files. Defaults to the temp folder." + }, + "SharedLibraryPath": { + "type": [ + "null", + "string" + ], + "description": "[Experimental] The location to which libraries shared with target processes will be copied at startup." } } }, @@ -1235,7 +1327,7 @@ "string" ], "description": "The default sliding window duration.", - "format": "time-span" + "format": "duration" } } }, @@ -1272,7 +1364,7 @@ "string" ], "description": "The default sliding window of time to consider whether the action list should be throttled based on the number of times the action list was executed.", - "format": "time-span" + "format": "duration" }, "RuleDuration": { "type": [ @@ -1280,7 +1372,7 @@ "string" ], "description": "The default amount of time before the rule will stop monitoring a process after it has been applied to a process.", - "format": "time-span" + "format": "duration" } } }, @@ -1335,7 +1427,9 @@ "enum": [ "CollectDump", "CollectGCDump", + "CollectLiveMetrics", "CollectLogs", + "CollectStacks", "CollectTrace", "Execute", "LoadProfiler", @@ -1393,6 +1487,66 @@ } } }, + "CollectLiveMetricsOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "IncludeDefaultProviders": { + "type": [ + "boolean", + "null" + ], + "description": "Determines if the default counter providers should be used.", + "default": true + }, + "Providers": { + "type": [ + "array", + "null" + ], + "description": "The array of providers for metrics to collect.", + "items": { + "$ref": "#/definitions/EventMetricsProvider" + } + }, + "Duration": { + "type": [ + "null", + "string" + ], + "description": "The duration of time in which live metrics are collected.", + "format": "duration", + "default": "00:00:30" + }, + "Egress": { + "type": "string", + "description": "The name of the egress provider to which the live metrics are egressed.", + "minLength": 1 + } + } + }, + "EventMetricsProvider": { + "type": "object", + "additionalProperties": false, + "required": [ + "ProviderName" + ], + "properties": { + "ProviderName": { + "type": "string", + "minLength": 1 + }, + "CounterNames": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, "CollectLogsOptions": { "type": "object", "additionalProperties": false, @@ -1440,7 +1594,7 @@ "string" ], "description": "The duration of time in which logs are collected.", - "format": "time-span", + "format": "duration", "default": "00:00:30" }, "Egress": { @@ -1476,6 +1630,41 @@ "JsonSequence" ] }, + "CollectStacksOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "Egress": { + "type": "string", + "description": "[Experimental] The name of the egress provider to which the call stacks are egressed.", + "minLength": 1 + }, + "Format": { + "description": "[Experimental] The format used to display the call stacks.", + "default": "Json", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/CallStackFormat" + } + ] + } + } + }, + "CallStackFormat": { + "type": "string", + "description": "", + "x-enumNames": [ + "Json", + "PlainText" + ], + "enum": [ + "Json", + "PlainText" + ] + }, "CollectTraceOptions": { "type": "object", "additionalProperties": false, @@ -1526,13 +1715,24 @@ "string" ], "description": "The duration of time in which trace events are collected.", - "format": "time-span", + "format": "duration", "default": "00:00:30" }, "Egress": { "type": "string", "description": "The name of the egress provider to which the trace is egressed.", "minLength": 1 + }, + "StoppingEvent": { + "description": "The event to watch for while collecting the trace, and once observed the trace will be stopped.", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/TraceEventFilter" + } + ] } } }, @@ -1604,6 +1804,36 @@ "Verbose" ] }, + "TraceEventFilter": { + "type": "object", + "additionalProperties": false, + "required": [ + "ProviderName", + "EventName" + ], + "properties": { + "ProviderName": { + "type": "string", + "description": "The event provider that will produce the specified event.", + "minLength": 1 + }, + "EventName": { + "type": "string", + "description": "The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name.", + "minLength": 1 + }, + "PayloadFilter": { + "type": [ + "null", + "object" + ], + "description": "A mapping of event payload field names to their expected value. A subset of the payload fields may be specified.", + "additionalProperties": { + "type": "string" + } + } + } + }, "ExecuteOptions": { "type": "object", "additionalProperties": false, @@ -1718,7 +1948,7 @@ "string" ], "description": "The sliding time window in which the number of requests are counted.", - "format": "time-span" + "format": "duration" }, "IncludePaths": { "type": [ @@ -1759,7 +1989,7 @@ "string" ], "description": "The threshold of the amount of time in which a request is considered to be slow.", - "format": "time-span", + "format": "duration", "default": "00:00:05" }, "SlidingWindowDuration": { @@ -1768,7 +1998,7 @@ "string" ], "description": "The sliding time window in which the the number of slow requests are counted.", - "format": "time-span" + "format": "duration" }, "IncludePaths": { "type": [ @@ -1821,7 +2051,7 @@ "string" ], "description": "The sliding time window in which the number of responses with matching status codes must occur.", - "format": "time-span" + "format": "duration" }, "IncludePaths": { "type": [ @@ -1885,7 +2115,7 @@ "string" ], "description": "The sliding time window in which the counter must maintain its value as specified by the threshold levels in GreaterThan and LessThan.", - "format": "time-span" + "format": "duration" } } }, @@ -1920,7 +2150,7 @@ "string" ], "description": "The sliding time window in which the counter must maintain its value as specified by the threshold levels in GreaterThan and LessThan.", - "format": "time-span", + "format": "duration", "default": "00:01:00" } } @@ -1954,7 +2184,7 @@ "string" ], "description": "The sliding time window in which the counter must maintain its value as specified by the threshold levels in GreaterThan and LessThan.", - "format": "time-span", + "format": "duration", "default": "00:01:00" } } @@ -1988,7 +2218,7 @@ "string" ], "description": "The sliding time window in which the counter must maintain its value as specified by the threshold levels in GreaterThan and LessThan.", - "format": "time-span", + "format": "duration", "default": "00:01:00" } } diff --git a/documentation/setup.md b/documentation/setup.md index 1ee016fd5c5..a9c88e10416 100644 --- a/documentation/setup.md +++ b/documentation/setup.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fsetup) + # Setup `dotnet monitor` is available via two different distribution mechanisms: diff --git a/documentation/troubleshooting.md b/documentation/troubleshooting.md index f1f68cf6fb2..b4804761288 100644 --- a/documentation/troubleshooting.md +++ b/documentation/troubleshooting.md @@ -1,3 +1,6 @@ + +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Ftroubleshooting) + # Troubleshooting Guide Here is our guide for diagnosing specific issues with `dotnet monitor`. This is an ongoing effort to add issues and solutions to this guide; you can help us by letting us know in the [Issues](https://github.com/dotnet/dotnet-monitor/issues) section if we should add something here. diff --git a/documentation/videos-and-tutorials.md b/documentation/videos-and-tutorials.md index 9b7db9b7ee9..cdec6f451c4 100644 --- a/documentation/videos-and-tutorials.md +++ b/documentation/videos-and-tutorials.md @@ -1,6 +1,12 @@ +### Was this documentation helpful? [Share feedback](https://www.research.net/r/DGDQWXH?src=documentation%2Fvideos-and-tutorials) + + ### Dotnet Monitor Fundamentals [![Video Tutorial For Getting Started Locally With Dotnet Monitor](https://i.ytimg.com/vi/pG0t19bEYJw/hq720.jpg)](https://www.youtube.com/watch?v=pG0t19bEYJw) +### Dotnet Monitor AKS Tutorial +[![Video Tutorial For Using Dotnet Monitor As A Sidecar In AKS](https://i.ytimg.com/vi/3nzZO34nUFQ/hq720.jpg)](https://www.youtube.com/watch?v=3nzZO34nUFQ) + ### Inspecting Application Metrics with Dotnet Monitor [![Video Tutorial For Inspecting Application Metrics with Dotnet Monitor](https://i.ytimg.com/vi/hbgPvjTJSLY/hq720.jpg)](https://www.youtube.com/watch?v=hbgPvjTJSLY) diff --git a/dotnet-monitor-codeql.yml b/dotnet-monitor-codeql.yml index 67169b61403..1014223660c 100644 --- a/dotnet-monitor-codeql.yml +++ b/dotnet-monitor-codeql.yml @@ -25,7 +25,7 @@ stages: timeoutInMinutes: 90 pool: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 steps: - checkout: self @@ -57,7 +57,7 @@ stages: timeoutInMinutes: 90 pool: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 steps: - checkout: self diff --git a/dotnet-monitor.sln b/dotnet-monitor.sln index 11050bb0284..341102ce1f4 100644 --- a/dotnet-monitor.sln +++ b/dotnet-monitor.sln @@ -11,11 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-monitor", "src\Tools EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.WebApi", "src\Microsoft.Diagnostics.Monitoring.WebApi\Microsoft.Diagnostics.Monitoring.WebApi.csproj", "{B54DE8DD-6591-45C2-B9F7-22C4A23A384C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{B24CD8F2-D809-4DB8-89A1-D45FA9218020}" - ProjectSection(SolutionItems) = preProject - src\Tools\Common\CommandExtensions.cs = src\Tools\Common\CommandExtensions.cs - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C7568468-1C79-4944-8136-18812A7F9EA7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.TestCommon", "src\Tests\Microsoft.Diagnostics.Monitoring.TestCommon\Microsoft.Diagnostics.Monitoring.TestCommon.csproj", "{863AA12D-0918-49CE-9E76-45A22680268B}" @@ -41,6 +36,18 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.ExecuteActionApp", "src\Tests\Microsoft.Diagnostics.Monitoring.ExecuteActionApp\Microsoft.Diagnostics.Monitoring.ExecuteActionApp.csproj", "{A5A0CAAB-C200-44D2-BC93-8445C6E748AD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureBlobStorage", "src\Extensions\AzureBlobStorage\AzureBlobStorage.csproj", "{5ED61A7B-F0AA-45F2-9E9A-8972FF7F7278}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.Profiler.UnitTests", "src\Tests\Microsoft.Diagnostics.Monitoring.Profiler.UnitTests\Microsoft.Diagnostics.Monitoring.Profiler.UnitTests.csproj", "{A25AC517-F7C6-43C6-B892-4A447914C42C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp", "src\Tests\Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp\Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.csproj", "{1CA2284B-A3A0-476A-9A93-A95E665E78BE}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D8427A5A-810A-4664-9B68-FB64B5E18178}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup", "src\Tests\Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup\Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup.csproj", "{F324BAD6-C9C0-42DE-9150-D8307D972E9A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook", "src\Tests\Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook\Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook.csproj", "{9BEB8058-7EE4-4EE2-B83D-592E85236E78}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -104,6 +111,22 @@ Global {5ED61A7B-F0AA-45F2-9E9A-8972FF7F7278}.Debug|Any CPU.Build.0 = Debug|Any CPU {5ED61A7B-F0AA-45F2-9E9A-8972FF7F7278}.Release|Any CPU.ActiveCfg = Release|Any CPU {5ED61A7B-F0AA-45F2-9E9A-8972FF7F7278}.Release|Any CPU.Build.0 = Release|Any CPU + {A25AC517-F7C6-43C6-B892-4A447914C42C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A25AC517-F7C6-43C6-B892-4A447914C42C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A25AC517-F7C6-43C6-B892-4A447914C42C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A25AC517-F7C6-43C6-B892-4A447914C42C}.Release|Any CPU.Build.0 = Release|Any CPU + {1CA2284B-A3A0-476A-9A93-A95E665E78BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CA2284B-A3A0-476A-9A93-A95E665E78BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CA2284B-A3A0-476A-9A93-A95E665E78BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CA2284B-A3A0-476A-9A93-A95E665E78BE}.Release|Any CPU.Build.0 = Release|Any CPU + {F324BAD6-C9C0-42DE-9150-D8307D972E9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F324BAD6-C9C0-42DE-9150-D8307D972E9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F324BAD6-C9C0-42DE-9150-D8307D972E9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F324BAD6-C9C0-42DE-9150-D8307D972E9A}.Release|Any CPU.Build.0 = Release|Any CPU + {9BEB8058-7EE4-4EE2-B83D-592E85236E78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BEB8058-7EE4-4EE2-B83D-592E85236E78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BEB8058-7EE4-4EE2-B83D-592E85236E78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BEB8058-7EE4-4EE2-B83D-592E85236E78}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -112,7 +135,6 @@ Global {B62728C8-1267-4043-B46F-5537BBAEC692} = {19FAB78C-3351-4911-8F0C-8C6056401740} {C57F7656-6663-4A3C-BE38-B75C6C57E77D} = {B62728C8-1267-4043-B46F-5537BBAEC692} {B54DE8DD-6591-45C2-B9F7-22C4A23A384C} = {19FAB78C-3351-4911-8F0C-8C6056401740} - {B24CD8F2-D809-4DB8-89A1-D45FA9218020} = {19FAB78C-3351-4911-8F0C-8C6056401740} {C7568468-1C79-4944-8136-18812A7F9EA7} = {19FAB78C-3351-4911-8F0C-8C6056401740} {863AA12D-0918-49CE-9E76-45A22680268B} = {C7568468-1C79-4944-8136-18812A7F9EA7} {5AB5A4B4-3A13-4D42-B42C-B9193E60F426} = {C7568468-1C79-4944-8136-18812A7F9EA7} @@ -126,6 +148,10 @@ Global {0DBE362D-82F1-4740-AE6A-40C1A82EDCDB} = {C7568468-1C79-4944-8136-18812A7F9EA7} {A5A0CAAB-C200-44D2-BC93-8445C6E748AD} = {C7568468-1C79-4944-8136-18812A7F9EA7} {5ED61A7B-F0AA-45F2-9E9A-8972FF7F7278} = {B62728C8-1267-4043-B46F-5537BBAEC692} + {A25AC517-F7C6-43C6-B892-4A447914C42C} = {C7568468-1C79-4944-8136-18812A7F9EA7} + {1CA2284B-A3A0-476A-9A93-A95E665E78BE} = {C7568468-1C79-4944-8136-18812A7F9EA7} + {F324BAD6-C9C0-42DE-9150-D8307D972E9A} = {C7568468-1C79-4944-8136-18812A7F9EA7} + {9BEB8058-7EE4-4EE2-B83D-592E85236E78} = {C7568468-1C79-4944-8136-18812A7F9EA7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/dotnet-monitor.yml b/dotnet-monitor.yml index 3627507b187..737a7b81678 100644 --- a/dotnet-monitor.yml +++ b/dotnet-monitor.yml @@ -8,6 +8,14 @@ pr: - release/* - internal/release/* - feature/* + paths: + exclude: + - .devcontainer + - .github + - .vscode + - .gitignore + - eng/actions + - '**.md' schedules: - cron: 15 11 * * 6 @@ -181,7 +189,7 @@ stages: - MacOS_arm64_Release pool: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 enablePublishUsingPipelines: true enableMicrobuild: true artifacts: @@ -213,7 +221,7 @@ stages: publishUsingPipelines: true pool: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 # These are the stages that perform validation of several SDL requirements and publish the bits required to the designated feed. - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}: - template: /eng/common/templates/post-build/post-build.yml diff --git a/dotnet.sh b/dotnet.sh old mode 100644 new mode 100755 diff --git a/eng/PrepareRelease.yml b/eng/PrepareRelease.yml index cad0f3bbc85..8fe308efeed 100644 --- a/eng/PrepareRelease.yml +++ b/eng/PrepareRelease.yml @@ -9,11 +9,11 @@ stages: displayName: Prepare release with Darc pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: - name: NetCore1ESPool-Public - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019.Open + name: NetCore-Public + demands: ImageOverride -equals 1es-windows-2019-open ${{ if ne(variables['System.TeamProject'], 'public') }}: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 variables: - ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest'), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/internal/release/'), startsWith(variables['Build.SourceBranch'], 'refs/heads/test/release/'))) }}: - group: DotNet-Diagnostics-Storage diff --git a/eng/Publishing.props b/eng/Publishing.props index d688bf150e8..a423aef2cff 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -97,24 +97,34 @@ Inputs="@(PackageToPublish)" Outputs="%(PackageToPublish.Identity).notexist"> - - - <_ChecksumFilePath>%(PackageToPublish.FullPath).sha512 - <_BuildVersionFilePath>%(PackageToPublish.FullPath).buildversion - <_PackageVersionFilePath>%(PackageToPublish.FullPath).version - - + + + + + + + + + <_PackageWithBuildVersionFileName>@(_PackageWithBuildVersionFileName) + <_PackageWithBuildVersionFilePath>%(PackageToPublish.RootDir)%(PackageToPublish.Directory)$(_PackageWithBuildVersionFileName) + <_ChecksumFilePath>%(PackageToPublish.FullPath).sha512 + <_BuildVersionFilePath>$(_PackageWithBuildVersionFilePath).buildversion + <_PackageVersionFilePath>$(_PackageWithBuildVersionFilePath).version + + + https://github.com/dotnet/dotnet-monitor - 7.0.0 - preview - 5 + 8.0.0 + alpha + 1 true + false + net6.0 - - $(ToolTargetFrameworks);net7.0 + $(ToolTargetFrameworks);net7.0 - netcoreapp3.1;net5.0;net6.0 - - $(TestTargetFrameworks);net7.0 + netcoreapp3.1;net6.0 + $(TestTargetFrameworks);net7.0 net6.0 - $(DefineConstants);INCLUDE_NEXT_DOTNET + $(DefineConstants);INCLUDE_NEXT_DOTNET false @@ -43,50 +46,36 @@ --> - 7.0.0-beta.22276.1 + 8.0.0-beta.22511.1 - 7.0.0-preview.6.22276.1 - 7.0.0-preview.6.22276.1 + 7.0.0-rtm.22511.13 + 7.0.0-rtm.22511.13 + + 2.0.0-beta4.22504.1 - 5.0.0-preview.22276.1 - 5.0.0-preview.22276.1 + 6.0.0-preview.22511.1 + 6.0.0-preview.22511.1 - 7.0.0-preview.5.22274.8 - 7.0.0-preview.5.22274.8 + 7.0.0-rtm.22507.1 + 7.0.0-rtm.22507.1 - 1.0.327301 + 1.0.345501 - 3.1.25 + 3.1.30 $(MicrosoftNETCoreApp31Version) 5.0.17 $(MicrosoftNETCoreApp50Version) - 6.0.5 - 6.0.5 + 6.0.10 + $(MicrosoftNETCoreApp60Version) $(MicrosoftNETCoreAppRuntimewinx64Version) $(MicrosoftAspNetCoreAppRuntimewinx64Version) - 1.6.0 - 12.12.0 - 12.10.0 - 6.0.5 - 6.0.5 - 6.0.0 - 6.0.5 - 6.0.1 - 6.0.0 - 6.0.0 - 6.17.0 - 1.2.3 - 2.0.0-beta3.22173.1 - 6.17.0 - 4.3.2 - 5.0.0 - - 10.5.2 - 5.6.3 - 2.4.1 + 6.0.10 + 7.0.0-rc.2.22476.2 + 6.0.10 + 7.0.0-rc.2.22476.2 + + diff --git a/eng/dependabot/NuGet.config b/eng/dependabot/NuGet.config new file mode 120000 index 00000000000..f0aeba16780 --- /dev/null +++ b/eng/dependabot/NuGet.config @@ -0,0 +1 @@ +../../NuGet.config \ No newline at end of file diff --git a/eng/dependabot/Packages.props b/eng/dependabot/Packages.props new file mode 100644 index 00000000000..16828e14461 --- /dev/null +++ b/eng/dependabot/Packages.props @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eng/dependabot/Versions.props b/eng/dependabot/Versions.props new file mode 100644 index 00000000000..4abd4be259f --- /dev/null +++ b/eng/dependabot/Versions.props @@ -0,0 +1,24 @@ + + + + + 1.7.0 + 12.13.1 + 12.11.1 + 6.0.0 + 6.0.5 + 6.0.2 + 6.0.0 + 6.0.0 + 6.23.1 + 1.4.1 + 6.23.1 + 4.3.2 + 5.0.0 + + + 13.0.1 + 10.8.0 + 6.4.0 + + diff --git a/eng/dependabot/dependabot.csproj b/eng/dependabot/dependabot.csproj new file mode 100644 index 00000000000..d6029d18934 --- /dev/null +++ b/eng/dependabot/dependabot.csproj @@ -0,0 +1,2 @@ + + diff --git a/eng/release.yml b/eng/release.yml index dbacd05e156..7cf7dabe080 100644 --- a/eng/release.yml +++ b/eng/release.yml @@ -31,7 +31,7 @@ stages: pool: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 jobs: - job: Validate @@ -95,7 +95,7 @@ stages: pool: name: NetCore1ESPool-Internal - demands: ImageOverride -equals Build.Windows.10.Amd64.VS2019 + demands: ImageOverride -equals 1es-windows-2019 jobs: - deployment: PublishToStorageAccounts diff --git a/eng/release/DiagnosticsReleaseTool/DiagnosticsReleaseTool.csproj b/eng/release/DiagnosticsReleaseTool/DiagnosticsReleaseTool.csproj index 08643e03466..46ace05f444 100644 --- a/eng/release/DiagnosticsReleaseTool/DiagnosticsReleaseTool.csproj +++ b/eng/release/DiagnosticsReleaseTool/DiagnosticsReleaseTool.csproj @@ -19,7 +19,7 @@ - + diff --git a/generate-dev-sln.ps1 b/generate-dev-sln.ps1 index e74c4155008..fec6c0823db 100644 --- a/generate-dev-sln.ps1 +++ b/generate-dev-sln.ps1 @@ -37,5 +37,4 @@ $slnFile = Get-Content $devSln #dotnet sln uses an older ProjectType Guid $slnFile -replace 'FAE04EC0-301F-11D3-BF4B-00C04F79EFBC', '9A19103F-16F7-4668-BE54-9A1E7A4F7556' | Out-File $devSln -$devenvPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -property productPath -& $devenvPath $PSScriptRoot\dotnet-monitor.dev.sln \ No newline at end of file +& "$PSScriptRoot\startvs.cmd" $PSScriptRoot\dotnet-monitor.dev.sln \ No newline at end of file diff --git a/global.json b/global.json index dd92c194893..0dbd8fd40c3 100644 --- a/global.json +++ b/global.json @@ -1,16 +1,24 @@ { "tools": { - "dotnet": "7.0.100-preview.4.22252.9", + "dotnet": "7.0.100-rc.1.22431.12", "runtimes": { "aspnetcore": [ "$(MicrosoftAspNetCoreApp31Version)", - "$(MicrosoftAspNetCoreApp50Version)", + "$(MicrosoftAspNetCoreApp60Version)", + "$(VSRedistCommonAspNetCoreSharedFrameworkx6470Version)" + ], + "aspnetcore/x86": [ + "$(MicrosoftAspNetCoreApp31Version)", "$(MicrosoftAspNetCoreApp60Version)", "$(VSRedistCommonAspNetCoreSharedFrameworkx6470Version)" ], "dotnet": [ "$(MicrosoftNETCoreApp31Version)", - "$(MicrosoftNETCoreApp50Version)", + "$(MicrosoftNETCoreApp60Version)", + "$(VSRedistCommonNetCoreSharedFrameworkx6470Version)" + ], + "dotnet/x86": [ + "$(MicrosoftNETCoreApp31Version)", "$(MicrosoftNETCoreApp60Version)", "$(VSRedistCommonNetCoreSharedFrameworkx6470Version)" ] @@ -18,6 +26,6 @@ }, "msbuild-sdks": { "Microsoft.Build.NoTargets": "2.0.1", - "Microsoft.DotNet.Arcade.Sdk": "7.0.0-beta.22276.1" + "Microsoft.DotNet.Arcade.Sdk": "8.0.0-beta.22511.1" } } diff --git a/samples/AKS_Tutorial/README.md b/samples/AKS_Tutorial/README.md new file mode 100644 index 00000000000..e0456cfb301 --- /dev/null +++ b/samples/AKS_Tutorial/README.md @@ -0,0 +1,27 @@ +### Dotnet Monitor AKS Tutorial +[![Video Tutorial For Using Dotnet Monitor As A Sidecar In AKS](https://i.ytimg.com/vi/3nzZO34nUFQ/hq720.jpg)](https://www.youtube.com/watch?v=3nzZO34nUFQ) + +#### Commands Used In The Video + +* 2:06 - `az acr login --name ` +* 2:09 - `az aks create --resource-group --name --node-count --generate-ssh-keys --attach-acr ` +* 2:14 - `az aks get-credentials --resource-group --name ` +* 2:17 - `kubectl config get-contexts` +* 2:21 - `kubectl get nodes` +* 5:38 - `kubectl apply -f egressmap.yaml` +* 5:48 - `kubectl create configmap dotnet-monitor-triggers --from-file=settings.json` +* 6:08 - `dotnet monitor generatekey` +* 6:18 - `kubectl create secret generic apikey --from-literal=Authentication__MonitorApiKey__Subject='...' --from-literal=Authentication__MonitorApiKey__PublicKey='...'` +* 6:27 - `kubectl get configmaps` +* 6:27 - `kubectl get secrets` +* 6:32 - `kubectl apply -f deploy.yaml` +* 6:41 - `kubectl get pods` +* 6:46 - `kubectl logs monitor` +* 7:14 - `kubectl get pods` +* 7:20 - `kubectl port-forward 80` +* 7:20 - `kubectl port-forward 52323` +* 7:42 - `curl -v -H "Authorization: Bearer " http://localhost:52323/processes` +* 8:02 - `curl -v -H "Authorization: Bearer " http://localhost:52323/gcdump?egressProvider=monitorBlob` +* 8:18 - `curl -v -H "Authorization: Bearer " http://localhost:52323/gcdump?egressProvider=monitorFile` +* 8:24 - `kubectl exec -it -c monitor --/bin/sh` +* 8:58 - `curl -v http://localhost:80/Invalid` diff --git a/samples/AKS_Tutorial/deploy.yaml b/samples/AKS_Tutorial/deploy.yaml new file mode 100644 index 00000000000..e94b31863db --- /dev/null +++ b/samples/AKS_Tutorial/deploy.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: akstest +spec: + replicas: 1 + selector: + matchLabels: + app: akstest + template: + metadata: + labels: + app: akstest + spec: + restartPolicy: Always + containers: + - name: publishedapp + image: mcr.microsoft.com/dotnet/samples:aspnetapp + imagePullPolicy: Always + env: + - name: ASPNETCORE_URLS + value: http://+:80 + - name: DOTNET_DiagnosticPorts + value: /diag/port.sock + volumeMounts: + - mountPath: /diag + name: diagvol + resources: + limits: + cpu: 250m + memory: 512Mi + - name: monitor + image: mcr.microsoft.com/dotnet/monitor:6 + imagePullPolicy: Always + env: + - name: DotnetMonitor_DiagnosticPort__ConnectionMode + value: 'Listen' + - name: DotnetMonitor_DiagnosticPort__EndpointName + value: /diag/port.sock + # ALWAYS use the HTTPS form of the URL for deployments in production; the removal of HTTPS is done for + # demonstration purposes only in this example. Please continue reading after this example for further details. + - name: DotnetMonitor_Urls + value: 'http://localhost:52323' + - name: DotnetMonitor_Storage__DumpTempFolder + value: /diag/dumps + - name: DotnetMonitor_Logging__Console__FormatterName + value: simple + volumeMounts: + - mountPath: /diag + name: diagvol + - mountPath: /etc/dotnet-monitor + name: dotnet-monitor-config + resources: + requests: + cpu: 50m + memory: 32Mi + limits: + cpu: 250m + memory: 256Mi + volumes: + - name: diagvol + emptyDir: {} + - name: dotnet-monitor-config + projected: + defaultMode: 400 + sources: + - configMap: + name: dotnet-monitor-egress + optional: false + - configMap: + name: dotnet-monitor-triggers + optional: false + - secret: + name: apikey diff --git a/samples/AKS_Tutorial/egressmap.yaml b/samples/AKS_Tutorial/egressmap.yaml new file mode 100644 index 00000000000..197af299b8c --- /dev/null +++ b/samples/AKS_Tutorial/egressmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: dotnet-monitor-egress +data: + Egress__FileSystem__monitorFile__directoryPath: /diag + Egress__AzureBlobStorage__monitorBlob__accountUri: "https://exampleaccount.blob.core.windows.net" + Egress__AzureBlobStorage__monitorBlob__containerName: "dotnet-monitor" + Egress__AzureBlobStorage__monitorBlob__blobPrefix: "artifacts" + Egress__AzureBlobStorage__monitorBlob__managedIdentityClientId: "ffffffff-ffff-ffff-ffff-ffffffffffff" diff --git a/samples/AKS_Tutorial/settings.json b/samples/AKS_Tutorial/settings.json new file mode 100644 index 00000000000..b7ec3bb2a0b --- /dev/null +++ b/samples/AKS_Tutorial/settings.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/dotnet-monitor/main/documentation/schema.json", + "CollectionRules":{ + "BadResponseStatus": { + "Trigger": { + "Type": "AspNetResponseStatus", + "Settings": { + "ResponseCount": 1, + "StatusCodes": [ + "400-499" + ] + } + }, + "Actions": [ + { + "Type": "CollectDump", + "Settings": { + "Egress": "monitorBlob", + "Type": "Mini" + } + } + ], + "Limits": { + "ActionCount": 1 + } + } + } +} diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 6f52c5bb80d..708482418ca 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -23,4 +23,95 @@ + + + + + + + + + + + $(AssemblyName).runtimeconfig.test.json + $(TargetDir)$(ProjectRuntimeConfigTestFileName) + + + + + + + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.NETCore.App' "> + $(MicrosoftNETCoreApp31Version) + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.AspNetCore.App' "> + $(MicrosoftAspNetCoreApp31Version) + + + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.NETCore.App' "> + $(MicrosoftNETCoreApp50Version) + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.AspNetCore.App' "> + $(MicrosoftAspNetCoreApp50Version) + + + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.NETCore.App' "> + $(MicrosoftNETCoreApp60Version) + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.AspNetCore.App' "> + $(MicrosoftAspNetCoreApp60Version) + + + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.NETCore.App' "> + $(MicrosoftNETCoreApp70Version) + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' == 'Microsoft.AspNetCore.App' "> + $(MicrosoftAspNetCoreApp70Version) + + + + <_TestRuntimeFramework Include="@(RuntimeFramework)" Condition=" '%(Identity)' != 'Microsoft.NETCore.App' and '%(Identity)' != 'Microsoft.AspNetCore.App' " /> + + + + diff --git a/src/Extensions/AzureBlobStorage/LoggingExtensions.cs b/src/Extensions/AzureBlobStorage/LoggingExtensions.cs index 773bb09fc1d..ab250898738 100644 --- a/src/Extensions/AzureBlobStorage/LoggingExtensions.cs +++ b/src/Extensions/AzureBlobStorage/LoggingExtensions.cs @@ -44,6 +44,30 @@ public static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_QueueOptionsPartiallySet); + private static readonly Action _invalidMetadata = + LoggerMessage.Define( + eventId: LoggingEventIds.InvalidMetadata.EventId(), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_InvalidMetadata); + + private static readonly Action _duplicateKeyInMetadata = + LoggerMessage.Define( + eventId: LoggingEventIds.DuplicateKeyInMetadata.EventId(), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_DuplicateKeyInMetadata); + + private static readonly Action _environmentVariableNotFound = + LoggerMessage.Define( + eventId: LoggingEventIds.EnvironmentVariableNotFound.EventId(), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_EnvironmentVariableNotFound); + + private static readonly Action _environmentBlockNotSupported = + LoggerMessage.Define( + eventId: LoggingEventIds.EnvironmentBlockNotSupported.EventId(), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_EnvironmentBlockNotSupported); + public static void EgressCopyActionStreamToEgressStream(this ILogger logger, int bufferSize) { _egressCopyActionStreamToEgressStream(logger, bufferSize, null); @@ -73,5 +97,25 @@ public static void QueueOptionsPartiallySet(this ILogger logger) { _queueOptionsPartiallySet(logger, nameof(AzureBlobEgressProviderOptions.QueueName), nameof(AzureBlobEgressProviderOptions.QueueAccountUri), null); } + + public static void InvalidMetadata(this ILogger logger, Exception ex) + { + _invalidMetadata(logger, ex); + } + + public static void DuplicateKeyInMetadata(this ILogger logger, string duplicateKey) + { + _duplicateKeyInMetadata(logger, duplicateKey, null); + } + + public static void EnvironmentVariableNotFound(this ILogger logger, string environmentVariable) + { + _environmentVariableNotFound(logger, environmentVariable, null); + } + + public static void EnvironmentBlockNotSupported(this ILogger logger) + { + _environmentBlockNotSupported(logger, null); + } } } diff --git a/src/Extensions/AzureBlobStorage/Program.cs b/src/Extensions/AzureBlobStorage/Program.cs index 5b7ece6a5e3..b721acde0a2 100644 --- a/src/Extensions/AzureBlobStorage/Program.cs +++ b/src/Extensions/AzureBlobStorage/Program.cs @@ -19,9 +19,15 @@ static async Task Main(string[] args) // Expected command line format is: AzureBlobStorage.exe Egress --Provider-Name MyProviderEndpointName RootCommand rootCommand = new RootCommand("Egresses an artifact to Azure Storage."); - Command egressCmd = new Command("Egress", "The class of extension being invoked; Egress is for egressing an artifact."); - egressCmd.Add(new Option("--Provider-Name", "The provider name given in the configuration to dotnet-monitor.")); - egressCmd.SetHandler(Program.Egress, egressCmd.Children.OfType().ToArray()); + var providerNameOption = new Option( + name: "--Provider-Name", + description: "The provider name given in the configuration to dotnet-monitor."); + + Command egressCmd = new Command("Egress", "The class of extension being invoked; Egress is for egressing an artifact.") + { providerNameOption }; + + egressCmd.SetHandler(Program.Egress, providerNameOption); + rootCommand.Add(egressCmd); return await rootCommand.InvokeAsync(args); @@ -123,4 +129,4 @@ internal class ExtensionEgressPayload public string ProfileName { get; set; } } -} \ No newline at end of file +} diff --git a/src/Extensions/AzureBlobStorage/Strings.Designer.cs b/src/Extensions/AzureBlobStorage/Strings.Designer.cs index 42dd4bcc1ca..1d583ef5bbb 100644 --- a/src/Extensions/AzureBlobStorage/Strings.Designer.cs +++ b/src/Extensions/AzureBlobStorage/Strings.Designer.cs @@ -87,6 +87,15 @@ internal static string ErrorMessage_EgressMissingCredentials { } } + /// + /// Looks up a localized string similar to Metadata cannot include duplicate keys; please change or remove the key '{key}'. + /// + internal static string LogFormatString_DuplicateKeyInMetadata { + get { + return ResourceManager.GetString("LogFormatString_DuplicateKeyInMetadata", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copying action stream to egress stream with buffer size {bufferSize}. /// @@ -114,6 +123,33 @@ internal static string LogFormatString_EgressProviderSavedStream { } } + /// + /// Looks up a localized string similar to Target framework does not support custom egress metadata.. + /// + internal static string LogFormatString_EnvironmentBlockNotSupported { + get { + return ResourceManager.GetString("LogFormatString_EnvironmentBlockNotSupported", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The environment variable '{name}' could not be found on the target process.. + /// + internal static string LogFormatString_EnvironmentVariableNotFound { + get { + return ResourceManager.GetString("LogFormatString_EnvironmentVariableNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid metadata; custom metadata keys must be valid C# identifiers.. + /// + internal static string LogFormatString_InvalidMetadata { + get { + return ResourceManager.GetString("LogFormatString_InvalidMetadata", resourceCulture); + } + } + /// /// Looks up a localized string similar to The queue {0} does not exist; ensure that the {queueName} and {queueAccountUri} fields are set correctly.. /// diff --git a/src/Extensions/AzureBlobStorage/Strings.resx b/src/Extensions/AzureBlobStorage/Strings.resx index fff3818d0a0..c9065e4cc80 100644 --- a/src/Extensions/AzureBlobStorage/Strings.resx +++ b/src/Extensions/AzureBlobStorage/Strings.resx @@ -131,6 +131,10 @@ SharedAccessSignature, AccountKey, or ManagedIdentityClientId must be specified. Gets a string similar to "SharedAccessSignature, AccountKey, or ManagedIdentityClientId must be specified.". + + Metadata cannot include duplicate keys; please change or remove the key '{key}' + Gets a string similar to "Metadata cannot include duplicate keys; please change or remove the key '{key}'". + Copying action stream to egress stream with buffer size {bufferSize} Gets the format string that is printed in the 5:EgressCopyActionStreamToEgressStream event. @@ -150,6 +154,17 @@ 1. providerType: Type of the provider 2. path: path where provider saved the stream + + Target framework does not support custom egress metadata. + + + The environment variable '{name}' could not be found on the target process. + Gets a string similar to "The environment variable '{name}' could not be found on the target process.". + + + Invalid metadata; custom metadata keys must be valid C# identifiers. + Gets a string similar to "Invalid metadata; custom metadata keys must be valid C# identifiers.". + The queue {0} does not exist; ensure that the {queueName} and {queueAccountUri} fields are set correctly. Gets the format string that is printed in the 57:QueueDoesNotExist event. diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/AzureBlobEgressProviderOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/AzureBlobEgressProviderOptions.cs index f428d440fcf..c5da3de38b2 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/AzureBlobEgressProviderOptions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/AzureBlobEgressProviderOptions.cs @@ -4,6 +4,7 @@ using Microsoft.Diagnostics.Monitoring.WebApi; using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Microsoft.Diagnostics.Tools.Monitor.Egress.AzureBlob @@ -70,5 +71,21 @@ internal sealed partial class AzureBlobEgressProviderOptions : ResourceType = typeof(OptionsDisplayStrings), Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueAccountUri))] public Uri QueueAccountUri { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueSharedAccessSignature))] + public string QueueSharedAccessSignature { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueSharedAccessSignatureName))] + public string QueueSharedAccessSignatureName { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_Metadata))] + public IDictionary Metadata { get; set; } + = new Dictionary(0); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/StorageOptionsDefaults.cs b/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleFinishedStates.cs similarity index 65% rename from src/Microsoft.Diagnostics.Monitoring.Options/StorageOptionsDefaults.cs rename to src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleFinishedStates.cs index d154cb8bafa..dc09f65c898 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/StorageOptionsDefaults.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleFinishedStates.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.IO; - namespace Microsoft.Diagnostics.Monitoring.WebApi { - internal sealed class StorageOptionsDefaults + public enum CollectionRuleFinishedStates { - public static readonly string DumpTempFolder = Path.GetTempPath(); + Startup, + RuleDurationReached, + ActionCountReached } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleMetadata.cs b/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleMetadata.cs new file mode 100644 index 00000000000..4a1aa0eed25 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleMetadata.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal class CollectionRuleMetadata + { + public string CollectionRuleName { get; set; } + + public int ActionListIndex { get; set; } + + public string ActionName { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleState.cs b/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleState.cs new file mode 100644 index 00000000000..486f232062c --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/CollectionRuleState.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + public enum CollectionRuleState + { + Running, + ActionExecuting, + Throttled, + Finished + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptions.cs index 757fd59129c..281d0a6e800 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptions.cs @@ -24,5 +24,11 @@ public class DiagnosticPortOptions ResourceType = typeof(OptionsDisplayStrings), Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_DiagnosticPortOptions_MaxConnections))] public int? MaxConnections { get; set; } + + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_DiagnosticPortOptions_DeleteEndpointOnStartup))] + [DefaultValue(DiagnosticPortOptionsDefaults.DeleteEndpointOnStartup)] + public bool? DeleteEndpointOnStartup { get; set; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsDefaults.cs b/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsDefaults.cs index 3ffc43e4c61..d4d42b10bb5 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsDefaults.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsDefaults.cs @@ -7,5 +7,6 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi internal class DiagnosticPortOptionsDefaults { public const DiagnosticPortConnectionMode ConnectionMode = DiagnosticPortConnectionMode.Connect; + public const bool DeleteEndpointOnStartup = false; } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsExtensions.cs index e281bb021f1..e1b2b184954 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/DiagnosticPortOptionsExtensions.cs @@ -6,9 +6,10 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class DiagnosticPortOptionsExtensions { - public static DiagnosticPortConnectionMode GetConnectionMode(this DiagnosticPortOptions options) - { - return options.ConnectionMode.GetValueOrDefault(DiagnosticPortOptionsDefaults.ConnectionMode); - } + public static DiagnosticPortConnectionMode GetConnectionMode(this DiagnosticPortOptions options) => + options.ConnectionMode.GetValueOrDefault(DiagnosticPortOptionsDefaults.ConnectionMode); + + public static bool GetDeleteEndpointOnStartup(this DiagnosticPortOptions options) + => options.DeleteEndpointOnStartup.GetValueOrDefault(DiagnosticPortOptionsDefaults.DeleteEndpointOnStartup); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/ExperimentalAttribute.cs b/src/Microsoft.Diagnostics.Monitoring.Options/ExperimentalAttribute.cs new file mode 100644 index 00000000000..aac5a7e2887 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/ExperimentalAttribute.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Diagnostics.Monitoring.Options +{ + /// + /// Attribute used to denote that a property belongs to an experimental feature. + /// + [AttributeUsage(AttributeTargets.Property)] + internal class ExperimentalAttribute : Attribute + { + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs index 4d2b7e3df24..1fd8f92e2fa 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptions.cs @@ -3,10 +3,8 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Text; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptionsDefaults.cs b/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptionsDefaults.cs index 9a891fa9877..0913638bffd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptionsDefaults.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/GlobalCounterOptionsDefaults.cs @@ -2,10 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Text; - namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class GlobalCounterOptionsDefaults diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptions.cs new file mode 100644 index 00000000000..c1fbbf2b616 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.Diagnostics.Monitoring.Options +{ + /// + /// Configuration options for in-process features, ones that execute within each target process. + /// + internal class InProcessFeaturesOptions + { + [Experimental] + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_InProcessFeaturesOptions_Enabled))] + [DefaultValue(InProcessFeaturesOptionsDefaults.Enabled)] + public bool? Enabled { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptionsDefaults.cs b/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptionsDefaults.cs new file mode 100644 index 00000000000..1867ab3bcea --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptionsDefaults.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.Options +{ + internal static class InProcessFeaturesOptionsDefaults + { + public const bool Enabled = false; + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptionsExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptionsExtensions.cs new file mode 100644 index 00000000000..0e5ce98fd4f --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/InProcessFeaturesOptionsExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.Options +{ + internal static class InProcessFeaturesOptionsExtensions + { + public static bool GetEnabled(this InProcessFeaturesOptions options) + { + return options.Enabled.GetValueOrDefault(InProcessFeaturesOptionsDefaults.Enabled); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/LogFormat.cs b/src/Microsoft.Diagnostics.Monitoring.Options/LogFormat.cs index d0813f1da6a..49d4fdc6cac 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/LogFormat.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/LogFormat.cs @@ -2,11 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace Microsoft.Diagnostics.Monitoring.Options { public enum LogFormat diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/MetricsOptionsExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/MetricsOptionsExtensions.cs new file mode 100644 index 00000000000..85b4ee48382 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.Options/MetricsOptionsExtensions.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class MetricsOptionsExtensions + { + public static bool GetEnabled(this MetricsOptions options) + { + return options.Enabled.GetValueOrDefault(MetricsOptionsDefaults.Enabled); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs index 9fe20e24497..e4bffb6763a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.Designer.cs @@ -251,7 +251,16 @@ public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_ } /// - /// Looks up a localized string similar to The URI of the Azure queue account.. + /// Looks up a localized string similar to A mapping of metadata keys to environment variable names. The values of the environment variables will be added as metadata for egressed artifacts.. + /// + public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_Metadata { + get { + return ResourceManager.GetString("DisplayAttributeDescription_AzureBlobEgressProviderOptions_Metadata", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The URI of the Azure queue storage account.. /// public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueAccountUri { get { @@ -269,7 +278,27 @@ public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_ } /// - /// Looks up a localized string similar to The shared access signature (SAS) used to access the azure blob storage account.. + /// Looks up a localized string similar to The shared access signature (SAS) used to access the Azure queue storage account.. + /// + public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueSharedAccessSignature { + get { + return ResourceManager.GetString("DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueSharedAccessSigna" + + "ture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of the queue shared access signature (SAS) used to look up the value from the Egress options Properties map.. + /// + public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueSharedAccessSignatureName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_AzureBlobEgressProviderOptions_QueueSharedAccessSigna" + + "tureName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The shared access signature (SAS) used to access the Azure blob and optionally queue storage accounts.. /// public static string DisplayAttributeDescription_AzureBlobEgressProviderOptions_SharedAccessSignature { get { @@ -524,6 +553,42 @@ public static string DisplayAttributeDescription_CollectionRuleTriggerOptions_Ty } } + /// + /// Looks up a localized string similar to The duration of time in which live metrics are collected.. + /// + public static string DisplayAttributeDescription_CollectLiveMetricsOptions_Duration { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectLiveMetricsOptions_Duration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of the egress provider to which the live metrics are egressed.. + /// + public static string DisplayAttributeDescription_CollectLiveMetricsOptions_Egress { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectLiveMetricsOptions_Egress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Determines if the default counter providers should be used.. + /// + public static string DisplayAttributeDescription_CollectLiveMetricsOptions_IncludeDefaultProviders { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectLiveMetricsOptions_IncludeDefaultProviders", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The array of providers for metrics to collect.. + /// + public static string DisplayAttributeDescription_CollectLiveMetricsOptions_Providers { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectLiveMetricsOptions_Providers", resourceCulture); + } + } + /// /// Looks up a localized string similar to The default log level at which logs are collected for entries in the FilterSpecs that do not have a specified LogLevel value.. /// @@ -578,6 +643,24 @@ public static string DisplayAttributeDescription_CollectLogsOptions_UseAppFilter } } + /// + /// Looks up a localized string similar to The name of the egress provider to which the call stacks are egressed.. + /// + public static string DisplayAttributeDescription_CollectStacksOptions_Egress { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectStacksOptions_Egress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The format used to display the callstacks.. + /// + public static string DisplayAttributeDescription_CollectStacksOptions_Format { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectStacksOptions_Format", resourceCulture); + } + } + /// /// Looks up a localized string similar to The size of the event pipe buffer to use in the target process. If the event pipe buffer fills with too many events, newer events will be dropped until the buffer is drained to fit new events.. /// @@ -632,6 +715,15 @@ public static string DisplayAttributeDescription_CollectTraceOptions_RequestRund } } + /// + /// Looks up a localized string similar to The event to watch for while collecting the trace, and once observed the trace will be stopped.. + /// + public static string DisplayAttributeDescription_CollectTraceOptions_StoppingEvent { + get { + return ResourceManager.GetString("DisplayAttributeDescription_CollectTraceOptions_StoppingEvent", resourceCulture); + } + } + /// /// Looks up a localized string similar to Buffer size used when copying data from an egress callback returning a stream to the egress callback that is provided a stream to which data is written.. /// @@ -767,6 +859,15 @@ public static string DisplayAttributeDescription_DiagnosticPortOptions_Connectio } } + /// + /// Looks up a localized string similar to In 'Listen' connection mode, deletes the domain socket file used for diagnostic port communication.. + /// + public static string DisplayAttributeDescription_DiagnosticPortOptions_DeleteEndpointOnStartup { + get { + return ResourceManager.GetString("DisplayAttributeDescription_DiagnosticPortOptions_DeleteEndpointOnStartup", resourceCulture); + } + } + /// /// Looks up a localized string similar to In 'Listen' mode, specifies the name of the named pipe or unix domain socket to use for connecting to the diagnostic server.. /// @@ -930,6 +1031,15 @@ public static string DisplayAttributeDescription_GlobalCounterOptions_IntervalSe } } + /// + /// Looks up a localized string similar to Allows features that require diagnostic components to be loaded into target processes to be enabled. These features may have minimal performance impact on target processes.. + /// + public static string DisplayAttributeDescription_InProcessFeaturesOptions_Enabled { + get { + return ResourceManager.GetString("DisplayAttributeDescription_InProcessFeaturesOptions_Enabled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Gets or sets JsonWriterOptions.. /// @@ -1245,6 +1355,15 @@ public static string DisplayAttributeDescription_SimpleConsoleFormatterOptions_S } } + /// + /// Looks up a localized string similar to The default path where assets will be shared between dotnet-monitor and target processes. Dumps are temporarily stored under this path or in a sub folder unless DumpTempFolder is specified. Shared libraries are stored under this path or in a sub folder unless SharedLibraryPath is specified. On non-Windows platforms, a server diagnostic port is created with the name of 'dotnet-monitor.sock' immediately under this path if running in listen mode unless the diagnostic port is specified on the command line or th [rest of string was truncated]";. + /// + public static string DisplayAttributeDescription_StorageOptions_DefaultSharedPath { + get { + return ResourceManager.GetString("DisplayAttributeDescription_StorageOptions_DefaultSharedPath", resourceCulture); + } + } + /// /// Looks up a localized string similar to The location for temporary dump files. Defaults to the temp folder.. /// @@ -1254,6 +1373,15 @@ public static string DisplayAttributeDescription_StorageOptions_DumpTempFolder { } } + /// + /// Looks up a localized string similar to The location to which libraries shared with target processes will be copied at startup.. + /// + public static string DisplayAttributeDescription_StorageOptions_SharedLibraryPath { + get { + return ResourceManager.GetString("DisplayAttributeDescription_StorageOptions_SharedLibraryPath", resourceCulture); + } + } + /// /// Looks up a localized string similar to The threshold level the counter must maintain (or higher) for the specified duration.. /// @@ -1272,6 +1400,34 @@ public static string DisplayAttributeDescription_ThreadpoolQueueLengthOptions_Le } } + /// + /// Looks up a localized string similar to The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name.. + /// + public static string DisplayAttributeDescription_TraceEventFilter_EventName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_EventName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A mapping of event payload field names to their expected value. A subset of the payload fields may be specified.. + /// + public static string DisplayAttributeDescription_TraceEventFilter_PayloadFilter { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_PayloadFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The event provider that will produce the specified event.. + /// + public static string DisplayAttributeDescription_TraceEventFilter_ProviderName { + get { + return ResourceManager.GetString("DisplayAttributeDescription_TraceEventFilter_ProviderName", resourceCulture); + } + } + + /// /// Looks up a localized string similar to The {0} field, {1} field, or {2} field is required.. /// public static string ErrorMessage_CredentialsMissing { diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx index 154d6035ce1..c5c6e2fcd41 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.Options/OptionsDisplayStrings.resx @@ -194,7 +194,7 @@ The description provided for the ContainerName parameter on AzureBlobEgressProviderOptions. - The shared access signature (SAS) used to access the azure blob storage account. + The shared access signature (SAS) used to access the Azure blob and optionally queue storage accounts. The description provided for the SharedAccessSignature parameter on AzureBlobEgressProviderOptions. @@ -573,7 +573,7 @@ The description provided for the Name parameter on GetEnvironmentVariableOptions. - The URI of the Azure queue account. + The URI of the Azure queue storage account. The description provided for the QueueAccountUri parameter on AzureBlobEgressProviderOptions. @@ -681,4 +681,68 @@ Client id of the Managed Identity used for authentication. The identity must have permissions to create containers and write to blob storage. + + The duration of time in which live metrics are collected. + The description provided for the Duration parameter on CollectLiveMetricsOptions. + + + The name of the egress provider to which the live metrics are egressed. + The description provided for the Egress parameter on CollectLiveMetricsOptions. + + + Determines if the default counter providers should be used. + The description provided for the IncludeDefaultProviders parameter on CollectLiveMetricsOptions. + + + The array of providers for metrics to collect. + The description provided for the Providers parameter on CollectLiveMetricsOptions. + + + In 'Listen' connection mode, deletes the domain socket file used for diagnostic port communication. + + + A mapping of metadata keys to environment variable names. The values of the environment variables will be added as metadata for egressed artifacts. + The description provided for the Metadata parameter on AzureBlobEgressProviderOptions + + + The shared access signature (SAS) used to access the Azure queue storage account. + The description provided for the QueueSharedAccessSignature parameter on AzureBlobEgressProviderOptions. + + + The name of the queue shared access signature (SAS) used to look up the value from the Egress options Properties map. + The description provided for the QueueSharedAccessSignatureName parameter on AzureBlobEgressProviderOptions. + + + The location to which libraries shared with target processes will be copied at startup. + The description provided for the SharedLibraryPath parameter on StorageOptions. + + + The default path where assets will be shared between dotnet-monitor and target processes. Dumps are temporarily stored under this path or in a sub folder unless DumpTempFolder is specified. Shared libraries are stored under this path or in a sub folder unless SharedLibraryPath is specified. On non-Windows platforms, a server diagnostic port is created with the name of 'dotnet-monitor.sock' immediately under this path if running in listen mode unless the diagnostic port is specified on the command line or the DiagnosticPort options are specified. + The description provided for the DefaultSharedPath parameter on StorageOptions. + + + Allows features that require diagnostic components to be loaded into target processes to be enabled. These features may have minimal performance impact on target processes. + + + The format used to display the call stacks. + + + The name of the egress provider to which the call stacks are egressed. + + + The event to watch for while collecting the trace, and once observed the trace will be stopped. + The description provided for the StoppingEvent parameter on CollectTraceOptions. + + + The name of the event, which is a concatenation of the task name and opcode name, if any. The task and opcode names are separated by a '/'. If the event has no opcode, then the event name is just the task name. + The description provided for the EventName parameter on TraceEventFilter. + + + The event provider that will produce the specified event. + The description provided for the ProviderName parameter on TraceEventFilter. + + + A mapping of event payload field names to their expected value. A subset of the payload fields may be specified. + The description provided for the PayloadFilter parameter on TraceEventFilter. + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.Options/StorageOptions.cs b/src/Microsoft.Diagnostics.Monitoring.Options/StorageOptions.cs index 99fbf0478d5..3c8270eaed8 100644 --- a/src/Microsoft.Diagnostics.Monitoring.Options/StorageOptions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.Options/StorageOptions.cs @@ -2,15 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Monitoring.Options; using System.ComponentModel.DataAnnotations; namespace Microsoft.Diagnostics.Monitoring.WebApi { internal class StorageOptions { + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_StorageOptions_DefaultSharedPath))] + public string DefaultSharedPath { get; set; } + [Display( ResourceType = typeof(OptionsDisplayStrings), Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_StorageOptions_DumpTempFolder))] - public string DumpTempFolder {get; set; } + public string DumpTempFolder { get; set; } + + [Experimental] + [Display( + ResourceType = typeof(OptionsDisplayStrings), + Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_StorageOptions_SharedLibraryPath))] + public string SharedLibraryPath { get; set; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/AssemblyExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/AssemblyExtensions.cs new file mode 100644 index 00000000000..19cb93dc598 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/AssemblyExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class AssemblyExtensions + { + public static string GetInformationalVersionString(this Assembly assembly) + { + if (assembly.GetCustomAttribute() + is AssemblyInformationalVersionAttribute assemblyVersionAttribute) + { + return assemblyVersionAttribute.InformationalVersion; + } + else + { + return assembly.GetName().Version.ToString(); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/CollectionRulePipelineState.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/CollectionRulePipelineState.cs new file mode 100644 index 00000000000..0fcf35f0fc9 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/CollectionRulePipelineState.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal class CollectionRulePipelineState + { + public CollectionRuleState CurrentState { get; private set; } = CollectionRuleState.Running; + public string CurrentStateReason { get; private set; } = Strings.Message_CollectionRuleStateReason_Running; + public Queue ExecutionTimestamps { get; set; } + public List AllExecutionTimestamps { get; set; } + public TimeSpan? ActionCountSlidingWindowDuration { get; private set; } + public TimeSpan? RuleDuration { get; private set; } + public int ActionCountLimit { get; private set; } + public DateTime PipelineStartTime { get; private set; } + + // By locking here, the caller isn't forced to remember to lock when updating the state. + // Locking here means that we will lock unnecessarily on the copy of the state; however, + // given the scale of API calls, this should not be a performance issue. + private readonly object _lock = new object(); + + public CollectionRulePipelineState(CollectionRulePipelineState other) + { + // Gets a deep copy of the CollectionRulePipelineState + lock (other._lock) + { + ActionCountLimit = other.ActionCountLimit; + ActionCountSlidingWindowDuration = other.ActionCountSlidingWindowDuration; + RuleDuration = other.RuleDuration; + ExecutionTimestamps = new Queue(other.ExecutionTimestamps); + AllExecutionTimestamps = new List(other.AllExecutionTimestamps); + PipelineStartTime = other.PipelineStartTime; + CurrentState = other.CurrentState; + CurrentStateReason = other.CurrentStateReason; + } + } + + public CollectionRulePipelineState(int actionCountLimit, TimeSpan? actionCountSlidingWindowDuration, TimeSpan? ruleDuration, DateTime pipelineStartTime) + { + ActionCountLimit = actionCountLimit; + ActionCountSlidingWindowDuration = actionCountSlidingWindowDuration; + RuleDuration = ruleDuration; + ExecutionTimestamps = new Queue(ActionCountLimit); + AllExecutionTimestamps = new List(); + PipelineStartTime = pipelineStartTime; + CurrentState = CollectionRuleState.Running; + CurrentStateReason = Strings.Message_CollectionRuleStateReason_Running; + } + + public bool BeginActionExecution(DateTime currentTime) + { + if (!CheckForThrottling(currentTime)) + { + lock (_lock) + { + Debug.Assert(CurrentState != CollectionRuleState.Finished); + + ExecutionTimestamps.Enqueue(currentTime); + AllExecutionTimestamps.Add(currentTime); + + CurrentState = CollectionRuleState.ActionExecuting; + CurrentStateReason = Strings.Message_CollectionRuleStateReason_ExecutingActions; + } + + return true; + } + + return false; + } + + private void ActionExecutionSucceeded() + { + lock (_lock) + { + Debug.Assert(CurrentState == CollectionRuleState.ActionExecuting); + + CurrentState = CollectionRuleState.Running; + CurrentStateReason = Strings.Message_CollectionRuleStateReason_Running; + } + } + + private void ActionExecutionFailed() + { + lock (_lock) + { + Debug.Assert(CurrentState == CollectionRuleState.ActionExecuting); + + CurrentState = CollectionRuleState.Running; + CurrentStateReason = Strings.Message_CollectionRuleStateReason_Running; + } + } + + private void BeginThrottled() + { + lock (_lock) + { + Debug.Assert(CurrentState != CollectionRuleState.Finished); + + CurrentState = CollectionRuleState.Throttled; + CurrentStateReason = Strings.Message_CollectionRuleStateReason_Throttled; + } + } + + private void EndThrottled() + { + lock (_lock) + { + if (CurrentState == CollectionRuleState.Throttled) + { + CurrentState = CollectionRuleState.Running; + CurrentStateReason = Strings.Message_CollectionRuleStateReason_Running; + } + } + } + + public void CollectionRuleFinished(CollectionRuleFinishedStates finishedState) + { + string finishedStateReason = ""; + + switch (finishedState) + { + case CollectionRuleFinishedStates.Startup: + finishedStateReason = Strings.Message_CollectionRuleStateReason_Finished_Startup; + break; + case CollectionRuleFinishedStates.ActionCountReached: + finishedStateReason = Strings.Message_CollectionRuleStateReason_Finished_ActionCount; + break; + case CollectionRuleFinishedStates.RuleDurationReached: + finishedStateReason = Strings.Message_CollectionRuleStateReason_Finished_RuleDuration; + break; + } + + lock (_lock) + { + Debug.Assert(CurrentState != CollectionRuleState.Finished); + + CurrentState = CollectionRuleState.Finished; + CurrentStateReason = finishedStateReason; + } + } + + public void RuleFailure(string errorMessage) + { + lock (_lock) + { + Debug.Assert(CurrentState != CollectionRuleState.Finished); + + CurrentState = CollectionRuleState.Finished; + CurrentStateReason = string.Format(CultureInfo.InvariantCulture, Strings.Message_CollectionRuleStateReason_Finished_Failure, errorMessage); + } + } + + public bool CheckForThrottling(DateTime currentTime) + { + bool isThrottled; + + lock (_lock) + { + DequeueOldTimestamps(ExecutionTimestamps, ActionCountSlidingWindowDuration, currentTime); + isThrottled = CheckForThrottling(ActionCountLimit, ActionCountSlidingWindowDuration, ExecutionTimestamps.Count); + } + + if (!isThrottled) + { + EndThrottled(); + } + else + { + BeginThrottled(); + } + + return isThrottled; + } + + public bool ActionExecutionCompleted(bool success) + { + if (!success) + { + ActionExecutionFailed(); + } + else + { + ActionExecutionSucceeded(); + } + + return success; + } + + public bool CheckForActionCountLimitReached() + { + bool limitReached; + + lock (_lock) + { + limitReached = ActionCountLimit <= ExecutionTimestamps.Count && !ActionCountSlidingWindowDuration.HasValue; + } + + if (limitReached) + { + CollectionRuleFinished(CollectionRuleFinishedStates.ActionCountReached); + } + + return limitReached; + } + + private static void DequeueOldTimestamps(Queue executionTimestamps, TimeSpan? actionCountWindowDuration, DateTime currentTimestamp) + { + // If rule has an action count window, remove all execution timestamps that fall outside the window. + if (actionCountWindowDuration.HasValue) + { + DateTime windowStartTimestamp = currentTimestamp - actionCountWindowDuration.Value; + + while (executionTimestamps.Count > 0) + { + DateTime executionTimestamp = executionTimestamps.Peek(); + if (executionTimestamp < windowStartTimestamp) + { + executionTimestamps.Dequeue(); + } + else + { + // Stop clearing out previous executions + break; + } + } + } + } + + private static bool CheckForThrottling(int actionCountLimit, TimeSpan? actionCountSWD, int executionTimestampsCount) + { + return actionCountSWD.HasValue && actionCountLimit <= executionTimestampsCount; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs index 45a128061a8..7137822e1b4 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.Metrics.cs @@ -5,17 +5,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Diagnostics.Monitoring.EventPipe; -using Microsoft.Diagnostics.Monitoring.WebApi.Validation; -using Microsoft.Diagnostics.NETCore.Client; -using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.IO; -using System.Linq; -using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; namespace Microsoft.Diagnostics.Monitoring.WebApi.Controllers { @@ -47,34 +39,26 @@ public Task CaptureMetrics( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(async (processInfo) => { - string fileName = GetMetricFilename(processInfo); + string fileName = MetricsUtilities.GetMetricFilename(processInfo.EndpointInfo); - Func action = async (outputStream, token) => - { - var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint); - EventPipeCounterPipelineSettings settings = EventCounterSettingsFactory.CreateSettings( - _counterOptions.CurrentValue, - includeDefaults: true, - durationSeconds: durationSeconds); + EventPipeCounterPipelineSettings settings = EventCounterSettingsFactory.CreateSettings( + _counterOptions.CurrentValue, + includeDefaults: true, + durationSeconds: durationSeconds); - await using EventCounterPipeline eventCounterPipeline = new EventCounterPipeline(client, - settings, - loggers: - new[] { new JsonCounterLogger(outputStream) }); - - await eventCounterPipeline.RunAsync(token); - }; + // Allow sync I/O on livemetrics routes due to JsonCounterLogger's usage. + HttpContext.AllowSynchronousIO(); return await Result(Utilities.ArtifactType_Metrics, egressProvider, - action, + (outputStream, token) => MetricsUtilities.CaptureLiveMetricsAsync(null, processInfo.EndpointInfo, settings, outputStream, token), fileName, ContentTypes.ApplicationJsonSequence, - processInfo.EndpointInfo); + processInfo); }, processKey, Utilities.ArtifactType_Metrics); } @@ -107,38 +91,27 @@ public Task CaptureMetricsCustom( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(async (processInfo) => { - string fileName = GetMetricFilename(processInfo); - - Func action = async (outputStream, token) => - { - var client = new DiagnosticsClient(processInfo.EndpointInfo.Endpoint); - EventPipeCounterPipelineSettings settings = EventCounterSettingsFactory.CreateSettings( - _counterOptions.CurrentValue, - durationSeconds, - configuration); + string fileName = MetricsUtilities.GetMetricFilename(processInfo.EndpointInfo); - await using EventCounterPipeline eventCounterPipeline = new EventCounterPipeline(client, - settings, - loggers: - new[] { new JsonCounterLogger(outputStream) }); + EventPipeCounterPipelineSettings settings = EventCounterSettingsFactory.CreateSettings( + _counterOptions.CurrentValue, + durationSeconds, + configuration); - await eventCounterPipeline.RunAsync(token); - }; + // Allow sync I/O on livemetrics routes due to JsonCounterLogger's usage. + HttpContext.AllowSynchronousIO(); return await Result(Utilities.ArtifactType_Metrics, egressProvider, - action, + (outputStream, token) => MetricsUtilities.CaptureLiveMetricsAsync(null, processInfo.EndpointInfo, settings, outputStream, token), fileName, ContentTypes.ApplicationJsonSequence, - processInfo.EndpointInfo); + processInfo); }, processKey, Utilities.ArtifactType_Metrics); } - - private static string GetMetricFilename(IProcessInfo processInfo) => - FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.metrics.json"); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs index c02d2d085e8..917c9386b09 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/DiagController.cs @@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.Monitoring.Options; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Monitoring.WebApi.Stacks; using Microsoft.Diagnostics.Monitoring.WebApi.Validation; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Extensions.DependencyInjection; @@ -43,10 +45,13 @@ public partial class DiagController : ControllerBase private readonly ILogger _logger; private readonly IDiagnosticServices _diagnosticServices; private readonly IOptions _diagnosticPortOptions; + private readonly IInProcessFeatures _inProcessFeatures; private readonly IOptionsMonitor _counterOptions; private readonly EgressOperationStore _operationsStore; private readonly IDumpService _dumpService; private readonly OperationTrackerService _operationTrackerService; + private readonly ICollectionRuleService _collectionRuleService; + private readonly ProfilerChannel _profilerChannel; public DiagController(ILogger logger, IServiceProvider serviceProvider) @@ -54,10 +59,13 @@ public DiagController(ILogger logger, _logger = logger; _diagnosticServices = serviceProvider.GetRequiredService(); _diagnosticPortOptions = serviceProvider.GetService>(); + _inProcessFeatures = serviceProvider.GetRequiredService(); _operationsStore = serviceProvider.GetRequiredService(); _dumpService = serviceProvider.GetRequiredService(); _counterOptions = serviceProvider.GetRequiredService>(); _operationTrackerService = serviceProvider.GetRequiredService(); + _collectionRuleService = serviceProvider.GetRequiredService(); + _profilerChannel = serviceProvider.GetRequiredService(); } /// @@ -75,10 +83,17 @@ public DiagController(ILogger logger, { defaultProcessInfo = await _diagnosticServices.GetProcessAsync(null, HttpContext.RequestAborted); } - catch (Exception) + catch (ArgumentException) { // Unable to locate a default process; no action required } + catch (InvalidOperationException) + { + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + _logger.DefaultProcessUnexpectedFailure(ex); + } IList processesIdentifiers = new List(); foreach (IProcessInfo p in await _diagnosticServices.GetProcessesAsync(processFilter: null, HttpContext.RequestAborted)) @@ -115,7 +130,7 @@ public DiagController(ILogger logger, [FromQuery] string name = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(processInfo => { @@ -153,7 +168,7 @@ public Task>> GetProcessEnvironment( [FromQuery] string name = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess>(async processInfo => { @@ -205,7 +220,7 @@ public Task CaptureDump( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(async processInfo => { @@ -228,7 +243,7 @@ public Task CaptureDump( token => _dumpService.DumpAsync(processInfo.EndpointInfo, type, token), egressProvider, dumpFileName, - processInfo.EndpointInfo, + processInfo, ContentTypes.ApplicationOctetStream, scope), limitKey: Utilities.ArtifactType_Dump); } @@ -262,19 +277,34 @@ public Task CaptureGcDump( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(processInfo => { - string fileName = FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{processInfo.EndpointInfo.ProcessId}.gcdump"); + string fileName = GCDumpUtilities.GenerateGCDumpFileName(processInfo.EndpointInfo); return Result( Utilities.ArtifactType_GCDump, egressProvider, - (stream, token) => GCDumpUtilities.CaptureGCDumpAsync(processInfo.EndpointInfo, stream, token), + async (stream, token) => + { + IDisposable operationRegistration = null; + try + { + if (_diagnosticPortOptions.Value.ConnectionMode == DiagnosticPortConnectionMode.Listen) + { + operationRegistration = _operationTrackerService.Register(processInfo.EndpointInfo); + } + await GCDumpUtilities.CaptureGCDumpAsync(processInfo.EndpointInfo, stream, token); + } + finally + { + operationRegistration?.Dispose(); + } + }, fileName, ContentTypes.ApplicationOctetStream, - processInfo.EndpointInfo); + processInfo); }, processKey, Utilities.ArtifactType_GCDump); } @@ -310,7 +340,7 @@ public Task CaptureTrace( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(processInfo => { @@ -354,11 +384,11 @@ public Task CaptureTraceCustom( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(processInfo => { - foreach(Models.EventPipeProvider provider in configuration.Providers) + foreach (Models.EventPipeProvider provider in configuration.Providers) { if (!CounterValidator.ValidateProvider(_counterOptions.CurrentValue, provider, out string errorMessage)) @@ -405,7 +435,7 @@ public Task CaptureLogs( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(processInfo => { @@ -461,7 +491,7 @@ public Task CaptureLogsCustom( [FromQuery] string egressProvider = null) { - ProcessKey? processKey = GetProcessKey(pid, uid, name); + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); return InvokeForProcess(processInfo => { @@ -480,7 +510,7 @@ public Task CaptureLogsCustom( } /// - /// Gets versioning and listening mode information about Dotnet-Monitor + /// Gets versioning and listening mode information about Dotnet-Monitor /// [HttpGet("info", Name = nameof(GetInfo))] [ProducesWithProblemDetails(ContentTypes.ApplicationJson)] @@ -489,7 +519,7 @@ public Task CaptureLogsCustom( { return this.InvokeService(() => { - string version = GetDotnetMonitorVersion(); + string version = Assembly.GetExecutingAssembly().GetInformationalVersionString(); string runtimeVersion = Environment.Version.ToString(); DiagnosticPortConnectionMode diagnosticPortMode = _diagnosticPortOptions.Value.GetConnectionMode(); string diagnosticPortName = GetDiagnosticPortName(); @@ -507,20 +537,91 @@ public Task CaptureLogsCustom( }, _logger); } - private static string GetDotnetMonitorVersion() + /// + /// Gets a brief summary about the current state of the collection rules. + /// + /// Process ID used to identify the target process. + /// The Runtime instance cookie used to identify the target process. + /// Process name used to identify the target process. + [HttpGet("collectionrules", Name = nameof(GetCollectionRulesDescription))] + [ProducesWithProblemDetails(ContentTypes.ApplicationJson)] + [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] + public Task>> GetCollectionRulesDescription( + [FromQuery] + int? pid = null, + [FromQuery] + Guid? uid = null, + [FromQuery] + string name = null) { - var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + return InvokeForProcess>(processInfo => + { + return _collectionRuleService.GetCollectionRulesDescriptions(processInfo.EndpointInfo); + }, + Utilities.GetProcessKey(pid, uid, name)); + } - var assemblyVersionAttribute = assembly.GetCustomAttribute(); + /// + /// Gets detailed information about the current state of the specified collection rule. + /// + /// The name of the collection rule for which a detailed description should be provided. + /// Process ID used to identify the target process. + /// The Runtime instance cookie used to identify the target process. + /// Process name used to identify the target process. + [HttpGet("collectionrules/{collectionRuleName}", Name = nameof(GetCollectionRuleDetailedDescription))] + [ProducesWithProblemDetails(ContentTypes.ApplicationJson)] + [ProducesResponseType(typeof(CollectionRuleDetailedDescription), StatusCodes.Status200OK)] + public Task> GetCollectionRuleDetailedDescription( + string collectionRuleName, + [FromQuery] + int? pid = null, + [FromQuery] + Guid? uid = null, + [FromQuery] + string name = null) + { + return InvokeForProcess(processInfo => + { + return _collectionRuleService.GetCollectionRuleDetailedDescription(collectionRuleName, processInfo.EndpointInfo); + }, + Utilities.GetProcessKey(pid, uid, name)); + } - if (assemblyVersionAttribute is null) + [HttpGet("stacks", Name = nameof(CaptureStacks))] + [ProducesWithProblemDetails(ContentTypes.ApplicationJson, ContentTypes.TextPlain)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] + [ProducesResponseType(typeof(string), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status202Accepted)] + [RequestLimit(LimitKey = Utilities.ArtifactType_Stacks)] + [EgressValidation] + public async Task CaptureStacks( + [FromQuery] + int? pid = null, + [FromQuery] + Guid? uid = null, + [FromQuery] + string name = null, + [FromQuery] + string egressProvider = null) + { + if (!_inProcessFeatures.IsCallStacksEnabled) { - return assembly.GetName().Version.ToString(); + return NotFound(); } - else + + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); + + return await InvokeForProcess(async processInfo => { - return assemblyVersionAttribute.InformationalVersion; - } + bool plainText = Request.GetTypedHeaders().Accept?.Contains(TextPlainHeader) ?? false; + + return await Result(Utilities.ArtifactType_Stacks, egressProvider, async (stream, token) => + { + await StackUtilities.CollectStacksAsync(null, processInfo.EndpointInfo, _profilerChannel, plainText, stream, token); + + }, StackUtilities.GenerateStacksFilename(processInfo.EndpointInfo, plainText), plainText ? ContentTypes.TextPlain : ContentTypes.ApplicationJson, processInfo, asAttachment: false); + + }, processKey, Utilities.ArtifactType_Stacks); } private string GetDiagnosticPortName() @@ -557,7 +658,7 @@ private Task StartTrace( }, fileName, ContentTypes.ApplicationOctetStream, - processInfo.EndpointInfo); + processInfo); } private Task StartLogs( @@ -571,6 +672,9 @@ private Task StartLogs( return Task.FromResult(this.NotAcceptable()); } + // Allow sync I/O on logging routes due to StreamLogger's usage. + HttpContext.AllowSynchronousIO(); + string fileName = LogsUtilities.GenerateLogsFileName(processInfo.EndpointInfo); string contentType = LogsUtilities.GetLogsContentType(format.Value); @@ -580,15 +684,10 @@ private Task StartLogs( (outputStream, token) => LogsUtilities.CaptureLogsAsync(null, format.Value, processInfo.EndpointInfo, settings, outputStream, token), fileName, contentType, - processInfo.EndpointInfo, + processInfo, format != LogFormat.PlainText); } - private static ProcessKey? GetProcessKey(int? pid, Guid? uid, string name) - { - return (pid == null && uid == null && name == null) ? null : new ProcessKey(pid, uid, name); - } - private static LogFormat? ComputeLogFormat(IList acceptedHeaders) { if (acceptedHeaders == null) @@ -629,10 +728,10 @@ private Task Result( Func action, string fileName, string contentType, - IEndpointInfo endpointInfo, + IProcessInfo processInfo, bool asAttachment = true) { - KeyValueLogScope scope = Utilities.CreateArtifactScope(artifactType, endpointInfo); + KeyValueLogScope scope = Utilities.CreateArtifactScope(artifactType, processInfo.EndpointInfo); if (string.IsNullOrEmpty(providerName)) { @@ -648,7 +747,7 @@ private Task Result( action, providerName, fileName, - endpointInfo, + processInfo, contentType, scope), limitKey: artifactType); @@ -718,4 +817,4 @@ private async Task> InvokeForProcess(Func { - if (!_metricsOptions.Enabled.GetValueOrDefault(MetricsOptionsDefaults.Enabled)) + if (!_metricsOptions.GetEnabled()) { throw new InvalidOperationException(Strings.ErrorMessage_MetricsDisabled); } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs index 14bcf456d8d..7eb26f5680b 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Controllers/OperationsController.cs @@ -9,8 +9,6 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi.Controllers { @@ -32,14 +30,28 @@ public OperationsController(ILogger logger, IServiceProvid _operationsStore = serviceProvider.GetRequiredService(); } + /// + /// Gets the operations list for the specified process (or all processes if left unspecified). + /// + /// Process ID used to identify the target process. + /// The Runtime instance cookie used to identify the target process. + /// Process name used to identify the target process. [HttpGet] [ProducesWithProblemDetails(ContentTypes.ApplicationJson)] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public ActionResult> GetOperations() + public ActionResult> GetOperations( + [FromQuery] + int? pid = null, + [FromQuery] + Guid? uid = null, + [FromQuery] + string name = null) { + ProcessKey? processKey = Utilities.GetProcessKey(pid, uid, name); + return this.InvokeService(() => { - return new ActionResult>(_operationsStore.GetOperations()); + return new ActionResult>(_operationsStore.GetOperations(processKey)); }, _logger); } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs index 2f38c12af19..515b77885d7 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/DumpService.cs @@ -5,7 +5,6 @@ using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Extensions.Options; using System; -using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.IO; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EgressValidationAttribute.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EgressValidationAttribute.cs index a7eb31c1fca..d45b0006e24 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EgressValidationAttribute.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EgressValidationAttribute.cs @@ -6,8 +6,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using System; namespace Microsoft.Diagnostics.Monitoring.WebApi diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoExtensions.cs new file mode 100644 index 00000000000..10b62848bec --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoExtensions.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class IEndpointInfoExtensions + { + public static bool TargetFrameworkSupportsProcessEnv(this IEndpointInfo endpointInfo) + { + return endpointInfo.RuntimeInstanceCookie != Guid.Empty; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoSource.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoSource.cs index 7419458808b..6bb6efb0666 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoSource.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EndpointInfo/IEndpointInfoSource.cs @@ -23,6 +23,29 @@ internal interface IEndpointInfo string OperatingSystem { get; } string ProcessArchitecture { get; } + + Version RuntimeVersion { get; } + } + + /// + /// Because IpcEndpoint may not be visible outside of this assembly (we have access to it here through InternalsVisibleTo), we + /// create a base class that allows skipping the Endpoint property. + /// + internal abstract class EndpointInfoBase : IEndpointInfo + { + public virtual IpcEndpoint Endpoint + { + get => throw new NotImplementedException(); + protected set => throw new NotImplementedException(); + } + + public abstract int ProcessId { get; protected set; } + public abstract Guid RuntimeInstanceCookie { get; protected set; } + public abstract string CommandLine { get; protected set; } + public abstract string OperatingSystem { get; protected set; } + public abstract string ProcessArchitecture { get; protected set; } + + public abstract Version RuntimeVersion { get; protected set; } } public interface IEndpointInfoSource diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs new file mode 100644 index 00000000000..7312162b2cb --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/EventMonitoringPassthroughStream.cs @@ -0,0 +1,260 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Tracing; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// + /// A stream that can monitor an event stream which is compatible with + /// for a specific event while also passing along the event data to a destination stream. + /// + public sealed class EventMonitoringPassthroughStream : Stream + { + private readonly Action _onPayloadFilterMismatch; + private readonly Action _onEvent; + private readonly bool _callOnEventOnlyOnce; + + private readonly Stream _sourceStream; + private readonly Stream _destinationStream; + private EventPipeEventSource _eventSource; + + private readonly string _providerName; + private readonly string _eventName; + + // The original payload filter of fieldName->fieldValue specified by the user. It will only be used to hydrate _payloadFilterIndexCache. + private readonly IDictionary _payloadFilter; + + // This tracks the exact indices into the provided event's payload to check for the expected values instead + // of repeatedly searching the payload for the field names in _payloadFilter. + private Dictionary _payloadFilterIndexCache; + + + /// + /// A stream that can monitor an event stream which is compatible with + /// for a specific event while also passing along the event data to a destination stream. + /// + /// The stopping event provider name. + /// The stopping event name, which is the concatenation of the task name and opcode name, if set. for more information about the format. + /// A mapping of the stopping event payload field names to their expected values. A subset of the payload fields may be specified. + /// A callback that will be invoked each time the requested event has been observed. + /// A callback that will be invoked if the field names specified in do not match those in the event's manifest. + /// The source event stream which is compatible with . + /// The destination stream to write events. It must either be full duplex or be write-only. + /// The size of the buffer to use when writing to the . + /// If true, the provided will only be called for the first matching event. + /// If true, the provided will not be automatically closed when this class is. + public EventMonitoringPassthroughStream( + string providerName, + string eventName, + IDictionary payloadFilter, + Action onEvent, + Action onPayloadFilterMismatch, + Stream sourceStream, + Stream destinationStream, + int bufferSize, + bool callOnEventOnlyOnce, + bool leaveDestinationStreamOpen) : base() + { + _providerName = providerName; + _eventName = eventName; + _onEvent = onEvent; + _onPayloadFilterMismatch = onPayloadFilterMismatch; + _sourceStream = sourceStream; + _payloadFilter = payloadFilter; + _callOnEventOnlyOnce = callOnEventOnlyOnce; + + // Wrap a buffered stream around the destination stream + // to avoid slowing down the event processing with the data + // passthrough unless there is significant pressure. + _destinationStream = new BufferedStream( + leaveDestinationStreamOpen + ? new StreamLeaveOpenWrapper(destinationStream) + : destinationStream, + bufferSize); + } + + /// + /// Start processing the event stream, monitoring it for the requested event and transferring its data to the specified destination stream. + /// This will continue to run until the event stream is complete or a stop is requested, regardless of if the requested event has been observed. + /// + /// The cancellation token. + /// + public Task ProcessAsync(CancellationToken token) + { + return Task.Run(() => + { + _eventSource = new EventPipeEventSource(this); + token.ThrowIfCancellationRequested(); + using IDisposable registration = token.Register(() => _eventSource.Dispose()); + + _eventSource.Dynamic.AddCallbackForProviderEvent(_providerName, _eventName, TraceEventCallback); + + // The EventPipeEventSource will drive the transferring of data to the destination stream as it processes events. + _eventSource.Process(); + token.ThrowIfCancellationRequested(); + }, token); + } + + /// + /// Stops monitoring for the specified stopping event. Data will continue to be written to the provided destination stream. + /// + private void StopMonitoringForEvent() + { + _eventSource?.Dynamic.RemoveCallback(TraceEventCallback); + } + + private void TraceEventCallback(TraceEvent obj) + { + if (_payloadFilterIndexCache == null && !HydratePayloadFilterCache(obj)) + { + // The payload filter doesn't map onto the actual data, + // we'll never match the event so stop checking it + // and proceed with just transferring the data to the destination stream. + StopMonitoringForEvent(); + _onPayloadFilterMismatch(obj); + return; + } + + if (!DoesPayloadMatch(obj)) + { + return; + } + + if (_callOnEventOnlyOnce) + { + StopMonitoringForEvent(); + } + + _onEvent(obj); + } + + /// + /// Hydrates the payload filter cache. + /// + /// An instance of the stopping event (matching provider, task name, and opcode), but without checking the payload yet. + /// + private bool HydratePayloadFilterCache(TraceEvent obj) + { + if (_payloadFilterIndexCache != null) + { + return true; + } + + // If there's no payload filter, there's nothing to do. + if (_payloadFilter == null || _payloadFilter.Count == 0) + { + _payloadFilterIndexCache = new Dictionary(capacity: 0); + return true; + } + + // If the payload has fewer fields than the requested filter, we can never match it. + // NOTE: this function will only ever be called with an instance of the stopping event + // (matching provider, task name, and opcode) but without checking the payload yet. + if (obj.PayloadNames.Length < _payloadFilter.Count) + { + return false; + } + + Dictionary payloadFilterCache = new(capacity: _payloadFilter.Count); + for (int i = 0; (i < obj.PayloadNames.Length) && (payloadFilterCache.Count < _payloadFilter.Count); i++) + { + if (_payloadFilter.TryGetValue(obj.PayloadNames[i], out string payloadValue)) + { + payloadFilterCache.Add(i, payloadValue); + } + } + + // Check if one or more of the requested filter field names did not exist on the actual payload. + if (_payloadFilter.Count != payloadFilterCache.Count) + { + return false; + } + + _payloadFilterIndexCache = payloadFilterCache; + + return true; + } + + private bool DoesPayloadMatch(TraceEvent obj) + { + foreach (var (fieldIndex, expectedValue) in _payloadFilterIndexCache) + { + string fieldValue = Convert.ToString(obj.PayloadValue(fieldIndex), CultureInfo.InvariantCulture) ?? string.Empty; + if (!string.Equals(fieldValue, expectedValue, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + + public override int Read(byte[] buffer, int offset, int count) + { + return Read(buffer.AsSpan(offset, count)); + } + + public override int Read(Span buffer) + { + int bytesRead = _sourceStream.Read(buffer); + if (bytesRead != 0) + { + _destinationStream.Write(buffer[..bytesRead]); + } + + return bytesRead; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int bytesRead = await _sourceStream.ReadAsync(buffer, cancellationToken); + if (bytesRead != 0) + { + await _destinationStream.WriteAsync(buffer[..bytesRead], cancellationToken); + } + + return bytesRead; + } + + public override bool CanSeek => false; + public override bool CanWrite => false; + + public override bool CanTimeout => _sourceStream.CanRead; + public override bool CanRead => _sourceStream.CanRead; + public override long Length => _sourceStream.Length; + + public override long Position { get => _sourceStream.Position; set => _sourceStream.Position = value; } + public override int ReadTimeout { get => _sourceStream.ReadTimeout; set => _sourceStream.ReadTimeout = value; } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override void CopyTo(Stream destination, int bufferSize) => throw new NotSupportedException(); + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => throw new NotSupportedException(); + + public override void Flush() => _destinationStream.Flush(); + public override Task FlushAsync(CancellationToken cancellationToken) => _destinationStream.FlushAsync(cancellationToken); + + public override async ValueTask DisposeAsync() + { + _eventSource?.Dispose(); + await _sourceStream.DisposeAsync(); + await _destinationStream.DisposeAsync(); + await base.DisposeAsync(); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/HttpContextExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/HttpContextExtensions.cs new file mode 100644 index 00000000000..70f23db0cf7 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/HttpContextExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class HttpContextExtensions + { + public static void AllowSynchronousIO(this HttpContext httpContext) + { + var syncIOFeature = httpContext.Features.Get(); + if (null == syncIOFeature) + { + Debug.Fail($"Unable to obtain {nameof(IHttpBodyControlFeature)}"); + } + else + { + syncIOFeature.AllowSynchronousIO = true; + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/ICollectionRuleService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/ICollectionRuleService.cs new file mode 100644 index 00000000000..4b06dd7f276 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/ICollectionRuleService.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Extensions.Hosting; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal interface ICollectionRuleService : IHostedService, IAsyncDisposable + { + Dictionary GetCollectionRulesDescriptions(IEndpointInfo endpointInfo); + + CollectionRuleDetailedDescription GetCollectionRuleDetailedDescription(string collectionRuleName, IEndpointInfo endpointInfo); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressOutputConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressOutputConfiguration.cs index 52e5381d8e5..986773100b5 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressOutputConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressOutputConfiguration.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; - namespace Microsoft.Diagnostics.Monitoring.WebApi { public interface IEgressOutputConfiguration diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs index aba8e5b5739..ad9c92832de 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IEgressService.cs @@ -19,6 +19,7 @@ Task EgressAsync( string fileName, string contentType, IEndpointInfo source, + CollectionRuleMetadata collectionRuleMetadata, CancellationToken token); Task EgressAsync( @@ -27,6 +28,7 @@ Task EgressAsync( string fileName, string contentType, IEndpointInfo source, + CollectionRuleMetadata collectionRuleMetadata, CancellationToken token); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetFrameworkReference.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IExperimentalFlags.cs similarity index 63% rename from src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetFrameworkReference.cs rename to src/Microsoft.Diagnostics.Monitoring.WebApi/IExperimentalFlags.cs index 86c2ef6db46..cfd9fe2453c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetFrameworkReference.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IExperimentalFlags.cs @@ -2,11 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#if UNITTEST namespace Microsoft.Diagnostics.Monitoring.TestCommon +#else +namespace Microsoft.Diagnostics.Monitoring.WebApi +#endif { - public enum DotNetFrameworkReference + internal interface IExperimentalFlags { - Microsoft_NetCore_App, - Microsoft_AspNetCore_App + bool IsCallStacksEnabled { get; } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/IInProcessFeatures.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/IInProcessFeatures.cs new file mode 100644 index 00000000000..fe089d00cee --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/IInProcessFeatures.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + public interface IInProcessFeatures + { + bool IsCallStacksEnabled { get; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs index f583c437ea8..7bd8da75bf9 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/LoggingExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Tracing; using Microsoft.Extensions.Logging; using System; @@ -45,6 +46,24 @@ internal static class LoggingExtensions logLevel: LogLevel.Warning, formatString: Strings.LogFormatString_ThrottledEndpoint); + private static readonly Action _defaultProcessUnexpectedFailure = + LoggerMessage.Define( + eventId: new EventId(7, "DefaultProcessUnexpectedFailure"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_DefaultProcessUnexpectedFailure); + + private static readonly Action _stoppingTraceEventHit = + LoggerMessage.Define( + eventId: new EventId(8, "StoppingTraceEventHit"), + logLevel: LogLevel.Debug, + formatString: Strings.LogFormatString_StoppingTraceEventHit); + + private static readonly Action _stoppingTraceEventPayloadFilterMismatch = + LoggerMessage.Define( + eventId: new EventId(9, "StoppingTraceEventPayloadFilterMismatch"), + logLevel: LogLevel.Warning, + formatString: Strings.LogFormatString_StoppingTraceEventPayloadFilterMismatch); + public static void RequestFailed(this ILogger logger, Exception ex) { _requestFailed(logger, ex); @@ -74,5 +93,20 @@ public static void WrittenToHttpStream(this ILogger logger) { _writtenToHttpStream(logger, null); } + + public static void DefaultProcessUnexpectedFailure(this ILogger logger, Exception ex) + { + _defaultProcessUnexpectedFailure(logger, ex); + } + + public static void StoppingTraceEventHit(this ILogger logger, TraceEvent traceEvent) + { + _stoppingTraceEventHit(logger, traceEvent.ProviderName, traceEvent.EventName, null); + } + + public static void StoppingTraceEventPayloadFilterMismatch(this ILogger logger, TraceEvent traceEvent) + { + _stoppingTraceEventPayloadFilterMismatch(logger, traceEvent.ProviderName, traceEvent.EventName, string.Join(' ', traceEvent.PayloadNames), null); + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/EventCounterSettingsFactory.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/EventCounterSettingsFactory.cs index 5e05c3d2fe9..8767cb419ff 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/EventCounterSettingsFactory.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/EventCounterSettingsFactory.cs @@ -3,12 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.EventPipe; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/IMetricsStore.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/IMetricsStore.cs index e8806d88700..85c48405eab 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/IMetricsStore.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/IMetricsStore.cs @@ -4,9 +4,7 @@ using Microsoft.Diagnostics.Monitoring.EventPipe; using System; -using System.Collections.Generic; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/JsonCounterLogger.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/JsonCounterLogger.cs index 8387eab4249..041a5d1fda2 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/JsonCounterLogger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/JsonCounterLogger.cs @@ -3,12 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.EventPipe; -using System; -using System.Globalization; using System.IO; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsLogger.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsLogger.cs index b77f4e71fc6..09f8cd5b617 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsLogger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsLogger.cs @@ -3,10 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.EventPipe; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsService.cs index 266a444c8a7..2808dfbf1bc 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsService.cs @@ -4,11 +4,10 @@ using Microsoft.Diagnostics.Monitoring.EventPipe; using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -25,19 +24,24 @@ internal sealed class MetricsService : BackgroundService private IOptionsMonitor _optionsMonitor; private IOptionsMonitor _counterOptions; - public MetricsService(IDiagnosticServices services, + public MetricsService(IServiceProvider serviceProvider, IOptionsMonitor optionsMonitor, IOptionsMonitor counterOptions, MetricsStoreService metricsStore) { _store = metricsStore; - _services = services; + _services = serviceProvider.GetRequiredService(); _optionsMonitor = optionsMonitor; _counterOptions = counterOptions; } - + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (!_optionsMonitor.CurrentValue.GetEnabled()) + { + return; + } + while (!stoppingToken.IsCancellationRequested) { stoppingToken.ThrowIfCancellationRequested(); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStore.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStore.cs index bc14f89178c..b3a3183c75d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStore.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStore.cs @@ -4,12 +4,9 @@ using Microsoft.Diagnostics.Monitoring.EventPipe; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -89,15 +86,13 @@ public async Task SnapshotMetrics(Stream outputStream, CancellationToken token) } } - using var writer = new StreamWriter(outputStream, EncodingCache.UTF8NoBOMNoThrow, bufferSize: 1024, leaveOpen: true); + await using var writer = new StreamWriter(outputStream, EncodingCache.UTF8NoBOMNoThrow, bufferSize: 1024, leaveOpen: true); writer.NewLine = "\n"; foreach (var metricGroup in copy) { ICounterPayload metricInfo = metricGroup.Value.First(); - string metricName = PrometheusDataModel.Normalize(metricInfo.Provider, metricInfo.Name, - metricInfo.Unit, metricInfo.Value, out string metricValue); - + string metricName = PrometheusDataModel.GetPrometheusNormalizedName(metricInfo.Provider, metricInfo.Name, metricInfo.Unit); string metricType = "gauge"; //TODO Some clr metrics claim to be incrementing, but are really gauges. @@ -107,6 +102,7 @@ public async Task SnapshotMetrics(Stream outputStream, CancellationToken token) foreach (var metric in metricGroup.Value) { + string metricValue = PrometheusDataModel.GetPrometheusNormalizedValue(metric.Unit, metric.Value); await WriteMetricDetails(writer, metric, metricName, metricValue); } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStoreService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStoreService.cs index 41fbdda91c2..2e3473a0f98 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStoreService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/MetricsStoreService.cs @@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi internal sealed class MetricsStoreService : IDisposable { public MetricsStore MetricsStore { get; } - + public MetricsStoreService( IOptions options) { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/PrometheusDataModel.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/PrometheusDataModel.cs index 52635082bd2..98f7cc0c36d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/PrometheusDataModel.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/PrometheusDataModel.cs @@ -3,13 +3,10 @@ // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostics.Monitoring.EventPipe; using System; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { @@ -26,18 +23,13 @@ internal static class PrometheusDataModel {"%", "ratio" }, }; - public static string Normalize(string metricProvider, string metric, string unit, double value, out string metricValue) + public static string GetPrometheusNormalizedName(string metricProvider, string metric, string unit) { string baseUnit = null; if ((unit != null) && (!KnownUnits.TryGetValue(unit, out baseUnit))) { baseUnit = unit; } - if (string.Equals(unit, "MB", StringComparison.OrdinalIgnoreCase)) - { - value *= 1_000_000; //Note that the metric uses MB not MiB - } - metricValue = value.ToString(CultureInfo.InvariantCulture); bool hasUnit = !string.IsNullOrEmpty(baseUnit); @@ -57,6 +49,15 @@ public static string Normalize(string metricProvider, string metric, string unit return builder.ToString(); } + public static string GetPrometheusNormalizedValue(string unit, double value) + { + if (string.Equals(unit, "MB", StringComparison.OrdinalIgnoreCase)) + { + value *= 1_000_000; //Note that the metric uses MB not MiB + } + return value.ToString(CultureInfo.InvariantCulture); + } + private static void NormalizeString(StringBuilder builder, string entity, bool isProvider) { //TODO We don't have any labels in the current metrics implementation, but may need to add support for it diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/StreamingCounterLogger.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/StreamingCounterLogger.cs index 0ce0df97899..df1d35a1f24 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/StreamingCounterLogger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Metrics/StreamingCounterLogger.cs @@ -3,11 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.EventPipe; -using System; using System.IO; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { @@ -17,7 +13,7 @@ internal abstract class StreamingCounterLogger : ICountersLogger protected abstract void SerializeCounter(Stream stream, ICounterPayload counter); - protected virtual void Cleanup() {} + protected virtual void Cleanup() { } protected StreamingCounterLogger(Stream stream) { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj b/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj index 680b3654bb5..f3b404958ce 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Microsoft.Diagnostics.Monitoring.WebApi.csproj @@ -16,6 +16,11 @@ true true Library + + false diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs new file mode 100644 index 00000000000..c75716fdfa6 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CallStackResults.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Models +{ + public class CallStackFrame + { + [JsonPropertyName("methodName")] + public string MethodName { get; set; } + + [JsonPropertyName("className")] + public string ClassName { get; set; } + + [JsonPropertyName("moduleName")] + public string ModuleName { get; set; } + + //TODO Bring this back once we have a relative il offset value. + //[JsonPropertyName("offset")] + //public ulong Offset { get; set; } + } + + public class CallStack + { + [JsonPropertyName("threadId")] + public uint ThreadId { get; set; } + + [JsonPropertyName("frames")] + public IList Frames { get; set; } = new List(); + } + + public class CallStackResult + { + [JsonPropertyName("stacks")] + public IList Stacks { get; set; } = new List(); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CollectionRuleDescription.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CollectionRuleDescription.cs new file mode 100644 index 00000000000..9fc9164c2f3 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CollectionRuleDescription.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Models +{ + public record class CollectionRuleDescription + { + /// + /// Indicates what state the collection rule is in for the process. + /// + [JsonPropertyName("state")] + public CollectionRuleState State { get; set; } + + /// + /// Human-readable explanation for the current state of the collection rule. + /// + [JsonPropertyName("stateReason")] + public string StateReason { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CollectionRuleDetailedDescription.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CollectionRuleDetailedDescription.cs new file mode 100644 index 00000000000..d9ad00aeb50 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/CollectionRuleDetailedDescription.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.Json.Serialization; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Models +{ + public record class CollectionRuleDetailedDescription : CollectionRuleDescription + { + /// + /// The number of times the trigger has executed for a process in its lifetime. + /// + [JsonPropertyName("lifetimeOccurrences")] + public int LifetimeOccurrences { get; set; } + + /// + /// The number of times the trigger has executed for a process in the current sliding window duration (as defined by Limits). + /// + [JsonPropertyName("slidingWindowOccurrences")] + public int SlidingWindowOccurrences { get; set; } + + /// + /// The number of times the trigger can execute for a process before being limited (as defined by Limits). + /// + [JsonPropertyName("actionCountLimit")] + public int ActionCountLimit { get; set; } + + /// + /// The sliding window duration in which the actionCountLimit is the maximum number of occurrences (as defined by Limits). + /// + [JsonPropertyName("actionCountSlidingWindowDurationLimit")] + public TimeSpan? ActionCountSlidingWindowDurationLimit { get; set; } + + /// + /// The amount of time that needs to pass before the slidingWindowOccurrences drops below the actionCountLimit + /// + [JsonPropertyName("slidingWindowDurationCountdown")] + public TimeSpan? SlidingWindowDurationCountdown { get; set; } + + /// + /// The amount of time that needs to pass before the rule is finished + /// + [JsonPropertyName("ruleFinishedCountdown")] + public TimeSpan? RuleFinishedCountdown { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs index 3962d2838a7..54edfca8e24 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EgressOperationStatus.cs @@ -20,6 +20,24 @@ public class OperationSummary [JsonPropertyName("status")] public OperationState Status { get; set; } + + [JsonPropertyName("process")] + public OperationProcessInfo Process { get; set; } + } + + /// + /// Represents the details of a given process used in an operation. + /// + public class OperationProcessInfo + { + [JsonPropertyName("pid")] + public int ProcessId { get; set; } + + [JsonPropertyName("uid")] + public Guid Uid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } } /// diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EventMetricsConfiguration.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EventMetricsConfiguration.cs index dcc7b6d2776..e25512b98b7 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EventMetricsConfiguration.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/EventMetricsConfiguration.cs @@ -2,12 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Text.Json.Serialization; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi.Models { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ProcessInfo.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ProcessInfo.cs index 0524f7f5730..1095fffed38 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ProcessInfo.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Models/ProcessInfo.cs @@ -27,4 +27,4 @@ public class ProcessInfo [JsonPropertyName("processArchitecture")] public string ProcessArchitecture { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs index 704cd59520a..756a9ab3d71 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperation.cs @@ -16,17 +16,37 @@ internal class EgressOperation : IEgressOperation private readonly Func> _egress; private readonly string _egressProvider; private readonly KeyValueLogScope _scope; + public EgressProcessInfo ProcessInfo { get; private set; } - public EgressOperation(Func> action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope) + public EgressOperation(Func> action, string endpointName, string artifactName, IProcessInfo processInfo, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata = null) { - _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, token); + _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, processInfo.EndpointInfo, collectionRuleMetadata, token); _egressProvider = endpointName; _scope = scope; + + ProcessInfo = new EgressProcessInfo(processInfo.ProcessName, processInfo.EndpointInfo.ProcessId, processInfo.EndpointInfo.RuntimeInstanceCookie); + } + + public EgressOperation(Func action, string endpointName, string artifactName, IProcessInfo processInfo, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata = null) + { + _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, processInfo.EndpointInfo, collectionRuleMetadata, token); + _egressProvider = endpointName; + _scope = scope; + + ProcessInfo = new EgressProcessInfo(processInfo.ProcessName, processInfo.EndpointInfo.ProcessId, processInfo.EndpointInfo.RuntimeInstanceCookie); } - public EgressOperation(Func action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope) + // The below constructors don't need EgressProcessInfo as their callers don't store to the operations table. + public EgressOperation(Func action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata) { - _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, token); + _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, collectionRuleMetadata, token); + _egressProvider = endpointName; + _scope = scope; + } + + public EgressOperation(Func> action, string endpointName, string artifactName, IEndpointInfo source, string contentType, KeyValueLogScope scope, CollectionRuleMetadata collectionRuleMetadata) + { + _egress = (service, token) => service.EgressAsync(endpointName, action, artifactName, contentType, source, collectionRuleMetadata, token); _egressProvider = endpointName; _scope = scope; } @@ -56,7 +76,7 @@ public async Task> ExecuteAsync(IServiceProvider s return ExecutionResult.Succeeded(egressResult); }, logger, token); } - + public void Validate(IServiceProvider serviceProvider) { serviceProvider @@ -64,4 +84,18 @@ public void Validate(IServiceProvider serviceProvider) .ValidateProvider(_egressProvider); } } + + internal class EgressProcessInfo + { + public string ProcessName { get; } + public int ProcessId { get; } + public Guid RuntimeInstanceCookie { get; } + + public EgressProcessInfo(string processName, int processId, Guid runtimeInstanceCookie) + { + this.ProcessName = processName; + this.ProcessId = processId; + this.RuntimeInstanceCookie = runtimeInstanceCookie; + } + } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationQueue.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationQueue.cs index f381e4aa3fa..69b5b3c2c83 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationQueue.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationQueue.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs index 31f0e84d018..5c59eaaf5f3 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationService.cs @@ -4,8 +4,6 @@ using Microsoft.Extensions.Hosting; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs index ac0192d57e9..4408ab7f569 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressOperationStore.cs @@ -2,13 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi @@ -18,7 +15,7 @@ internal sealed class EgressOperationStore private sealed class EgressEntry { public ExecutionResult ExecutionResult { get; set; } - public Models.OperationState State { get; set;} + public Models.OperationState State { get; set; } public EgressRequest EgressRequest { get; set; } @@ -124,15 +121,57 @@ public void CompleteOperation(Guid operationId, ExecutionResult re } } - public IEnumerable GetOperations() + public IEnumerable GetOperations(ProcessKey? processKey) { lock (_requests) { - return _requests.Select((kvp) => new Models.OperationSummary + IEnumerable> requests = _requests; + + if (null != processKey) + { + requests = requests.Where((kvp) => + { + EgressProcessInfo processInfo = kvp.Value.EgressRequest.EgressOperation.ProcessInfo; + + // Check that if a field is specified, it meets the conditions. + if (!string.IsNullOrEmpty(processKey.Value.ProcessName) + && processInfo.ProcessName != processKey.Value.ProcessName) + { + return false; + } + + if (processKey.Value.ProcessId.HasValue + && processInfo.ProcessId != processKey.Value.ProcessId.Value) + { + return false; + } + + if (processKey.Value.RuntimeInstanceCookie.HasValue + && processInfo.RuntimeInstanceCookie != processKey.Value.RuntimeInstanceCookie.Value) + { + return false; + } + + return true; + }); + } + + return requests.Select((kvp) => { - OperationId = kvp.Key, - CreatedDateTime = kvp.Value.CreatedDateTime, - Status = kvp.Value.State + EgressProcessInfo processInfo = kvp.Value.EgressRequest.EgressOperation.ProcessInfo; + return new Models.OperationSummary + { + OperationId = kvp.Key, + CreatedDateTime = kvp.Value.CreatedDateTime, + Status = kvp.Value.State, + Process = processInfo != null ? + new Models.OperationProcessInfo + { + Name = processInfo.ProcessName, + ProcessId = processInfo.ProcessId, + Uid = processInfo.RuntimeInstanceCookie + } : null + }; }).ToList(); } } @@ -145,12 +184,20 @@ public Models.OperationStatus GetOperationStatus(Guid operationId) { throw new InvalidOperationException(Strings.ErrorMessage_OperationNotFound); } + EgressProcessInfo processInfo = entry.EgressRequest.EgressOperation.ProcessInfo; var status = new Models.OperationStatus() { OperationId = entry.EgressRequest.OperationId, Status = entry.State, CreatedDateTime = entry.CreatedDateTime, + Process = processInfo != null ? + new Models.OperationProcessInfo + { + Name = processInfo.ProcessName, + ProcessId = processInfo.ProcessId, + Uid = processInfo.RuntimeInstanceCookie + } : null }; if (entry.State == Models.OperationState.Succeeded) diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressRequest.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressRequest.cs index 3ff5d407c06..bb914d8ccfd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressRequest.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/EgressRequest.cs @@ -3,10 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs index 28e6ee9fb0c..057fec61971 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/IEgressOperation.cs @@ -10,6 +10,8 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal interface IEgressOperation { + public EgressProcessInfo ProcessInfo { get; } + Task> ExecuteAsync(IServiceProvider serviceProvider, CancellationToken token); void Validate(IServiceProvider serviceProvider); diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/OperationExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/OperationExtensions.cs index 5be176d6898..2d040d5d9e0 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/OperationExtensions.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/OperationExtensions.cs @@ -3,10 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/TooManyRequestsException.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/TooManyRequestsException.cs index 1cf54bdece0..f13bdd54b11 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/TooManyRequestsException.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Operation/TooManyRequestsException.cs @@ -2,11 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - namespace Microsoft.Diagnostics.Monitoring.WebApi { internal sealed class TooManyRequestsException : MonitoringException diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/OutputStreamResult.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/OutputStreamResult.cs index 2468c168db3..0ff26cdeebd 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/OutputStreamResult.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/OutputStreamResult.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.IO; using System.Net.Http.Headers; using System.Threading; diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs index 3a55dc2e993..6eaa99eabb2 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/ProcessInfoImpl.cs @@ -64,7 +64,7 @@ public static async Task FromEndpointInfoAsync(IEndpointInfo endpo string commandLine = endpointInfo.CommandLine; if (string.IsNullOrEmpty(commandLine)) { - // The EventProcessInfoPipeline will frequently hang during disposal of its + // The EventProcessInfoPipeline will frequently block during disposal of its // EventPipeStreamProvider, which is trying to send a SessionStop command to // stop the event pipe session. When this happens, it waits for the 30 timeout // before giving up. Because this is happening during a disposal call, it is @@ -92,7 +92,10 @@ public static async Task FromEndpointInfoAsync(IEndpointInfo endpo commandLine = await commandLineSource.Task; } - catch + catch (PipelineException) + { + } + catch (OperationCanceledException) { } finally diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/ProfilerChannel.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/ProfilerChannel.cs new file mode 100644 index 00000000000..db248214f59 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/ProfilerChannel.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Options; +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal enum ProfilerMessageType : short + { + OK, + Error, + Callstack + }; + + internal struct ProfilerMessage + { + public ProfilerMessageType MessageType { get; set; } + + // This is currently unsupported, but some possible future additions: + // Parameter Metadata. (I.e. IMetadataImport.GetMethodProps + signature resolution) + // Resolve frame offsets (Resolving absolute native address to relative offset then convert to IL using IL-to-native maps. + public int Parameter { get; set; } + } + + /// + /// Communicates with the profiler, using a Unix Domain Socket. + /// + internal sealed class ProfilerChannel + { + private IOptionsMonitor _storageOptions; + + public ProfilerChannel(IOptionsMonitor storageOptions) + { + _storageOptions = storageOptions; + } + + public async Task SendMessage(IEndpointInfo endpointInfo, ProfilerMessage message, CancellationToken token) + { +#if NET6_0_OR_GREATER + string channelPath = ComputeChannelPath(endpointInfo); + var endpoint = new UnixDomainSocketEndPoint(channelPath); + using var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + + //Note that this is still getting built for 3.1 due to test app dependencies. + + await socket.ConnectAsync(endpoint); + + byte[] buffer = new byte[sizeof(short) + sizeof(int)]; + var memoryStream = new MemoryStream(buffer); + using BinaryWriter writer = new BinaryWriter(memoryStream); + writer.Write((short)message.MessageType); + writer.Write(message.Parameter); + writer.Dispose(); + + await socket.SendAsync(new ReadOnlyMemory(buffer), SocketFlags.None, token); + int received = await socket.ReceiveAsync(new Memory(buffer), SocketFlags.None, token); + if (received < buffer.Length) + { + //TODO Figure out if fragmentation is possible over UDS. + throw new InvalidOperationException("Could not receive message from server."); + } + + return new ProfilerMessage + { + MessageType = (ProfilerMessageType)BitConverter.ToInt16(buffer, startIndex: 0), + Parameter = BitConverter.ToInt32(buffer, startIndex: 2) + }; +#else + return await Task.FromException(new NotImplementedException()); +#endif + } + + private string ComputeChannelPath(IEndpointInfo endpointInfo) + { + string defaultSharedPath = _storageOptions.CurrentValue.DefaultSharedPath; + if (string.IsNullOrEmpty(_storageOptions.CurrentValue.DefaultSharedPath)) + { + //Note this fallback does not work well for sidecar scenarios. + defaultSharedPath = Path.GetTempPath(); + } + return Path.Combine(defaultSharedPath, FormattableString.Invariant($"{endpointInfo.RuntimeInstanceCookie:D}.sock")); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs index 880b4866e45..a6347efed62 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitAttribute.cs @@ -3,9 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs index 8b715c70dbe..97a17368417 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/RequestThrottling/RequestLimitTracker.cs @@ -2,9 +2,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.WebApi { @@ -33,6 +31,7 @@ public RequestLimitTracker(ILogger logger) _requestLimitTable.Add(Utilities.ArtifactType_Logs, 3); _requestLimitTable.Add(Utilities.ArtifactType_Trace, 3); _requestLimitTable.Add(Utilities.ArtifactType_Metrics, 3); + _requestLimitTable.Add(Utilities.ArtifactType_Stacks, 1); _logger = logger; } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/ScopeState.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/ScopeState.cs index c8e6ff1916b..b0d7c0369cb 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/ScopeState.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/ScopeState.cs @@ -5,7 +5,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Text; namespace Microsoft.Diagnostics.Monitoring.WebApi { diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/CallStackData.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/CallStackData.cs new file mode 100644 index 00000000000..d52777152f7 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/CallStackData.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Stacks +{ + /// + /// This data model is mostly 1:1 with the information that comes from the EventStacksPipeline. + /// Note that most data is either ClassID's or mdToken information. + /// + internal sealed class CallStackResult + { + public List Stacks { get; } = new(); + + public NameCache NameCache { get; } = new NameCache(); + } + + internal sealed class NameCache + { + public Dictionary ClassData { get; } = new(); + public Dictionary FunctionData { get; } = new(); + public Dictionary ModuleData { get; } = new(); + public Dictionary<(ulong moduleId, ulong typeDef), TokenData> TokenData { get; } = new(); + } + + internal enum ClassFlags : uint + { + None = 0, + Array, + Composite, + IncompleteData, + Error = 0xff + } + + internal sealed class ClassData + { + public ulong[] TypeArgs { get; set; } + + // We do not store the name of the class directly. The name can be retrieved from the TokenData. + public uint Token { get; set; } + + public ulong ModuleId { get; set; } + + public ClassFlags Flags { get; set; } + } + + internal sealed class TokenData + { + public uint OuterToken { get; set; } + + public string Name { get; set; } + } + + internal sealed class FunctionData + { + public string Name { get; set; } + + /// + /// ClassID of the containing class for this function. Note it's possible that the ClassID could not be retrieved by the profiler. + /// In this case, only the token will be available. + /// + public ulong ParentClass { get; set; } + + /// + /// If the ClassID could not be retrieved, the token can be used to get the name. + /// + public uint ParentToken { get; set; } + + public ulong[] TypeArgs { get; set; } + + public ulong ModuleId { get; set; } + } + + internal sealed class ModuleData + { + public string Name { get; set; } + } + + internal sealed class CallStackFrame + { + public ulong FunctionId { get; set; } + + public ulong Offset { get; set; } + } + + internal sealed class CallStack + { + public List Frames = new List(); + + public uint ThreadId { get; set; } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/CallStackEvents.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/CallStackEvents.cs new file mode 100644 index 00000000000..437da8132ad --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/CallStackEvents.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Tracing; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Stacks +{ + internal static class CallStackEvents + { + public const string Provider = "DotnetMonitorStacksEventProvider"; + + public const TraceEventID Callstack = (TraceEventID)1; + public const TraceEventID FunctionDesc = (TraceEventID)2; + public const TraceEventID ClassDesc = (TraceEventID)3; + public const TraceEventID ModuleDesc = (TraceEventID)4; + public const TraceEventID TokenDesc = (TraceEventID)5; + public const TraceEventID End = (TraceEventID)6; + + public static class CallstackPayloads + { + public const int ThreadId = 0; + public const int FunctionIds = 1; + public const int IpOffsets = 2; + } + + public static class FunctionDescPayloads + { + public const int FunctionId = 0; + public const int ClassId = 1; + public const int ClassToken = 2; + public const int ModuleId = 3; + public const int Name = 4; + public const int TypeArgs = 5; + } + + public static class ClassDescPayloads + { + public const int ClassId = 0; + public const int ModuleId = 1; + public const int Token = 2; + public const int Flags = 3; + public const int TypeArgs = 4; + } + + public static class ModuleDescPayloads + { + public const int ModuleId = 0; + public const int Name = 1; + } + public static class TokenDescPayloads + { + public const int ModuleId = 0; + public const int Token = 1; + public const int OuterToken = 2; + public const int Name = 3; + } + + public static class EndPayloads + { + public const int Unused = 0; + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs new file mode 100644 index 00000000000..b7aea5d54f2 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/EventStacksPipeline.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.EventPipe; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Diagnostics.Tracing; +using System; +using System.Diagnostics.Tracing; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Stacks +{ + internal sealed class EventStacksPipelineSettings : EventSourcePipelineSettings + { + public EventStacksPipelineSettings() + { + Duration = System.Threading.Timeout.InfiniteTimeSpan; + } + + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(5); + } + + internal sealed class EventStacksPipeline : EventSourcePipeline + { + private TaskCompletionSource _stackResult = new(TaskCreationOptions.RunContinuationsAsynchronously); + private CallStackResult _result = new(); + + public EventStacksPipeline(DiagnosticsClient client, EventStacksPipelineSettings settings) + : base(client, settings) + { + } + + protected override MonitoringSourceConfiguration CreateConfiguration() + { + return new EventPipeProviderSourceConfiguration(requestRundown: false, bufferSizeInMB: 256, new[] + { + new EventPipeProvider(CallStackEvents.Provider, EventLevel.LogAlways) + }); + } + + protected override async Task OnEventSourceAvailable(EventPipeEventSource eventSource, Func stopSessionAsync, CancellationToken token) + { + eventSource.Dynamic.AddCallbackForProviderEvent(CallStackEvents.Provider, eventName: null, Callback); + + using EventTaskSource sourceComplete = new EventTaskSource( + taskComplete => taskComplete, + addHandler => eventSource.Completed += addHandler, + removeHandler => eventSource.Completed -= removeHandler, + token); + + // This is the same issue as GCDumps. We don't always get events back in realtime, so we have to stop the session and then process the events. + Task eventsTimeoutTask = Task.Delay(Settings.Timeout, token); + Task completedTask = await Task.WhenAny(_stackResult.Task, eventsTimeoutTask); + + await completedTask; + + await stopSessionAsync(); + await sourceComplete.Task; + + if (_stackResult.Task.Status != TaskStatus.RanToCompletion) + { + throw new InvalidOperationException(Strings.ErrorMessage_StacksTimeout); + } + } + + public Task Result => _stackResult.Task; + + private void Callback(TraceEvent action) + { + //We do not have a manifest for our events, but we also lookup data by id instead of string. + if (action.ID == CallStackEvents.Callstack) + { + var stack = new CallStack + { + ThreadId = action.GetPayload(CallStackEvents.CallstackPayloads.ThreadId) + }; + ulong[] functionIds = action.GetPayload(CallStackEvents.CallstackPayloads.FunctionIds); + ulong[] offsets = action.GetPayload(CallStackEvents.CallstackPayloads.IpOffsets); + + _result.Stacks.Add(stack); + + if (functionIds != null && offsets != null && functionIds.Length == offsets.Length) + { + for (int i = 0; i < functionIds.Length; i++) + { + stack.Frames.Add(new CallStackFrame { FunctionId = functionIds[i], Offset = offsets[i] }); + } + } + } + else if (action.ID == CallStackEvents.FunctionDesc) + { + ulong id = action.GetPayload(CallStackEvents.FunctionDescPayloads.FunctionId); + var functionData = new FunctionData + { + Name = action.GetPayload(CallStackEvents.FunctionDescPayloads.Name), + ParentClass = action.GetPayload(CallStackEvents.FunctionDescPayloads.ClassId), + ParentToken = action.GetPayload(CallStackEvents.FunctionDescPayloads.ClassToken), + ModuleId = action.GetPayload(CallStackEvents.FunctionDescPayloads.ModuleId), + TypeArgs = action.GetPayload(CallStackEvents.FunctionDescPayloads.TypeArgs) ?? Array.Empty() + }; + + _result.NameCache.FunctionData.Add(id, functionData); + } + else if (action.ID == CallStackEvents.ClassDesc) + { + ulong id = action.GetPayload(CallStackEvents.ClassDescPayloads.ClassId); + var classData = new ClassData + { + ModuleId = action.GetPayload(CallStackEvents.ClassDescPayloads.ModuleId), + Token = action.GetPayload(CallStackEvents.ClassDescPayloads.Token), + Flags = (ClassFlags)action.GetPayload(CallStackEvents.ClassDescPayloads.Flags), + TypeArgs = action.GetPayload(CallStackEvents.ClassDescPayloads.TypeArgs) ?? Array.Empty() + }; + + _result.NameCache.ClassData.Add(id, classData); + } + else if (action.ID == CallStackEvents.ModuleDesc) + { + ulong id = action.GetPayload(CallStackEvents.ModuleDescPayloads.ModuleId); + var moduleData = new ModuleData + { + Name = action.GetPayload(CallStackEvents.ModuleDescPayloads.Name) + }; + + _result.NameCache.ModuleData.Add(id, moduleData); + } + else if (action.ID == CallStackEvents.TokenDesc) + { + ulong modId = action.GetPayload(CallStackEvents.TokenDescPayloads.ModuleId); + ulong token = action.GetPayload(CallStackEvents.TokenDescPayloads.Token); + var tokenData = new TokenData() + { + Name = action.GetPayload(CallStackEvents.TokenDescPayloads.Name), + OuterToken = action.GetPayload(CallStackEvents.TokenDescPayloads.OuterToken) + }; + + _result.NameCache.TokenData.Add((modId, token), tokenData); + } + else if (action.ID == CallStackEvents.End) + { + //TODO Consider using opcodes instead of a separate event for stopping + _stackResult.TrySetResult(_result); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs new file mode 100644 index 00000000000..aa28382d09d --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/JsonStacksFormatter.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Stacks +{ + internal sealed class JsonStacksFormatter : StacksFormatter + { + public JsonStacksFormatter(Stream outputStream) : base(outputStream) + { + } + + public override async Task FormatStack(CallStackResult stackResult, CancellationToken token) + { + Models.CallStackResult stackResultModel = new Models.CallStackResult(); + NameCache cache = stackResult.NameCache; + var builder = new StringBuilder(); + + foreach (CallStack stack in stackResult.Stacks) + { + Models.CallStack stackModel = new Models.CallStack(); + stackModel.ThreadId = stack.ThreadId; + + foreach (CallStackFrame frame in stack.Frames) + { + Models.CallStackFrame frameModel = new Models.CallStackFrame() + { + ClassName = UnknownClass, + MethodName = UnknownFunction, + //TODO Bring this back once we have a useful offset value + //Offset = frame.Offset, + ModuleName = UnknownModule + }; + if (frame.FunctionId == 0) + { + frameModel.MethodName = NativeFrame; + frameModel.ModuleName = NativeFrame; + frameModel.ClassName = NativeFrame; + } + else if (cache.FunctionData.TryGetValue(frame.FunctionId, out FunctionData functionData)) + { + frameModel.ModuleName = GetModuleName(cache, functionData.ModuleId); + frameModel.MethodName = functionData.Name; + + builder.Clear(); + BuildClassName(builder, cache, functionData); + frameModel.ClassName = builder.ToString(); + + if (functionData.TypeArgs.Length > 0) + { + builder.Clear(); + builder.Append(functionData.Name); + BuildGenericParameters(builder, cache, functionData.TypeArgs); + frameModel.MethodName = builder.ToString(); + } + } + + stackModel.Frames.Add(frameModel); + } + stackResultModel.Stacks.Add(stackModel); + } + + await JsonSerializer.SerializeAsync(OutputStream, stackResultModel, cancellationToken: token); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/StacksFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/StacksFormatter.cs new file mode 100644 index 00000000000..58293b3f9e6 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/StacksFormatter.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Stacks +{ + internal abstract class StacksFormatter + { + protected const string UnknownModule = "UnknownModule"; + protected const string UnknownClass = "UnknownClass"; + protected const string UnknownFunction = "UnknownFunction"; + + protected const string ArrayType = "_ArrayType_"; + protected const string CompositeType = "_CompositeType_"; + protected const string NativeFrame = "[NativeFrame]"; + + protected const char NestedSeparator = '+'; + protected const char GenericStart = '['; + protected const char GenericSeparator = ','; + protected const char GenericEnd = ']'; + + protected Stream OutputStream { get; } + + public StacksFormatter(Stream outputStream) + { + OutputStream = outputStream; + } + + public abstract Task FormatStack(CallStackResult stackResult, CancellationToken token); + + protected string GetModuleName(NameCache cache, ulong moduleId) + { + string moduleName = UnknownModule; + if (cache.ModuleData.TryGetValue(moduleId, out ModuleData moduleData)) + { + moduleName = moduleData.Name; + } + return moduleName; + } + + protected void BuildClassName(StringBuilder builder, NameCache cache, FunctionData functionData) + { + if (functionData.ParentClass != 0) + { + BuildClassName(builder, cache, functionData.ParentClass); + } + else + { + BuildClassName(builder, cache, functionData.ModuleId, functionData.ParentToken); + } + } + + private void BuildClassName(StringBuilder builder, NameCache cache, ulong classId) + { + string className = UnknownClass; + if (cache.ClassData.TryGetValue(classId, out ClassData classData)) + { + if (classData.Flags != ClassFlags.None) + { + switch (classData.Flags) + { + case ClassFlags.Array: + className = ArrayType; + break; + case ClassFlags.Composite: + className = CompositeType; + break; + default: + //All other cases default to UnknownClass + break; + } + + builder.Append(className); + } + else + { + BuildClassName(builder, cache, classData.ModuleId, classData.Token); + } + BuildGenericParameters(builder, cache, classData.TypeArgs); + } + else + { + builder.Append(className); + } + } + + private void BuildClassName(StringBuilder builder, NameCache cache, ulong moduleId, uint token) + { + var classNames = new Stack(); + + uint currentToken = token; + while (currentToken != 0 && cache.TokenData.TryGetValue((moduleId, currentToken), out TokenData tokenData)) + { + classNames.Push(tokenData.Name); + currentToken = tokenData.OuterToken; + } + + if (classNames.Count == 0) + { + builder.Append(UnknownClass); + } + + while (classNames.Count > 0) + { + string className = classNames.Pop(); + builder.Append(className); + if (classNames.Count > 0) + { + builder.Append(NestedSeparator); + } + } + } + + protected void BuildGenericParameters(StringBuilder builder, NameCache cache, ulong[] parameters) + { + for (int i = 0; i < parameters?.Length; i++) + { + if (i == 0) + { + builder.Append(GenericStart); + } + BuildClassName(builder, cache, parameters[i]); + if (i < parameters.Length - 1) + { + builder.Append(GenericSeparator); + } + else if (i == parameters.Length - 1) + { + builder.Append(GenericEnd); + } + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs new file mode 100644 index 00000000000..b5bcb144cea --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Stacks/TextStacksFormatter.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.Stacks +{ + internal sealed class TextStacksFormatter : StacksFormatter + { + private const char ModuleSeparator = '!'; + private const char ClassSeparator = '.'; + private const string Indent = " "; + + public TextStacksFormatter(Stream outputStream) : base(outputStream) + { + } + + public override async Task FormatStack(CallStackResult stackResult, CancellationToken token) + { + await using StreamWriter writer = new StreamWriter(this.OutputStream, Encoding.UTF8, leaveOpen: true); + var builder = new StringBuilder(); + foreach (var stack in stackResult.Stacks) + { + token.ThrowIfCancellationRequested(); + + await writer.WriteLineAsync(string.Format(CultureInfo.CurrentCulture, Strings.CallstackThreadHeader, stack.ThreadId)); + foreach (var frame in stack.Frames) + { + builder.Clear(); + builder.Append(Indent); + BuildFrame(builder, stackResult.NameCache, frame); + await writer.WriteLineAsync(builder, token); + } + await writer.WriteLineAsync(); + } + } + + private void BuildFrame(StringBuilder builder, NameCache cache, CallStackFrame frame) + { + if (frame.FunctionId == 0) + { + builder.Append(NativeFrame); + } + else if (cache.FunctionData.TryGetValue(frame.FunctionId, out FunctionData functionData)) + { + builder.Append(base.GetModuleName(cache, functionData.ModuleId)); + builder.Append(ModuleSeparator); + BuildClassName(builder, cache, functionData); + builder.Append(ClassSeparator); + builder.Append(functionData.Name); + BuildGenericParameters(builder, cache, functionData.TypeArgs); + } + else + { + builder.Append(UnknownFunction); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs new file mode 100644 index 00000000000..635d87bbf2d --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamLeaveOpenWrapper.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + /// + /// Wraps a given stream but leaves it open on Dispose. + /// + public sealed class StreamLeaveOpenWrapper : Stream + { + private readonly Stream _baseStream; + + public StreamLeaveOpenWrapper(Stream baseStream) : base() + { + _baseStream = baseStream; + } + + public override bool CanSeek => _baseStream.CanSeek; + + public override bool CanTimeout => _baseStream.CanTimeout; + + public override bool CanRead => _baseStream.CanRead; + + public override bool CanWrite => _baseStream.CanWrite; + + public override long Length => _baseStream.Length; + + public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } + + public override int ReadTimeout { get => _baseStream.ReadTimeout; set => _baseStream.ReadTimeout = value; } + + public override int WriteTimeout { get => _baseStream.WriteTimeout; set => _baseStream.WriteTimeout = value; } + + public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin); + + public override int Read(Span buffer) => _baseStream.Read(buffer); + + public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count); + + public override int ReadByte() => _baseStream.ReadByte(); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _baseStream.ReadAsync(buffer, offset, count, cancellationToken); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => _baseStream.ReadAsync(buffer, cancellationToken); + + public override void Flush() => _baseStream.Flush(); + + public override void SetLength(long value) => _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); + + public override void Write(ReadOnlySpan buffer) => _baseStream.Write(buffer); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _baseStream.WriteAsync(buffer, offset, count, cancellationToken); + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => _baseStream.WriteAsync(buffer, cancellationToken); + + public override void WriteByte(byte value) => _baseStream.WriteByte(value); + + public override Task FlushAsync(CancellationToken cancellationToken) => _baseStream.FlushAsync(cancellationToken); + + public override void CopyTo(Stream destination, int bufferSize) => _baseStream.CopyTo(destination, bufferSize); + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _baseStream.CopyToAsync(destination, bufferSize, cancellationToken); + + public override async ValueTask DisposeAsync() => await base.DisposeAsync(); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamingLogger.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamingLogger.cs index d19d716324b..a6129d2d3d1 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamingLogger.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/StreamingLogger.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; using System.Text.Json; namespace Microsoft.Diagnostics.Monitoring.WebApi @@ -175,7 +174,7 @@ private void LogText(LogLevel logLevel, EventId eventId, TState state, E // Note: This deviates slightly from the simple console format in that the event name // is also logged as a suffix on the first line whereas the simple console format does // not log the event name at all. - + // Timestamp Level: Category[EventId][EventName] // => Scope1Name1:Scope1Value1, Scope1Name2:Scope1Value2 => Scope2Name1:Scope2Value2 // Message diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs index 250fd8afa02..c2344e16bea 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.Designer.cs @@ -60,6 +60,15 @@ internal Strings() { } } + /// + /// Looks up a localized string similar to Thread: (0x{0:X}). + /// + internal static string CallstackThreadHeader { + get { + return ResourceManager.GetString("CallstackThreadHeader", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unable to get process environment.. /// @@ -159,6 +168,15 @@ internal static string ErrorMessage_ProcessEnumeratuinFailed { } } + /// + /// Looks up a localized string similar to Unable to process stack in timely manner.. + /// + internal static string ErrorMessage_StacksTimeout { + get { + return ResourceManager.GetString("ErrorMessage_StacksTimeout", resourceCulture); + } + } + /// /// Looks up a localized string similar to Rate limit reached.. /// @@ -222,6 +240,15 @@ internal static string ErrorMessage_ValueNotString { } } + /// + /// Looks up a localized string similar to Failed to determine the default process.. + /// + internal static string LogFormatString_DefaultProcessUnexpectedFailure { + get { + return ResourceManager.GetString("LogFormatString_DefaultProcessUnexpectedFailure", resourceCulture); + } + } + /// /// Looks up a localized string similar to Egressed artifact to {location}. /// @@ -258,6 +285,24 @@ internal static string LogFormatString_ResolvedTargetProcess { } } + /// + /// Looks up a localized string similar to Hit stopping trace event '{providerName}/{eventName}'. + /// + internal static string LogFormatString_StoppingTraceEventHit { + get { + return ResourceManager.GetString("LogFormatString_StoppingTraceEventHit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One or more field names specified in the payload filter for event '{providerName}/{eventName}' do not match any of the known field names: '{payloadFieldNames}'. As a result the requested stopping event is unreachable; will continue to collect the trace for the remaining specified duration.. + /// + internal static string LogFormatString_StoppingTraceEventPayloadFilterMismatch { + get { + return ResourceManager.GetString("LogFormatString_StoppingTraceEventPayloadFilterMismatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Request limit for endpoint reached. Limit: {limit}, oustanding requests: {requests}. /// @@ -275,5 +320,68 @@ internal static string LogFormatString_WrittenToHttpStream { return ResourceManager.GetString("LogFormatString_WrittenToHttpStream", resourceCulture); } } + + /// + /// Looks up a localized string similar to This collection rule has had its triggering conditions satisfied and is currently executing its action list.. + /// + internal static string Message_CollectionRuleStateReason_ExecutingActions { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_ExecutingActions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The collection rule will no longer trigger because the ActionCount was reached.. + /// + internal static string Message_CollectionRuleStateReason_Finished_ActionCount { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_Finished_ActionCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The collection rule will no longer trigger because a failure occurred with message: {0}.. + /// + internal static string Message_CollectionRuleStateReason_Finished_Failure { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_Finished_Failure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The collection rule will no longer trigger because the RuleDuration limit was reached.. + /// + internal static string Message_CollectionRuleStateReason_Finished_RuleDuration { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_Finished_RuleDuration", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The collection rule will no longer trigger because the Startup trigger only executes once.. + /// + internal static string Message_CollectionRuleStateReason_Finished_Startup { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_Finished_Startup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This collection rule is active and waiting for its triggering conditions to be satisfied.. + /// + internal static string Message_CollectionRuleStateReason_Running { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_Running", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This collection rule is temporarily throttled because the ActionCountLimit has been reached within the ActionCountSlidingWindowDuration.. + /// + internal static string Message_CollectionRuleStateReason_Throttled { + get { + return ResourceManager.GetString("Message_CollectionRuleStateReason_Throttled", resourceCulture); + } + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx index 5391d98f4b7..dd4f32fda9a 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Strings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Thread: (0x{0:X}) + Unable to get process environment. Gets a string similar to "Unable to get process environment.". @@ -158,6 +161,9 @@ Unable to enumerate processes. Gets a string similar to "Unable to enumerate processes.". + + Unable to process stack in timely manner. + Rate limit reached. @@ -194,6 +200,11 @@ Value must be of type string. Gets a string similar to "Value must be of type string.". + + Failed to determine the default process. + Gets the format string that is printed in the 7:DefaultProcessUnexpectedFailure event. +0 Format Parameters + Egressed artifact to {location} Gets the format string that is printed in the 4:EgressedArtifact event. @@ -214,6 +225,21 @@ Resolved target process. Gets the format string that is printed in the 3:ResolvedTargetProcess event. 0 Format Parameters + + + Hit stopping trace event '{providerName}/{eventName}' + Gets the format string that is printed in the 8:StoppingTraceEventHit event. +2 Format Parameter: +1. providerName: The stopping event provider name. +2. eventName: The stopping event name. + + + One or more field names specified in the payload filter for event '{providerName}/{eventName}' do not match any of the known field names: '{payloadFieldNames}'. As a result the requested stopping event is unreachable; will continue to collect the trace for the remaining specified duration. + Gets the format string that is printed in the 9:StoppingTraceEventPayloadFilterMismatch. +3 Format Parameter: +1. providerName: The stopping event provider name. +2. eventName: The stopping event name. +3. payloadFieldNames: The available payload field names. Request limit for endpoint reached. Limit: {limit}, oustanding requests: {requests} @@ -227,4 +253,25 @@ Gets the format string that is printed in the 5:WrittenToHttpStream event. 0 Format Parameters + + This collection rule has had its triggering conditions satisfied and is currently executing its action list. + + + The collection rule will no longer trigger because the ActionCount was reached. + + + The collection rule will no longer trigger because a failure occurred with message: {0}. + + + The collection rule will no longer trigger because the RuleDuration limit was reached. + + + The collection rule will no longer trigger because the Startup trigger only executes once. + + + This collection rule is active and waiting for its triggering conditions to be satisfied. + + + This collection rule is temporarily throttled because the ActionCountLimit has been reached within the ActionCountSlidingWindowDuration. + \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/GCDumpUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/GCDumpUtilities.cs index 9a3d6d39c0e..da16fdd2a7d 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/GCDumpUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/GCDumpUtilities.cs @@ -80,4 +80,3 @@ private static async Task WriteToStream(IFastSerializable serializable, Stream t } } } - \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/MetricsUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/MetricsUtilities.cs new file mode 100644 index 00000000000..0b9afbc38ec --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/MetricsUtilities.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.EventPipe; +using Microsoft.Diagnostics.NETCore.Client; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class MetricsUtilities + { + public static async Task CaptureLiveMetricsAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, EventPipeCounterPipelineSettings settings, Stream outputStream, CancellationToken token) + { + var client = new DiagnosticsClient(endpointInfo.Endpoint); + + await using EventCounterPipeline eventCounterPipeline = new EventCounterPipeline(client, + settings, + loggers: + new[] { new JsonCounterLogger(outputStream) }); + + Task runTask = await eventCounterPipeline.StartAsync(token); + + startCompletionSource?.TrySetResult(null); + + await runTask; + } + + public static string GetMetricFilename(IEndpointInfo endpointInfo) => + FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{endpointInfo.ProcessId}.metrics.json"); + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs new file mode 100644 index 00000000000..0ffa0c33287 --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/StackUtilities.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.WebApi.Stacks; +using Microsoft.Diagnostics.NETCore.Client; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class StackUtilities + { + public static string GenerateStacksFilename(IEndpointInfo endpointInfo, bool plainText) + { + string extension = plainText ? "txt" : "json"; + return FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{endpointInfo.ProcessId}.stacks.{extension}"); + } + + public static async Task CollectStacksAsync(TaskCompletionSource startCompletionSource, + IEndpointInfo endpointInfo, + ProfilerChannel profilerChannel, + bool plainText, + Stream outputStream, CancellationToken token) + { + var settings = new EventStacksPipelineSettings + { + Duration = Timeout.InfiniteTimeSpan + }; + await using var eventTracePipeline = new EventStacksPipeline(new DiagnosticsClient(endpointInfo.Endpoint), settings); + + Task runPipelineTask = await eventTracePipeline.StartAsync(token); + + //CONSIDER Should we set this before or after the profiler message has been sent. + startCompletionSource?.TrySetResult(null); + + ProfilerMessage response = await profilerChannel.SendMessage( + endpointInfo, + new ProfilerMessage { MessageType = ProfilerMessageType.Callstack, Parameter = 0 }, + token); + + if (response.MessageType == ProfilerMessageType.Error) + { + throw new InvalidOperationException($"Profiler request failed: 0x{response.Parameter:X8}"); + } + await runPipelineTask; + Stacks.CallStackResult result = await eventTracePipeline.Result; + + StacksFormatter formatter = (plainText == true) ? new TextStacksFormatter(outputStream) : new JsonStacksFormatter(outputStream); + + await formatter.FormatStack(result, token); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs new file mode 100644 index 00000000000..cf37861ee6b --- /dev/null +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceEventExtensions.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Tracing; + +namespace Microsoft.Diagnostics.Monitoring.WebApi +{ + internal static class TraceEventExtensions + { + public static T GetPayload(this TraceEvent traceEvent, int index) + { + return (T)traceEvent.PayloadValue(index); + } + } +} diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs index 0a60cf5948a..050b6856186 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/TraceUtilities.cs @@ -17,6 +17,9 @@ namespace Microsoft.Diagnostics.Monitoring.WebApi { internal static class TraceUtilities { + // Buffer size matches FileStreamResult + private const int DefaultBufferSize = 0x10000; + public static string GenerateTraceFileName(IEndpointInfo endpointInfo) { return FormattableString.Invariant($"{Utilities.GetFileNameTimeStampUtcNow()}_{endpointInfo.ProcessId}.nettrace"); @@ -82,9 +85,8 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom { startCompletionSource.TrySetResult(null); } - //Buffer size matches FileStreamResult //CONSIDER Should we allow client to change the buffer size? - await eventStream.CopyToAsync(outputStream, 0x10000, token); + await eventStream.CopyToAsync(outputStream, DefaultBufferSize, token); }; var client = new DiagnosticsClient(endpointInfo.Endpoint); @@ -97,5 +99,50 @@ public static async Task CaptureTraceAsync(TaskCompletionSource startCom await pipeProcessor.RunAsync(token); } + + public static async Task CaptureTraceUntilEventAsync(TaskCompletionSource startCompletionSource, IEndpointInfo endpointInfo, MonitoringSourceConfiguration configuration, TimeSpan timeout, Stream outputStream, string providerName, string eventName, IDictionary payloadFilter, ILogger logger, CancellationToken token) + { + DiagnosticsClient client = new(endpointInfo.Endpoint); + TaskCompletionSource stoppingEventHitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + using IDisposable registration = token.Register( + () => stoppingEventHitSource.TrySetCanceled(token)); + + await using EventTracePipeline pipeProcessor = new(client, new EventTracePipelineSettings + { + Configuration = configuration, + Duration = timeout, + }, + async (eventStream, token) => + { + startCompletionSource?.TrySetResult(null); + await using EventMonitoringPassthroughStream eventMonitoringStream = new( + providerName, + eventName, + payloadFilter, + onEvent: (traceEvent) => + { + logger.StoppingTraceEventHit(traceEvent); + stoppingEventHitSource.TrySetResult(null); + }, + onPayloadFilterMismatch: logger.StoppingTraceEventPayloadFilterMismatch, + eventStream, + outputStream, + DefaultBufferSize, + callOnEventOnlyOnce: true, + leaveDestinationStreamOpen: true /* We do not have ownership of the outputStream */); + + await eventMonitoringStream.ProcessAsync(token); + }); + + Task pipelineRunTask = pipeProcessor.RunAsync(token); + await Task.WhenAny(pipelineRunTask, stoppingEventHitSource.Task).Unwrap(); + + if (stoppingEventHitSource.Task.IsCompleted) + { + await pipeProcessor.StopAsync(token); + await pipelineRunTask; + } + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/Utilities.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/Utilities.cs index 3ea8e92c199..87bc81d1021 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/Utilities.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Utilities/Utilities.cs @@ -14,6 +14,7 @@ internal static class Utilities public const string ArtifactType_Logs = "logs"; public const string ArtifactType_Trace = "trace"; public const string ArtifactType_Metrics = "livemetrics"; + public const string ArtifactType_Stacks = "stacks"; public static TimeSpan ConvertSecondsToTimeSpan(int durationSeconds) { @@ -34,5 +35,10 @@ public static KeyValueLogScope CreateArtifactScope(string artifactType, IEndpoin scope.AddArtifactEndpointInfo(endpointInfo); return scope; } + + public static ProcessKey? GetProcessKey(int? pid, Guid? uid, string name) + { + return (!pid.HasValue && !uid.HasValue && string.IsNullOrEmpty(name)) ? null : new ProcessKey(pid, uid, name); + } } } diff --git a/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs b/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs index 8d3ad5373a9..08974309e59 100644 --- a/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs +++ b/src/Microsoft.Diagnostics.Monitoring.WebApi/Validation/CounterValidator.cs @@ -3,9 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.WebApi.Models; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; using System.Globalization; namespace Microsoft.Diagnostics.Monitoring.WebApi.Validation diff --git a/src/MonitorProfiler/CMakeLists.txt b/src/MonitorProfiler/CMakeLists.txt index 06d43be228a..175a48c1db3 100644 --- a/src/MonitorProfiler/CMakeLists.txt +++ b/src/MonitorProfiler/CMakeLists.txt @@ -16,8 +16,20 @@ set(SOURCES ${PROFILER_SOURCES} Environment/EnvironmentHelper.cpp Environment/ProfilerEnvironment.cpp + EventProvider/ProfilerEventProvider.cpp Logging/AggregateLogger.cpp + Logging/Logger.cpp + Logging/LoggerHelper.cpp + Logging/NullLogger.cpp + Logging/StdErrLogger.cpp + MainProfiler/ExceptionTracker.cpp MainProfiler/MainProfiler.cpp + MainProfiler/ThreadData.cpp + MainProfiler/ThreadDataManager.cpp + Stacks/StacksEventProvider.cpp + Stacks/StackSampler.cpp + Utilities/NameCache.cpp + Utilities/TypeNameUtilities.cpp ClassFactory.cpp DllMain.cpp ProfilerBase.cpp @@ -26,6 +38,9 @@ set(SOURCES Communication/CommandServer.cpp ) +# Include exceptions tracking feature +#add_compile_options(-DDOTNETMONITOR_FEATURE_EXCEPTIONS) + # Build library and split symbols add_library_clr(MonitorProfiler SHARED ${SOURCES}) diff --git a/src/MonitorProfiler/Communication/CommandServer.cpp b/src/MonitorProfiler/Communication/CommandServer.cpp index f307b529945..2b1a5a96869 100644 --- a/src/MonitorProfiler/Communication/CommandServer.cpp +++ b/src/MonitorProfiler/Communication/CommandServer.cpp @@ -6,7 +6,11 @@ #include #include "../Logging/Logger.h" -CommandServer::CommandServer(const std::shared_ptr& logger) : _shutdown(false), _logger(logger) +CommandServer::CommandServer(const std::shared_ptr& logger, ICorProfilerInfo12* profilerInfo) : + _shutdown(false), + _server(logger), + _logger(logger), + _profilerInfo(profilerInfo) { } @@ -65,7 +69,7 @@ void CommandServer::ListeningThread() hr = client->Receive(message); if (FAILED(hr)) { - _logger->Log(LogLevel::Error, _T("Unexpected error when receiving data: 0x%08x"), hr); + _logger->Log(LogLevel::Error, _LS("Unexpected error when receiving data: 0x%08x"), hr); continue; } @@ -76,13 +80,13 @@ void CommandServer::ListeningThread() hr = client->Send(response); if (FAILED(hr)) { - _logger->Log(LogLevel::Error, _T("Unexpected error when sending data: 0x%08x"), hr); + _logger->Log(LogLevel::Error, _LS("Unexpected error when sending data: 0x%08x"), hr); continue; } hr = client->Shutdown(); if (FAILED(hr)) { - _logger->Log(LogLevel::Warning, _T("Unexpected error during shutdown: 0x%08x"), hr); + _logger->Log(LogLevel::Warning, _LS("Unexpected error during shutdown: 0x%08x"), hr); } _clientQueue.Enqueue(message); @@ -91,10 +95,18 @@ void CommandServer::ListeningThread() void CommandServer::ClientProcessingThread() { + HRESULT hr = _profilerInfo->InitializeCurrentThread(); + + if (FAILED(hr)) + { + _logger->Log(LogLevel::Error, _LS("Unable to initialize thread: 0x%08x"), hr); + return; + } + while (true) { IpcMessage message; - HRESULT hr = _clientQueue.BlockingDequeue(message); + hr = _clientQueue.BlockingDequeue(message); if (hr != S_OK) { //We are complete, discard all messages @@ -103,7 +115,7 @@ void CommandServer::ClientProcessingThread() hr = _callback(message); if (hr != S_OK) { - _logger->Log(LogLevel::Warning, _T("IpcMessage callback failed: 0x%08x"), hr); + _logger->Log(LogLevel::Warning, _LS("IpcMessage callback failed: 0x%08x"), hr); } } -} \ No newline at end of file +} diff --git a/src/MonitorProfiler/Communication/CommandServer.h b/src/MonitorProfiler/Communication/CommandServer.h index 323929b3dea..39b32eb7a72 100644 --- a/src/MonitorProfiler/Communication/CommandServer.h +++ b/src/MonitorProfiler/Communication/CommandServer.h @@ -6,6 +6,9 @@ #include "IpcCommServer.h" #include "Messages.h" +#include "cor.h" +#include "corprof.h" +#include "com.h" #include #include #include @@ -16,7 +19,7 @@ class CommandServer final { public: - CommandServer(const std::shared_ptr& logger); + CommandServer(const std::shared_ptr& logger, ICorProfilerInfo12* profilerInfo); HRESULT Start(const std::string& path, std::function callback); void Shutdown(); @@ -34,4 +37,6 @@ class CommandServer final std::thread _listeningThread; std::thread _clientThread; + + ComPtr _profilerInfo; }; \ No newline at end of file diff --git a/src/MonitorProfiler/Communication/IpcCommServer.cpp b/src/MonitorProfiler/Communication/IpcCommServer.cpp index 6ba9c414cc2..1e4d871fc93 100644 --- a/src/MonitorProfiler/Communication/IpcCommServer.cpp +++ b/src/MonitorProfiler/Communication/IpcCommServer.cpp @@ -5,8 +5,9 @@ #include "IpcCommClient.h" #include "IpcCommServer.h" #include "../Logging/Logger.h" +#include "../Utilities/StringUtilities.h" -IpcCommServer::IpcCommServer() : _shutdown(false) +IpcCommServer::IpcCommServer(const std::shared_ptr& logger) : _shutdown(false), _logger(logger) { } @@ -29,8 +30,7 @@ HRESULT IpcCommServer::Bind(const std::string& rootAddress) return E_UNEXPECTED; } - _rootAddress = rootAddress; - + HRESULT hr; sockaddr_un address; memset(&address, 0, sizeof(address)); @@ -39,22 +39,20 @@ HRESULT IpcCommServer::Bind(const std::string& rootAddress) return E_INVALIDARG; } + _rootAddress = rootAddress; + + address.sun_family = AF_UNIX; + IfFailRet(StringUtilities::Copy(address.sun_path, rootAddress.c_str())); + //We don't error check this on purpose std::remove(rootAddress.c_str()); - address.sun_family = AF_UNIX; -#if TARGET_WINDOWS - strncpy_s(address.sun_path, rootAddress.c_str(), sizeof(address.sun_path)); -#else - strncpy(address.sun_path, rootAddress.c_str(), sizeof(address.sun_path)); -#endif _domainSocket = socket(AF_UNIX, SOCK_STREAM, 0); if (!_domainSocket.Valid()) { return SocketWrapper::GetSocketError(); } - HRESULT hr; IfFailRet(_domainSocket.SetBlocking(false)); if (bind(_domainSocket, reinterpret_cast(&address), sizeof(address)) != 0) @@ -84,6 +82,8 @@ HRESULT IpcCommServer::Accept(std::shared_ptr& client) do { +#if TARGET_WINDOWS + fd_set set; FD_ZERO(&set); FD_SET(_domainSocket, &set); @@ -91,8 +91,17 @@ HRESULT IpcCommServer::Accept(std::shared_ptr& client) TIMEVAL timeout; timeout.tv_sec = AcceptTimeoutSeconds; timeout.tv_usec = 0; - result = select(2, &set, nullptr, nullptr, &timeout); + result = select(0, &set, nullptr, nullptr, &timeout); +#else + //select has limitations on Linux; any descriptor value over 1024 is ignored. + + pollfd set[1]; + set[0].fd = _domainSocket; + set[0].events = POLLIN; + set[0].revents = 0; + result = poll(set, 1, AcceptTimeoutSeconds * 1000); +#endif if (_shutdown.load()) { return E_ABORT; @@ -116,6 +125,7 @@ HRESULT IpcCommServer::Accept(std::shared_ptr& client) { return SocketWrapper::GetSocketError(); } + #if TARGET_WINDOWS DWORD receiveTimeout = ReceiveTimeoutMilliseconds; diff --git a/src/MonitorProfiler/Communication/IpcCommServer.h b/src/MonitorProfiler/Communication/IpcCommServer.h index 8e3bdcb0b5d..1f03862bf8d 100644 --- a/src/MonitorProfiler/Communication/IpcCommServer.h +++ b/src/MonitorProfiler/Communication/IpcCommServer.h @@ -10,11 +10,12 @@ #include "SocketWrapper.h" #include "IpcCommClient.h" +#include "../Logging/Logger.h" class IpcCommServer { public: - IpcCommServer(); + IpcCommServer(const std::shared_ptr& logger); ~IpcCommServer(); HRESULT Bind(const std::string& rootAddress); HRESULT Accept(std::shared_ptr& client); @@ -26,4 +27,5 @@ class IpcCommServer std::string _rootAddress; SocketWrapper _domainSocket = 0; std::atomic_bool _shutdown; -}; \ No newline at end of file + std::shared_ptr _logger; +}; diff --git a/src/MonitorProfiler/Communication/Messages.h b/src/MonitorProfiler/Communication/Messages.h index c03a959d952..88108767b98 100644 --- a/src/MonitorProfiler/Communication/Messages.h +++ b/src/MonitorProfiler/Communication/Messages.h @@ -15,4 +15,4 @@ struct IpcMessage { MessageType MessageType = MessageType::OK; int Parameters = 0; -}; \ No newline at end of file +}; diff --git a/src/MonitorProfiler/Communication/SocketWrapper.h b/src/MonitorProfiler/Communication/SocketWrapper.h index f42a3adbbdd..49763f95285 100644 --- a/src/MonitorProfiler/Communication/SocketWrapper.h +++ b/src/MonitorProfiler/Communication/SocketWrapper.h @@ -9,10 +9,10 @@ #include #else #include -#include +#include #include -#include -#include +#include +#include typedef int SOCKET; typedef struct timeval TIMEVAL; #endif diff --git a/src/MonitorProfiler/DllMain.cpp b/src/MonitorProfiler/DllMain.cpp index f264c26b084..edbfda740f3 100644 --- a/src/MonitorProfiler/DllMain.cpp +++ b/src/MonitorProfiler/DllMain.cpp @@ -16,6 +16,13 @@ STDMETHODIMP_(BOOL) DLLEXPORT DllMain(HMODULE hModule, DWORD ul_reason_for_call, return TRUE; } +STDAPI DLLEXPORT TestHook(_In_ void (*Callback)()) +{ + // This method is used by testing to inject a native frame into a callstack. + Callback(); + return 0; +} + _Check_return_ STDAPI DLLEXPORT DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv) { diff --git a/src/MonitorProfiler/Environment/EnvironmentHelper.cpp b/src/MonitorProfiler/Environment/EnvironmentHelper.cpp index 8c1dc830772..a0fc3131967 100644 --- a/src/MonitorProfiler/Environment/EnvironmentHelper.cpp +++ b/src/MonitorProfiler/Environment/EnvironmentHelper.cpp @@ -8,17 +8,21 @@ using namespace std; -#define IfFailLogRet(EXPR) IfFailLogRet_(pLogger, EXPR) +#define IfFailLogRet(EXPR) IfFailLogRet_(_logger, EXPR) -HRESULT EnvironmentHelper::GetDebugLoggerLevel( +EnvironmentHelper::EnvironmentHelper( const std::shared_ptr& pEnvironment, - LogLevel& level) + const std::shared_ptr& pLogger) : _environment(pEnvironment), _logger(pLogger) +{ +} + +HRESULT EnvironmentHelper::GetDebugLoggerLevel(LogLevel& level) { HRESULT hr = S_OK; tstring tstrLevel; - IfFailRet(pEnvironment->GetEnvironmentVariable( - s_wszDebugLoggerLevelEnvVar, + IfFailRet(_environment->GetEnvironmentVariable( + DebugLoggerLevelEnvVar, tstrLevel )); @@ -27,44 +31,77 @@ HRESULT EnvironmentHelper::GetDebugLoggerLevel( return S_OK; } -HRESULT EnvironmentHelper::SetProductVersion( - const shared_ptr& pEnvironment, - const shared_ptr& pLogger) +HRESULT EnvironmentHelper::SetProductVersion() { HRESULT hr = S_OK; - IfFailLogRet(pEnvironment->SetEnvironmentVariable( - s_wszProfilerVersionEnvVar, + IfFailLogRet(_environment->SetEnvironmentVariable( + ProfilerVersionEnvVar, MonitorProductVersion_TSTR )); return S_OK; } -HRESULT EnvironmentHelper::GetRuntimeInstanceId(const std::shared_ptr& pEnvironment, const std::shared_ptr& pLogger, tstring& instanceId) +HRESULT EnvironmentHelper::GetRuntimeInstanceId(tstring& instanceId) +{ + HRESULT hr = S_OK; + + IfFailLogRet(_environment->GetEnvironmentVariable(RuntimeInstanceEnvVar, instanceId)); + + return S_OK; +} + +HRESULT EnvironmentHelper::GetSharedPath(tstring& sharedPath) { HRESULT hr = S_OK; - IfFailLogRet(pEnvironment->GetEnvironmentVariable(s_wszRuntimeInstanceEnvVar, instanceId)); + hr = _environment->GetEnvironmentVariable(SharedPathEnvVar, sharedPath); + if (FAILED(hr)) + { + if (hr != HRESULT_FROM_WIN32(ERROR_ENVVAR_NOT_FOUND)) + { + return hr; + } + IfFailRet(GetTempFolder(sharedPath)); + } return S_OK; } -HRESULT EnvironmentHelper::GetTempFolder(const std::shared_ptr& pEnvironment, const std::shared_ptr& pLogger, tstring& tempFolder) +HRESULT EnvironmentHelper::GetStdErrLoggerLevel(LogLevel& level) +{ + HRESULT hr = S_OK; + + tstring tstrLevel; + IfFailRet(_environment->GetEnvironmentVariable( + StdErrLoggerLevelEnvVar, + tstrLevel + )); + + IfFailRet(LogLevelHelper::ToLogLevel(tstrLevel, level)); + + return S_OK; +} + +HRESULT EnvironmentHelper::GetTempFolder(tstring& tempFolder) { HRESULT hr = S_OK; tstring tmpDir; #if TARGET_WINDOWS - IfFailLogRet(pEnvironment->GetEnvironmentVariable(s_wszTempEnvVar, tmpDir)); + IfFailLogRet(_environment->GetEnvironmentVariable(TempEnvVar, tmpDir)); #else - hr = pEnvironment->GetEnvironmentVariable(s_wszTempEnvVar, tmpDir); -#endif - + hr = _environment->GetEnvironmentVariable(TempEnvVar, tmpDir); if (FAILED(hr)) { - tmpDir = s_wszDefaultTempFolder; + if (hr != HRESULT_FROM_WIN32(ERROR_ENVVAR_NOT_FOUND)) + { + return hr; + } + tmpDir = DefaultTempFolder; } +#endif tempFolder = std::move(tmpDir); diff --git a/src/MonitorProfiler/Environment/EnvironmentHelper.h b/src/MonitorProfiler/Environment/EnvironmentHelper.h index a0a9cb04979..803b32bdd84 100644 --- a/src/MonitorProfiler/Environment/EnvironmentHelper.h +++ b/src/MonitorProfiler/Environment/EnvironmentHelper.h @@ -8,18 +8,30 @@ #include "Environment.h" #include "../Logging/Logger.h" +#ifndef ERROR_ENVVAR_NOT_FOUND +#define ERROR_ENVVAR_NOT_FOUND 203L +#endif + /// /// Helper class for getting and setting known environment variables. /// class EnvironmentHelper final { private: - static constexpr LPCWSTR s_wszDebugLoggerLevelEnvVar = _T("DotnetMonitorProfiler_DebugLogger_Level"); - static constexpr LPCWSTR s_wszProfilerVersionEnvVar = _T("DotnetMonitorProfiler_ProductVersion"); - static constexpr LPCWSTR s_wszRuntimeInstanceEnvVar = _T("DotnetMonitorProfiler_InstanceId"); - static constexpr LPCWSTR s_wszDefaultTempFolder = _T("/tmp"); + static constexpr LPCWSTR DebugLoggerLevelEnvVar = _T("DotnetMonitor_Profiler_DebugLogger_Level"); + static constexpr LPCWSTR ProfilerVersionEnvVar = _T("DotnetMonitor_Profiler_ProductVersion"); + static constexpr LPCWSTR RuntimeInstanceEnvVar = _T("DotnetMonitor_Profiler_RuntimeInstanceId"); + static constexpr LPCWSTR SharedPathEnvVar = _T("DotnetMonitor_Profiler_SharedPath"); + static constexpr LPCWSTR StdErrLoggerLevelEnvVar = _T("DotnetMonitor_Profiler_StdErrLogger_Level"); - static constexpr LPCWSTR s_wszTempEnvVar = + std::shared_ptr _environment; + std::shared_ptr _logger; + +#if TARGET_UNIX + static constexpr LPCWSTR DefaultTempFolder = _T("/tmp"); +#endif + + static constexpr LPCWSTR TempEnvVar = #if TARGET_WINDOWS _T("TEMP"); #else @@ -27,27 +39,28 @@ class EnvironmentHelper final #endif public: + + EnvironmentHelper(const std::shared_ptr& pEnvironment, + const std::shared_ptr& pLogger); + /// /// Gets the log level for the debug logger from the environment. /// - static HRESULT GetDebugLoggerLevel( - const std::shared_ptr& pEnvironment, - LogLevel& level); + HRESULT GetDebugLoggerLevel(LogLevel& level); /// /// Sets the product version environment variable in the specified environment. /// - static HRESULT SetProductVersion( - const std::shared_ptr& pEnvironment, - const std::shared_ptr& pLogger); + HRESULT SetProductVersion(); + + HRESULT GetRuntimeInstanceId(tstring& instanceId); - static HRESULT GetRuntimeInstanceId( - const std::shared_ptr& pEnvironment, - const std::shared_ptr& pLogger, - tstring& instanceId); + HRESULT GetSharedPath(tstring& instanceId); + + /// + /// Gets the log level for the stderr logger from the environment. + /// + HRESULT GetStdErrLoggerLevel(LogLevel& level); - static HRESULT GetTempFolder( - const std::shared_ptr& pEnvironment, - const std::shared_ptr& pLogger, - tstring& tempFolder); + HRESULT GetTempFolder(tstring& tempFolder); }; diff --git a/src/MonitorProfiler/Environment/ProfilerEnvironment.cpp b/src/MonitorProfiler/Environment/ProfilerEnvironment.cpp index eb8be0160b7..69d6cd07d27 100644 --- a/src/MonitorProfiler/Environment/ProfilerEnvironment.cpp +++ b/src/MonitorProfiler/Environment/ProfilerEnvironment.cpp @@ -6,7 +6,7 @@ using namespace std; -ProfilerEnvironment::ProfilerEnvironment(ICorProfilerInfo11* pCorProfilerInfo) : +ProfilerEnvironment::ProfilerEnvironment(ICorProfilerInfo12* pCorProfilerInfo) : m_pCorProfilerInfo(pCorProfilerInfo) { } diff --git a/src/MonitorProfiler/Environment/ProfilerEnvironment.h b/src/MonitorProfiler/Environment/ProfilerEnvironment.h index 0ed8acd9be4..38ae8470714 100644 --- a/src/MonitorProfiler/Environment/ProfilerEnvironment.h +++ b/src/MonitorProfiler/Environment/ProfilerEnvironment.h @@ -29,10 +29,10 @@ class ProfilerEnvironment final : public IEnvironment { private: - ComPtr m_pCorProfilerInfo; + ComPtr m_pCorProfilerInfo; public: - ProfilerEnvironment(ICorProfilerInfo11* pCorProfilerInfo); + ProfilerEnvironment(ICorProfilerInfo12* pCorProfilerInfo); public: // IEnvironment diff --git a/src/MonitorProfiler/EventProvider/EventTypeMapping.h b/src/MonitorProfiler/EventProvider/EventTypeMapping.h new file mode 100644 index 00000000000..c0733ab63ec --- /dev/null +++ b/src/MonitorProfiler/EventProvider/EventTypeMapping.h @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "cor.h" +#include "corprof.h" +#include "com.h" +#include "tstring.h" +#include +#include + +/// +/// Helper class to convert types to COR_PRF_EVENTPIPE_PARAM_DESC representations +/// TODO: Need to map out remaining types, which are not used for the current data. +/// +template +class EventTypeMapping +{ +public: + void GetType(COR_PRF_EVENTPIPE_PARAM_DESC& descriptor) + { + // If we got here, it means we do not know how to convert the type to a COR_PRF_EVENTPIPE_PARAM_DESC + // We are not allowed to say static_assert(false), even if we never bind to this specialization. + static_assert(sizeof(T) != sizeof(T), "Invalid type"); + } +}; + +template<> +class EventTypeMapping +{ +public: + void GetType(COR_PRF_EVENTPIPE_PARAM_DESC& descriptor) + { + descriptor.type = COR_PRF_EVENTPIPE_UINT32; + descriptor.elementType = 0; + } +}; + +template<> +class EventTypeMapping +{ +public: + void GetType(COR_PRF_EVENTPIPE_PARAM_DESC& descriptor) + { + descriptor.type = COR_PRF_EVENTPIPE_UINT64; + descriptor.elementType = 0; + } +}; + +template<> +class EventTypeMapping +{ +public: + void GetType(COR_PRF_EVENTPIPE_PARAM_DESC& descriptor) + { + descriptor.type = COR_PRF_EVENTPIPE_STRING; + descriptor.elementType = 0; + } +}; + +template<> +class EventTypeMapping> +{ +public: + void GetType(COR_PRF_EVENTPIPE_PARAM_DESC& descriptor) + { + descriptor.type = COR_PRF_EVENTPIPE_ARRAY; + descriptor.elementType = COR_PRF_EVENTPIPE_UINT64; + } +}; diff --git a/src/MonitorProfiler/EventProvider/ProfilerEvent.h b/src/MonitorProfiler/EventProvider/ProfilerEvent.h new file mode 100644 index 00000000000..7c13177b7c1 --- /dev/null +++ b/src/MonitorProfiler/EventProvider/ProfilerEvent.h @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "cor.h" +#include "corprof.h" +#include "com.h" +#include "EventTypeMapping.h" +#include +#include +#include + +/// +/// Helper class used to write EventSource data. +/// Initialize is used to declare the data types and fill the COR_PRF_EVENTPIPE_PARAM_DESC structure. +/// WritePayload is used to create COR_PRF_EVENT_DATA and write the actual data. +/// We rely on variadic templates and template specialization for both of the above. +/// +/// IMPORTANT All data to WritePayload must be valid until the data is written by ICorProfiler12, not just assigned to COR_PRF_EVENT_DATA. +/// The profiler API uses memory addresses even for primitive types. +/// +template +class ProfilerEvent +{ + friend class ProfilerEventProvider; +public: + HRESULT WritePayload(const Args&... args); + +private: + ProfilerEvent(ICorProfilerInfo12* profilerInfo); + + template + HRESULT Initialize(const WCHAR* (&names)[sizeof...(Args)]); + + template + HRESULT Initialize(const WCHAR* (&names)[sizeof...(Args)]); + + template + HRESULT WritePayload(COR_PRF_EVENT_DATA* data, const T& first, TArgs... rest); + + // In order to specialize with variadic templates, all the overloads have to declare their template parameters + template + HRESULT WritePayload(COR_PRF_EVENT_DATA* data, const tstring& first, TArgs... rest); + + template + HRESULT WritePayload(COR_PRF_EVENT_DATA* data, const std::vector& first, TArgs... rest); + + template + static std::vector GetEventBuffer(const std::vector& data); + + template + static void WriteToBuffer(BYTE* pBuffer, size_t* pOffset, const T& value); + + template + HRESULT WritePayload(COR_PRF_EVENT_DATA* data); + +private: + + //TODO We don't have a way of modeling data with no payload. 0 sized arrays are not allowed. + COR_PRF_EVENTPIPE_PARAM_DESC _descriptor[sizeof...(Args)]; + + // Note this field is set by ProfilerEventProvider. + EVENTPIPE_EVENT _event; + ComPtr _profilerInfo; +}; + +template +ProfilerEvent::ProfilerEvent(ICorProfilerInfo12* profilerInfo) : _event(0), _profilerInfo(profilerInfo) +{ + memset(_descriptor, 0, sizeof(_descriptor)); +} + +template +template +HRESULT ProfilerEvent::Initialize(const WCHAR* (&names)[sizeof...(Args)]) +{ + _descriptor[index].name = names[index]; + EventTypeMapping typeMapper; + typeMapper.GetType(_descriptor[index]); + return Initialize(names); +} + +template +template +HRESULT ProfilerEvent::Initialize(const WCHAR* (&names)[sizeof...(Args)]) +{ + //CONSIDER An alternate design is to make each event call DefineEvent instead of leaving this responsibility on the provider. + return S_OK; +} + +template +HRESULT ProfilerEvent::WritePayload(const Args&... args) +{ + COR_PRF_EVENT_DATA data[sizeof...(Args)]; + return WritePayload<0, Args...>(data, args...); +} + +template +template +HRESULT ProfilerEvent::WritePayload(COR_PRF_EVENT_DATA* data, const T& first, TArgs... rest) +{ + data[index].ptr = reinterpret_cast(&first); + data[index].size = static_cast(sizeof(T)); + data[index].reserved = 0; + return WritePayload(data, rest...); +} + +template +template +HRESULT ProfilerEvent::WritePayload(COR_PRF_EVENT_DATA* data, const tstring& first, TArgs... rest) +{ + if (first.size() == 0) + { + data[index].ptr = 0; + data[index].size = 0; + data[index].reserved = 0; + } + else + { + data[index].ptr = reinterpret_cast(first.c_str()); + data[index].size = static_cast((first.size() + 1) * sizeof(WCHAR)); + data[index].reserved = 0; + } + return WritePayload(data, rest...); +} + + template + template + HRESULT ProfilerEvent::WritePayload(COR_PRF_EVENT_DATA* data, const std::vector& first, TArgs... rest) + { + // This value must stay in scope during all the WritePayload functions. + std::vector buffer(0); + + if (first.size() == 0) + { + data[index].ptr = 0; + data[index].size = 0; + data[index].reserved = 0; + } + else + { + buffer = std::move(GetEventBuffer(first)); + data[index].ptr = reinterpret_cast(buffer.data()); + data[index].size = static_cast(buffer.size()); + data[index].reserved = 0; + } + return WritePayload(data, rest...); + } + +template +template +std::vector ProfilerEvent::GetEventBuffer(const std::vector& data) +{ + size_t offset = 0; + //2 byte length prefix + size_t bufferSize = sizeof(UINT16) + (data.size() * sizeof(T)); + std::vector buffer = std::vector(bufferSize); + WriteToBuffer(buffer.data(), &offset, (UINT16)data.size()); + + for (const T& element : data) + { + WriteToBuffer(buffer.data(), &offset, element); + } + + return buffer; +} + +template +template +void ProfilerEvent::WriteToBuffer(BYTE* pBuffer, size_t* pOffset, const T& value) +{ + *(T*)(pBuffer + *pOffset) = value; + *pOffset += sizeof(T); +} + +template +template +HRESULT ProfilerEvent::WritePayload(COR_PRF_EVENT_DATA* data) +{ + return _profilerInfo->EventPipeWriteEvent(_event, sizeof...(Args), data, nullptr, nullptr); +} diff --git a/src/MonitorProfiler/EventProvider/ProfilerEventProvider.cpp b/src/MonitorProfiler/EventProvider/ProfilerEventProvider.cpp new file mode 100644 index 00000000000..d1e289d4ba4 --- /dev/null +++ b/src/MonitorProfiler/EventProvider/ProfilerEventProvider.cpp @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "ProfilerEventProvider.h" +#include "corhlpr.h" + +HRESULT ProfilerEventProvider::CreateProvider(const WCHAR* providerName, ICorProfilerInfo12* profilerInfo, std::unique_ptr& provider) +{ + EVENTPIPE_PROVIDER eventProvider = 0; + HRESULT hr; + + IfFailRet(profilerInfo->EventPipeCreateProvider(providerName, &eventProvider)); + provider.reset(new ProfilerEventProvider(profilerInfo, eventProvider)); + + return S_OK; +} + +ProfilerEventProvider::ProfilerEventProvider(ICorProfilerInfo12* profilerInfo, EVENTPIPE_PROVIDER provider) : _provider(provider), _profilerInfo(profilerInfo) +{ +} diff --git a/src/MonitorProfiler/EventProvider/ProfilerEventProvider.h b/src/MonitorProfiler/EventProvider/ProfilerEventProvider.h new file mode 100644 index 00000000000..c4ea2289ef9 --- /dev/null +++ b/src/MonitorProfiler/EventProvider/ProfilerEventProvider.h @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "cor.h" +#include "corprof.h" +#include "com.h" +#include "corhlpr.h" +#include "ProfilerEvent.h" +#include + +class ProfilerEventProvider +{ + public: + static HRESULT CreateProvider(const WCHAR* providerName, ICorProfilerInfo12* profilerInfo, std::unique_ptr& provider); + + template + HRESULT DefineEvent(const WCHAR* eventName, std::unique_ptr>& profilerEventDescriptor, const WCHAR* (&names)[sizeof...(TArgs)]); + + EVENTPIPE_PROVIDER GetProvider() { return _provider; } + + private: + ProfilerEventProvider(ICorProfilerInfo12* profilerInfo, EVENTPIPE_PROVIDER provider); + EVENTPIPE_PROVIDER _provider = 0; + int _currentEventId = 1; + ComPtr _profilerInfo; +}; + +template +HRESULT ProfilerEventProvider::DefineEvent(const WCHAR* eventName, typename std::unique_ptr>& profilerEventDescriptor, const WCHAR* (&names)[sizeof...(TArgs)]) +{ + EVENTPIPE_EVENT event = 0; + HRESULT hr; + + auto newEvent = typename std::unique_ptr>(new ProfilerEvent(_profilerInfo)); + hr = newEvent->template Initialize<0, TArgs...>(names); + IfFailRet(hr); + + IfFailRet(_profilerInfo->EventPipeDefineEvent( + _provider, + eventName, + _currentEventId, + 0, //We not use keywords + 1, // eventVersion + COR_PRF_EVENTPIPE_LOGALWAYS, + 0, //We not use opcodes + FALSE, //No need for stacks + sizeof...(TArgs), + newEvent->_descriptor, + &event)); + + profilerEventDescriptor.reset(newEvent.release()); + profilerEventDescriptor->_event = event; + _currentEventId++; + return S_OK; +} diff --git a/src/MonitorProfiler/Logging/AggregateLogger.cpp b/src/MonitorProfiler/Logging/AggregateLogger.cpp index 7740653cde0..e2f4b52f0ac 100644 --- a/src/MonitorProfiler/Logging/AggregateLogger.cpp +++ b/src/MonitorProfiler/Logging/AggregateLogger.cpp @@ -30,7 +30,7 @@ STDMETHODIMP_(bool) AggregateLogger::IsEnabled(LogLevel level) return false; } -STDMETHODIMP AggregateLogger::Log(LogLevel level, const tstring format, va_list args) +STDMETHODIMP AggregateLogger::Log(LogLevel level, const lstring& message) { HRESULT hr = S_OK; @@ -42,7 +42,7 @@ STDMETHODIMP AggregateLogger::Log(LogLevel level, const tstring format, va_list // not need to check if the level is enabled for each Log call. if (pLogger->IsEnabled(level)) { - IfFailRet(pLogger->Log(level, format, args)); + IfFailRet(pLogger->Log(level, message)); } } diff --git a/src/MonitorProfiler/Logging/AggregateLogger.h b/src/MonitorProfiler/Logging/AggregateLogger.h index 26b26588d1c..143780e2dba 100644 --- a/src/MonitorProfiler/Logging/AggregateLogger.h +++ b/src/MonitorProfiler/Logging/AggregateLogger.h @@ -35,5 +35,5 @@ class AggregateLogger final : /// /// Invokes the Log method on each registered ILogger implementation. /// - STDMETHOD(Log)(LogLevel level, const tstring format, va_list args) override; + STDMETHOD(Log)(LogLevel level, const lstring& message) override; }; diff --git a/src/MonitorProfiler/Logging/DebugLogger.cpp b/src/MonitorProfiler/Logging/DebugLogger.cpp index 2477a7dc849..38705240e46 100644 --- a/src/MonitorProfiler/Logging/DebugLogger.cpp +++ b/src/MonitorProfiler/Logging/DebugLogger.cpp @@ -3,27 +3,32 @@ // See the LICENSE file in the project root for more information. #include "DebugLogger.h" +#include "LoggerHelper.h" #include "LogLevelHelper.h" #include "../Environment/EnvironmentHelper.h" +#include "NullLogger.h" +#include "macros.h" using namespace std; -DebugLogger::DebugLogger(const shared_ptr& pEnvironment) +DebugLogger::DebugLogger(const shared_ptr& environment) { // Try to get log level from environment - if (FAILED(EnvironmentHelper::GetDebugLoggerLevel(pEnvironment, m_level))) + + EnvironmentHelper helper(environment, NullLogger::Instance); + if (FAILED(helper.GetDebugLoggerLevel(_level))) { // Fallback to default level - m_level = s_DefaultLevel; + _level = DefaultLevel; } } STDMETHODIMP_(bool) DebugLogger::IsEnabled(LogLevel level) { - return LogLevelHelper::IsEnabled(level, m_level); + return LogLevelHelper::IsEnabled(level, _level); } -STDMETHODIMP DebugLogger::Log(LogLevel level, const tstring format, va_list args) +STDMETHODIMP DebugLogger::Log(LogLevel level, const lstring& message) { if (!IsEnabled(level)) { @@ -32,30 +37,18 @@ STDMETHODIMP DebugLogger::Log(LogLevel level, const tstring format, va_list args HRESULT hr = S_OK; - WCHAR wszMessage[s_nMaxEntrySize]; - _vsnwprintf_s( - wszMessage, - s_nMaxEntrySize, - _TRUNCATE, - format.c_str(), - args); + lstring levelStr; + IfFailRet(LogLevelHelper::GetShortName(level, levelStr)); - tstring tstrLevel; - if (FAILED(LogLevelHelper::GetShortName(level, tstrLevel))) - { - tstrLevel.assign(_T("ukwn")); - } + WCHAR output[MaxEntrySize] = {}; - WCHAR wszString[s_nMaxEntrySize]; - _snwprintf_s( - wszString, - s_nMaxEntrySize, - _TRUNCATE, + IfFailRet(LoggerHelper::FormatTruncate( + output, _T("[profiler]%s: %s\r\n"), - tstrLevel.c_str(), - wszMessage); + levelStr.c_str(), + message.c_str())); - OutputDebugStringW(wszString); + OutputDebugStringW(output); return S_OK; } diff --git a/src/MonitorProfiler/Logging/DebugLogger.h b/src/MonitorProfiler/Logging/DebugLogger.h index e35a3ede094..e4a7086b5f6 100644 --- a/src/MonitorProfiler/Logging/DebugLogger.h +++ b/src/MonitorProfiler/Logging/DebugLogger.h @@ -16,13 +16,13 @@ class DebugLogger final : public ILogger { private: - const static LogLevel s_DefaultLevel = LogLevel::Information; - const static int s_nMaxEntrySize = 1000; + const static LogLevel DefaultLevel = LogLevel::Information; + const static size_t MaxEntrySize = 1000; - LogLevel m_level = s_DefaultLevel; + LogLevel _level = DefaultLevel; public: - DebugLogger(const std::shared_ptr& pEnvironment); + DebugLogger(const std::shared_ptr& environment); public: // ILogger Members @@ -31,5 +31,5 @@ class DebugLogger final : STDMETHOD_(bool, IsEnabled)(LogLevel level) override; /// - STDMETHOD(Log)(LogLevel level, const tstring format, va_list args) override; + STDMETHOD(Log)(LogLevel level, const lstring& message) override; }; diff --git a/src/MonitorProfiler/Logging/LogLevelHelper.h b/src/MonitorProfiler/Logging/LogLevelHelper.h index 36540f2c4e9..8c02b4da307 100644 --- a/src/MonitorProfiler/Logging/LogLevelHelper.h +++ b/src/MonitorProfiler/Logging/LogLevelHelper.h @@ -16,35 +16,36 @@ class LogLevelHelper final /// /// Gets the short name of the log level. /// - static HRESULT GetShortName(LogLevel level, tstring& strName) + static HRESULT GetShortName(LogLevel level, lstring& strName) { // The log levels are intentionally four characters long // to allow for easy horizontal alignment. switch (level) { case LogLevel::Critical: - strName.assign(_T("crit")); + strName.assign(_LS("crit")); return S_OK; case LogLevel::Debug: - strName.assign(_T("dbug")); + strName.assign(_LS("dbug")); return S_OK; case LogLevel::Error: - strName.assign(_T("fail")); + strName.assign(_LS("fail")); return S_OK; case LogLevel::Information: - strName.assign(_T("info")); + strName.assign(_LS("info")); return S_OK; case LogLevel::None: - strName.assign(_T("none")); + strName.assign(_LS("none")); return S_OK; case LogLevel::Trace: - strName.assign(_T("trce")); + strName.assign(_LS("trce")); return S_OK; case LogLevel::Warning: - strName.assign(_T("warn")); + strName.assign(_LS("warn")); return S_OK; default: - return E_FAIL; + strName.assign(_LS("ukwn")); + return S_OK; } } diff --git a/src/MonitorProfiler/Logging/Logger.cpp b/src/MonitorProfiler/Logging/Logger.cpp new file mode 100644 index 00000000000..b2b6263397b --- /dev/null +++ b/src/MonitorProfiler/Logging/Logger.cpp @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "Logger.h" +#include "LoggerHelper.h" +#include "macros.h" + +using namespace std; + +const LCHAR* ILogger::ConvertArg(const char* str, std::vector& argStrings) +{ +#ifdef TARGET_WINDOWS + return ConvertArg(std::string(str), argStrings); +#else + return str; +#endif +} + +const LCHAR* ILogger::ConvertArg(const std::string& str, std::vector& argStrings) +{ +#ifdef TARGET_WINDOWS + // Convert the string, place it in the argStrings vector, and return the raw string for formatting. + return argStrings.emplace(argStrings.end(), to_tstring(str))->c_str(); +#else + // string and lstring have the same width on non-Windows + return str.c_str(); +#endif +} + +const LCHAR* ILogger::ConvertArg(const tstring& str, std::vector& argStrings) +{ +#ifdef TARGET_WINDOWS + // tstring and lstring have the same width on Windows + return str.c_str(); +#else + // Convert the string, place it in the argStrings vector, and return the raw string for formatting. + return argStrings.emplace(argStrings.end(), to_string(str))->c_str(); +#endif +} + +STDMETHODIMP ILogger::LogV(LogLevel level, const lstring format, ...) +{ + // CONSIDER: The current approach of formatting the format string before + // sending it off to the individual logger implementations prevents + // structured logging. The was already precluded because of the use of va_list + // without knowledge of the arguments types. If structured logging is desired, + // consider some way of capturing individual format arguments and passing + // their information and values in an alternative manner than using va_list. + HRESULT hr = S_OK; + + LCHAR message[MaxEntrySize] = {}; + + va_list args; + va_start(args, format); + + hr = LoggerHelper::FormatTruncate(message, format.c_str(), args); + + va_end(args); + + IfFailRet(hr); + + IfFailRet(Log(level, message)); + + return S_OK; +} diff --git a/src/MonitorProfiler/Logging/Logger.h b/src/MonitorProfiler/Logging/Logger.h index 5459c512322..fb9a083754e 100644 --- a/src/MonitorProfiler/Logging/Logger.h +++ b/src/MonitorProfiler/Logging/Logger.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include "corhlpr.h" #include "tstring.h" @@ -22,11 +23,29 @@ enum class LogLevel None }; +#ifdef TARGET_WINDOWS +#define _LS(str) L##str +#else +#define _LS(str) str +#endif + +#ifdef TARGET_WINDOWS +typedef std::wstring lstring; +typedef wchar_t LCHAR; +#else +typedef std::string lstring; +typedef char LCHAR; +#endif + /// /// Interface for logging messages. /// DECLARE_INTERFACE(ILogger) { +private: + const static size_t MaxEntrySize = 1000; + +public: /// /// Determines if the logger accepts a message at the given LogLevel. /// @@ -35,16 +54,54 @@ DECLARE_INTERFACE(ILogger) /// /// Writes a log message. /// - STDMETHOD(Log)(LogLevel level, const tstring format, va_list args) PURE; + STDMETHOD(Log)(LogLevel level, const lstring& message) PURE; - inline STDMETHODIMP Log(LogLevel level, const tstring format, ...) + template + inline STDMETHODIMP Log(LogLevel level, const lstring format, const T&... args) { - va_list args; - va_start(args, format); - HRESULT hr = Log(level, format, args); - va_end(args); - return hr; + // A cache of strings that were converted from their original width + // to the string width for the target platform. This prevents freeing of the + // converted strings before logging can complete. + // CONSIDER: Make this a static thread local so that the vector does not + // need to be allocated for each log call. Reserve the new size before calling + // LogV and clear it after calling LogV. + std::vector argStrings; + argStrings.reserve(sizeof...(args)); + + // Call LogV method with the pack expansion converting strings to the + // appropriate string width for the target platform. + return LogV(level, format, ConvertArg(args, argStrings)...); } + +private: + /// + /// Convert char* strings to logging string width for the target platform. + /// + static const LCHAR* ConvertArg(const char* str, std::vector& argStrings); + + /// + /// Convert narrow strings to logging string width for the target platform. + /// + static const LCHAR* ConvertArg(const std::string& str, std::vector& argStrings); + + /// + /// Convert tstrings to logging string width for the target platform. + /// + static const LCHAR* ConvertArg(const tstring& str, std::vector& argStrings); + + /// + /// Pass through all other argument types as-is. + /// + template + inline static T ConvertArg(const T& value, std::vector& argStrings) + { + return value; + } + + /// + /// Formats the format string with the variable arguments and calls the Log(level, message) function. + /// + STDMETHODIMP LogV(LogLevel level, const lstring format, ...); }; // Checks if EXPR is a failed HRESULT @@ -58,8 +115,8 @@ DECLARE_INTERFACE(ILogger) { \ pLogger->Log(\ LogLevel::Error, \ - _T("IfFailLogRet(" #EXPR ") failed in function %s: 0x%08x"), \ - to_tstring(__func__).c_str(), \ + _LS("IfFailLogRet(" #EXPR ") failed in function %s: 0x%08x"), \ + __func__, \ hr); \ } \ } \ @@ -67,6 +124,25 @@ DECLARE_INTERFACE(ILogger) } \ } while (0) +// Checks if EXPR is false +// If false, logs the failure and returns the provided HRESULT +#define IfFalseLogRet_(pLogger, EXPR, hr) \ + do { \ + if(!(EXPR)) { \ + if (nullptr != pLogger) { \ + if (pLogger->IsEnabled(LogLevel::Error)) \ + { \ + pLogger->Log(\ + LogLevel::Error, \ + _LS("IfFalseLogRet(" #EXPR ") is false in function %s: 0x%08x"), \ + __func__, \ + hr); \ + } \ + } \ + return hr; \ + } \ + } while (0) + // Checks if EXPR is nullptr // If nullptr, logs the failure and returns E_POINTER #define IfNullLogRetPtr_(pLogger, EXPR) \ @@ -77,8 +153,8 @@ DECLARE_INTERFACE(ILogger) { \ pLogger->Log( \ LogLevel::Error, \ - _T("IfNullLogRetPtr(" #EXPR ") failed in function %s"), \ - to_tstring(__func__).c_str()); \ + _LS("IfNullLogRetPtr(" #EXPR ") failed in function %s"), \ + __func__); \ } \ } \ return E_POINTER; \ @@ -90,7 +166,7 @@ DECLARE_INTERFACE(ILogger) #define LogV_(pLogger, level, format, ...) \ if (pLogger->IsEnabled(level)) \ { \ - IfFailRet(pLogger->Log(level, format, __VA_ARGS__)); \ + IfFailRet(pLogger->Log(level, _LS(format), __VA_ARGS__)); \ } // Logs a message at the Trace level diff --git a/src/MonitorProfiler/Logging/LoggerHelper.cpp b/src/MonitorProfiler/Logging/LoggerHelper.cpp new file mode 100644 index 00000000000..2c80131bf26 --- /dev/null +++ b/src/MonitorProfiler/Logging/LoggerHelper.cpp @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "LoggerHelper.h" +#include "macros.h" + +using namespace std; + +HRESULT LoggerHelper::FormatTruncate(LCHAR* buffer, size_t size, const LCHAR* format, ...) +{ + va_list args; + va_start(args, format); + int result = FormatTruncate(buffer, size, format, args); + va_end(args); + return result; +} + +HRESULT LoggerHelper::FormatTruncate(LCHAR* buffer, size_t size, const LCHAR* format, va_list args) +{ + HRESULT hr = S_OK; + + IfFailRet(SaveRestoreErrno(FormatTruncateImpl, buffer, size, format, args)); + + return S_OK; +} + +HRESULT LoggerHelper::Write(FILE* stream, const LCHAR* format, ...) +{ + va_list args; + va_start(args, format); + + HRESULT hr = S_OK; + + hr = SaveRestoreErrno(WriteImpl, stream, format, args); + + va_end(args); + + IfFailRet(hr); + + // SaveRestoreErrno returns S_FALSE if failure was indicated but errno was zero. + if (S_FALSE == hr) + { + // Multibyte encoding errors will set the stream to an error state. + // Get the error indicator from the stream. + int result = ferror(stream); + if (0 != result) + { + hr = HRESULT_FROM_ERRNO(result); + } + else + { + // This is an undocumented condition. + hr = E_UNEXPECTED; + } + } + + if (FAILED(hr)) + { + return hr; + } + + return S_OK; +} + +int LoggerHelper::FormatTruncateImpl(LCHAR* buffer, size_t size, const LCHAR* format, va_list args) +{ +#ifdef TARGET_WINDOWS + return _vsnwprintf_s( + buffer, + size, + _TRUNCATE, + format, + args); +#else + return vsnprintf( + buffer, + size, + format, + args); +#endif +} + +template +HRESULT LoggerHelper::SaveRestoreErrno(int (*func)(T...), T... args) +{ + // Save the errno state before func invocation. + int previousError = errno; + errno = 0; + + HRESULT hr = S_OK; + if (func(args...) < 0) + { + if (0 != errno) + { + // Create HRESULT from errno. + hr = HRESULT_FROM_ERRNO(errno); + } + else + { + // An error may not have actually occured since errno + // was not set. For example, string formatting APIs will + // return -1 if truncation occurs, which may not be an error. + hr = S_FALSE; + } + } + + // Restore errno to value before func invocation. + errno = previousError; + + return hr; +} + +int LoggerHelper::WriteImpl(FILE* stream, const LCHAR* format, va_list args) +{ +#ifdef TARGET_WINDOWS + return vfwprintf_s( + stream, + format, + args); +#else + return vfprintf( + stream, + format, + args); +#endif +} diff --git a/src/MonitorProfiler/Logging/LoggerHelper.h b/src/MonitorProfiler/Logging/LoggerHelper.h new file mode 100644 index 00000000000..41b28633d98 --- /dev/null +++ b/src/MonitorProfiler/Logging/LoggerHelper.h @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "corhlpr.h" +#include "Logger.h" + +class LoggerHelper final +{ +public: + /// + /// Write formatted output using a pointer to a list of arguments. + /// + static HRESULT FormatTruncate( + LCHAR* buffer, + size_t size, + const LCHAR* format, + va_list args); + + /// + /// Write formatted output using a pointer to a list of arguments. + /// + template + inline static HRESULT FormatTruncate( + LCHAR (&buffer)[size], + const LCHAR* format, + va_list args) + { + return FormatTruncate(buffer, size, format, args); + } + + /// + /// Writes formatted data to a string. + /// + static HRESULT FormatTruncate( + LCHAR* buffer, + size_t size, + const LCHAR* format, + ...); + + /// + /// Writes formatted data to a string. + /// + template + inline static HRESULT FormatTruncate( + LCHAR (&buffer)[size], + const LCHAR* format, + ...) + { + va_list args; + va_start(args, format); + int result = FormatTruncate(buffer, size, format, args); + va_end(args); + return result; + } + + /// + /// Write formatted output to a stream using a pointer to a list of arguments. + /// + static HRESULT Write( + FILE* stream, + const LCHAR* format, + ...); + +private: + /// + /// _vsnwprintf_s / vsnprintf + /// + inline static int FormatTruncateImpl( + LCHAR* buffer, + size_t size, + const LCHAR* format, + va_list args); + + /// + /// Saves the current errno value, executes the function, restores errno, and returns + /// an HRESULT based on the value of the error reported from the function execution. + /// + template + static HRESULT SaveRestoreErrno( + int (*func)(T...), + T... args); + + /// + /// vfwprintf_s / vfprintf + /// + inline static int WriteImpl( + FILE* stream, + const LCHAR* format, + va_list args); +}; diff --git a/src/MonitorProfiler/Logging/NullLogger.cpp b/src/MonitorProfiler/Logging/NullLogger.cpp new file mode 100644 index 00000000000..62ca651ed48 --- /dev/null +++ b/src/MonitorProfiler/Logging/NullLogger.cpp @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "NullLogger.h" + +std::shared_ptr NullLogger::Instance = std::make_shared(); diff --git a/src/MonitorProfiler/Logging/NullLogger.h b/src/MonitorProfiler/Logging/NullLogger.h new file mode 100644 index 00000000000..439fba7b1ab --- /dev/null +++ b/src/MonitorProfiler/Logging/NullLogger.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "Logger.h" +#include + +/// +/// Provides an empty logger implementation +/// +class NullLogger final : + public ILogger +{ +public: + // ILogger Members + + static std::shared_ptr Instance; + + /// + STDMETHOD_(bool, IsEnabled)(LogLevel level) override + { + return false; + } + + /// + STDMETHOD(Log)(LogLevel level, const lstring& message) override + { + return S_OK; + } +}; diff --git a/src/MonitorProfiler/Logging/StdErrLogger.cpp b/src/MonitorProfiler/Logging/StdErrLogger.cpp new file mode 100644 index 00000000000..1fadfac5dd7 --- /dev/null +++ b/src/MonitorProfiler/Logging/StdErrLogger.cpp @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "StdErrLogger.h" +#include "LoggerHelper.h" +#include "LogLevelHelper.h" +#include "../Environment/EnvironmentHelper.h" +#include "NullLogger.h" +#include "macros.h" + +using namespace std; + +StdErrLogger::StdErrLogger(const shared_ptr& pEnvironment) +{ + // Try to get log level from environment + + EnvironmentHelper helper(pEnvironment, NullLogger::Instance); + if (FAILED(helper.GetStdErrLoggerLevel(_level))) + { + // Fallback to default level + _level = DefaultLevel; + } +} + +STDMETHODIMP_(bool) StdErrLogger::IsEnabled(LogLevel level) +{ + return LogLevelHelper::IsEnabled(level, _level); +} + +STDMETHODIMP StdErrLogger::Log(LogLevel level, const lstring& message) +{ + if (!IsEnabled(level)) + { + return S_FALSE; + } + + HRESULT hr = S_OK; + + lstring levelStr; + IfFailRet(LogLevelHelper::GetShortName(level, levelStr)); + + IfFailRet(LoggerHelper::Write( + stderr, + _LS("[profiler]%s: %s\n"), + levelStr.c_str(), + message.c_str())); + + return S_OK; +} diff --git a/src/MonitorProfiler/Logging/StdErrLogger.h b/src/MonitorProfiler/Logging/StdErrLogger.h new file mode 100644 index 00000000000..e5b862bc677 --- /dev/null +++ b/src/MonitorProfiler/Logging/StdErrLogger.h @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include +#include +#include "Logger.h" +#include "../Environment/Environment.h" + +/// +/// Logs messages to the stderr stream. +/// +class StdErrLogger final : + public ILogger +{ +private: + const static LogLevel DefaultLevel = LogLevel::None; + const static size_t MaxEntrySize = 1000; + + LogLevel _level = DefaultLevel; + +public: + StdErrLogger(const std::shared_ptr& pEnvironment); + +public: + // ILogger Members + + /// + STDMETHOD_(bool, IsEnabled)(LogLevel level) override; + + /// + STDMETHOD(Log)(LogLevel level, const lstring& message) override; +}; diff --git a/src/MonitorProfiler/MainProfiler/ExceptionTracker.cpp b/src/MonitorProfiler/MainProfiler/ExceptionTracker.cpp new file mode 100644 index 00000000000..4e8e026ca3a --- /dev/null +++ b/src/MonitorProfiler/MainProfiler/ExceptionTracker.cpp @@ -0,0 +1,230 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include "ExceptionTracker.h" +#include "../Utilities/NameCache.h" +#include "../Utilities/TypeNameUtilities.h" + +using namespace std; + +#define IfFailLogRet(EXPR) IfFailLogRet_(_logger, EXPR) +#define IfFalseLogRet(EXPR, hr) IfFalseLogRet_(_logger, EXPR, hr) + +#define LogDebugV(format, ...) LogDebugV_(_logger, format, __VA_ARGS__) +#define LogInformationV(format, ...) LogInformationV_(_logger, format, __VA_ARGS__) +#define LogErrorV(format, ...) LogErrorV_(_logger, format, __VA_ARGS__) + +/* + Exception Callbacks + + When a managed exception is thrown, the runtime will invoke the profiler with several + callback methods during the exception processing phases. For a thrown exception, the + following callbacks are invoked in the following order: + - Phase 1: Throwing + - ExceptionThrown: Invoked when the exception object has been thrown. + - Phase 2: Finding Exception Catcher. Each frame on the callstack is inspected for a matching handler. + The following are invoked for each frame starting at the leaf frame (the frame from which the exception is thrown): + - ExceptionSearchFunctionEnter: Invoked when beginning to inspect a function for a matching handler. + - If the function has handlers with filters (e.g. C#'s when clause), the following are invoked for each filter: + - ExceptionSearchFilterEnter: Invoked when beginning to evaluate the filter. + - ExceptionSearchFilterLeave: Invoked when finishing the evaluation of the filter. + - ExceptionSearchCatcherFound: Invoked only if a matching filter is found. + - ExceptionSearchFunctionLeave: Invoked when finishing the inspection of the function for a matching handler. + If a matching handler was found, the exception catcher finding phase ends after the ExceptionSearchFunctionLeave callback. + - Phase 3: Unwinding. + The following are invoked for each frame starting at the leaf frame (the frame from which the exception is thrown): + - ExceptionUnwindFunctionEnter: Invoked when beginning to unwind a frame. + If a matching handler was found, the unwind phase ends after the invocation of + the ExceptionUnwindFunctionEnter callback for the frame that contains the matching handler. + - ExceptionUnwindFunctionLeave: Invoked when finishing the unwind of a frame. + If all of the frames are unwound to completion, the process is terminated. + + Unhandled Exception Detection Algorithm + + A subset of the aboved described callbacks is used to determine if an unhandled exception has occurred: + - ExceptionThrown: Record the originating FunctionID and the existance of the exception. + - ExceptionSearchCatcherFound: Record that the exception will be handled in a catching FunctionID. + - ExceptionUnwindFunctionEnter: If the exception was not handled (ExceptionSearchCatcherFound was not invoked), + then the exception is unhandled at this point. Do some extra bookkeeping in the case when the exception is + handled so that the exception is cleared once the frame with the corresponding catching FunctionID is encountered. + + Current pitfalls: + - Algorithm doesn't account for exceptions thrown within exception filters. These will be seen as a new set of + callback invocations starting with throwing, searching, and unwinding, however, the callstack does not unwind + out of the exception filter and the original exception processing is resumed. + - Algorithm doesn't account for exceptions thrown within finally blocks. Presumably, these will be seen as new set + of callback invocations starting with throwing, searching, and unwinding. The original exception processing is + superceded by the new set of callbacks for the new exception. +*/ + + +ExceptionTracker::ExceptionTracker( + const shared_ptr& logger, + const shared_ptr threadDataManager, + ICorProfilerInfo12* corProfilerInfo) +{ + _corProfilerInfo = corProfilerInfo; + _logger = logger; + _threadDataManager = threadDataManager; +} + +void ExceptionTracker::AddProfilerEventMask(DWORD& eventsLow) +{ + if (_logger->IsEnabled(LogLevel::Debug)) + { + eventsLow |= COR_PRF_MONITOR::COR_PRF_ENABLE_STACK_SNAPSHOT; + } +} + +HRESULT ExceptionTracker::ExceptionThrown(ThreadID threadId, ObjectID objectId) +{ + // CAUTION: Do not store the exception ObjectID. It is not guaranteed to be correct + // outside of the ExceptionThrown callback without updating it using GC callbacks. + + HRESULT hr = S_OK; + + IfFailLogRet(_threadDataManager->SetHasException(threadId)); + + // Exception throwing is common; don't pay to calculate method name if it won't be logged. + if (_logger->IsEnabled(LogLevel::Debug)) + { + ClassID classId; + IfFailLogRet(_corProfilerInfo->GetClassFromObject(objectId, &classId)); + + tstring className; + IfFailLogRet(GetFullyQualifiedClassName(classId, className)); + LogDebugV("Exception thrown: %s", className); + + hr = _corProfilerInfo->DoStackSnapshot( + threadId, + LogExceptionThrownFrameCallback, + COR_PRF_SNAPSHOT_INFO::COR_PRF_SNAPSHOT_DEFAULT, + this, + nullptr, + 0); + + if (FAILED(hr) && hr != CORPROF_E_STACKSNAPSHOT_ABORTED) + { + LogErrorV("DoStackSnapshot failed in function %s: 0x%08x", __func__, hr); + return hr; + } + } + + return S_OK; +} + +HRESULT ExceptionTracker::ExceptionSearchCatcherFound(ThreadID threadId, FunctionID functionId) +{ + HRESULT hr = S_OK; + + IfFailLogRet(_threadDataManager->SetExceptionCatcherFunction(threadId, functionId)); + + return S_OK; +} + +HRESULT ExceptionTracker::ExceptionUnwindFunctionEnter(ThreadID threadId, FunctionID functionId) +{ + HRESULT hr = S_OK; + + bool hasException = false; + FunctionID catcherFunctionId = ThreadData::NoFunctionId; + IfFailLogRet(_threadDataManager->GetException(threadId, &hasException, &catcherFunctionId)); + IfFalseLogRet(hasException, E_UNEXPECTED); + + if (ThreadData::NoFunctionId == catcherFunctionId) + { + tstring methodName; + IfFailLogRet(GetFullyQualifiedMethodName(functionId, methodName)); + LogInformationV("Exception unhandled: %s", methodName); + + // Future: Block thread until collection is initiated of the desired artifact. + // Possible serialization of some context of the exception and surrounding method + // information such as locals and parameters. + } + else if (functionId == catcherFunctionId) + { + IfFailLogRet(_threadDataManager->ClearException(threadId)); + + // Exception handling is common; don't pay to calculate method name if it won't be logged. + if (_logger->IsEnabled(LogLevel::Debug)) + { + tstring methodName; + IfFailLogRet(GetFullyQualifiedMethodName(functionId, methodName)); + LogDebugV("Exception handled: %s", methodName); + } + } + + return S_OK; +} + +HRESULT ExceptionTracker::GetFullyQualifiedClassName(ClassID classId, tstring& fullTypeName) +{ + HRESULT hr = S_OK; + + NameCache cache; + TypeNameUtilities typeNameUtilities(_corProfilerInfo); + + IfFailRet(typeNameUtilities.CacheNames(cache, classId)); + IfFailRet(cache.GetFullyQualifiedClassName(classId, fullTypeName)); + + return S_OK; +} + +HRESULT ExceptionTracker::GetFullyQualifiedMethodName(FunctionID functionId, tstring& fullMethodName) +{ + HRESULT hr = S_OK; + + IfFailRet(GetFullyQualifiedMethodName(functionId, 0, fullMethodName)); + + return S_OK; +} + +HRESULT ExceptionTracker::GetFullyQualifiedMethodName(FunctionID functionId, COR_PRF_FRAME_INFO frameInfo, tstring& fullMethodName) +{ + HRESULT hr = S_OK; + + NameCache cache; + TypeNameUtilities typeNameUtilities(_corProfilerInfo); + + IfFailRet(typeNameUtilities.CacheNames(cache, functionId, frameInfo)); + IfFailRet(cache.GetFullyQualifiedName(functionId, fullMethodName)); + + return S_OK; +} + +HRESULT ExceptionTracker::LogExceptionThrownFrame(FunctionID functionId, COR_PRF_FRAME_INFO frameInfo) +{ + HRESULT hr = S_OK; + + tstring methodName; + IfFailLogRet(GetFullyQualifiedMethodName(functionId, frameInfo, methodName)); + LogDebugV("Exception method: %s", methodName); + + return S_OK; +} + +HRESULT ExceptionTracker::LogExceptionThrownFrameCallback( + FunctionID functionId, + UINT_PTR ip, + COR_PRF_FRAME_INFO frameInfo, + ULONG32 contextSize, + BYTE context[], + void* clientData) +{ + if (nullptr == clientData) + { + return E_POINTER; + } + + ExceptionTracker* exceptionTracker = static_cast(clientData); + + HRESULT hr = S_OK; + + IfFailRet(exceptionTracker->LogExceptionThrownFrame(functionId, frameInfo)); + + // Cancel stack snapshot callbacks after the top frame. + return S_FALSE; +} +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS diff --git a/src/MonitorProfiler/MainProfiler/ExceptionTracker.h b/src/MonitorProfiler/MainProfiler/ExceptionTracker.h new file mode 100644 index 00000000000..34f327491af --- /dev/null +++ b/src/MonitorProfiler/MainProfiler/ExceptionTracker.h @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include +#include "../Logging/Logger.h" +#include "ThreadDataManager.h" +#include "com.h" + +/// +/// Class for tracking exceptions for a runtime instance. +/// +class ExceptionTracker +{ +private: + ComPtr _corProfilerInfo; + std::shared_ptr _logger; + std::shared_ptr _threadDataManager; + +public: + ExceptionTracker( + const std::shared_ptr& logger, + const std::shared_ptr threadDataManager, + ICorProfilerInfo12* corProfilerInfo); + + /// + /// Adds profiler event masks needed by class. + /// + void AddProfilerEventMask(DWORD& eventsLow); + + // Exceptions + HRESULT ExceptionThrown(ThreadID threadId, ObjectID objectId); + HRESULT ExceptionSearchCatcherFound(ThreadID threadId, FunctionID functionId); + HRESULT ExceptionUnwindFunctionEnter(ThreadID threadId, FunctionID functionId); + +private: + // Method and type name utilities + HRESULT GetFullyQualifiedClassName(ClassID classId, tstring& fullTypeName); + HRESULT GetFullyQualifiedMethodName(FunctionID functionId, tstring& fullMethodName); + HRESULT GetFullyQualifiedMethodName(FunctionID functionId, COR_PRF_FRAME_INFO frameInfo, tstring& fullMethodName); + + // ExceptionThrown frame logging utilities + HRESULT LogExceptionThrownFrame(FunctionID functionId, COR_PRF_FRAME_INFO frameInfo); + static HRESULT STDMETHODCALLTYPE LogExceptionThrownFrameCallback( + FunctionID functionId, + UINT_PTR ip, + COR_PRF_FRAME_INFO frameInfo, + ULONG32 contextSize, + BYTE context[], + void* clientData); +}; +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS diff --git a/src/MonitorProfiler/MainProfiler/MainProfiler.cpp b/src/MonitorProfiler/MainProfiler/MainProfiler.cpp index e20908153d4..e6c350ca302 100644 --- a/src/MonitorProfiler/MainProfiler/MainProfiler.cpp +++ b/src/MonitorProfiler/MainProfiler/MainProfiler.cpp @@ -7,6 +7,9 @@ #include "../Environment/ProfilerEnvironment.h" #include "../Logging/AggregateLogger.h" #include "../Logging/DebugLogger.h" +#include "../Logging/StdErrLogger.h" +#include "../Stacks/StacksEventProvider.h" +#include "../Stacks/StackSampler.h" #include "corhlpr.h" #include "macros.h" #include @@ -15,8 +18,6 @@ using namespace std; #define IfFailLogRet(EXPR) IfFailLogRet_(m_pLogger, EXPR) -#define LogInformationV(format, ...) LogInformationV_(m_pLogger, format, __VA_ARGS__) - GUID MainProfiler::GetClsid() { // {6A494330-5848-4A23-9D87-0E57BBF6DE79} @@ -29,24 +30,10 @@ STDMETHODIMP MainProfiler::Initialize(IUnknown *pICorProfilerInfoUnk) HRESULT hr = S_OK; - //These should always be initialized first + // These should always be initialized first IfFailRet(ProfilerBase::Initialize(pICorProfilerInfoUnk)); - IfFailRet(InitializeEnvironment()); - IfFailRet(InitializeLogging()); - - IfFailLogRet(InitializeCommandServer()); - - // Logging is initialized and can now be used - - // Set product version environment variable to allow discovery of if the profiler - // as been applied to a target process. Diagnostic tools must use the diagnostic - // communication channel's GetProcessEnvironment command to get this value. - IfFailLogRet(EnvironmentHelper::SetProductVersion(m_pEnvironment, m_pLogger)); -#ifdef TARGET_WINDOWS - DWORD processId = GetCurrentProcessId(); - LogInformationV(_T("Process Id: %d"), processId); -#endif + IfFailRet(InitializeCommon()); return S_OK; } @@ -61,6 +48,82 @@ STDMETHODIMP MainProfiler::Shutdown() return ProfilerBase::Shutdown(); } +STDMETHODIMP MainProfiler::ThreadCreated(ThreadID threadId) +{ + HRESULT hr = S_OK; + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + IfFailLogRet(_threadDataManager->ThreadCreated(threadId)); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + + return S_OK; +} + +STDMETHODIMP MainProfiler::ThreadDestroyed(ThreadID threadId) +{ + HRESULT hr = S_OK; + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + IfFailLogRet(_threadDataManager->ThreadDestroyed(threadId)); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + + return S_OK; +} + +STDMETHODIMP MainProfiler::ExceptionThrown(ObjectID thrownObjectId) +{ + HRESULT hr = S_OK; + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + ThreadID threadId; + IfFailLogRet(m_pCorProfilerInfo->GetCurrentThreadID(&threadId)); + + IfFailLogRet(_exceptionTracker->ExceptionThrown(threadId, thrownObjectId)); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + + return S_OK; +} + +STDMETHODIMP MainProfiler::ExceptionSearchCatcherFound(FunctionID functionId) +{ + HRESULT hr = S_OK; + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + ThreadID threadId; + IfFailLogRet(m_pCorProfilerInfo->GetCurrentThreadID(&threadId)); + + IfFailLogRet(_exceptionTracker->ExceptionSearchCatcherFound(threadId, functionId)); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + + return S_OK; +} + +STDMETHODIMP MainProfiler::ExceptionUnwindFunctionEnter(FunctionID functionId) +{ + HRESULT hr = S_OK; + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + ThreadID threadId; + IfFailLogRet(m_pCorProfilerInfo->GetCurrentThreadID(&threadId)); + + IfFailLogRet(_exceptionTracker->ExceptionUnwindFunctionEnter(threadId, functionId)); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + + return S_OK; +} + +STDMETHODIMP MainProfiler::InitializeForAttach(IUnknown* pCorProfilerInfoUnk, void* pvClientData, UINT cbClientData) +{ + HRESULT hr = S_OK; + + // These should always be initialized first + IfFailRet(ProfilerBase::Initialize(pCorProfilerInfoUnk)); + + IfFailRet(InitializeCommon()); + + return S_OK; +} + STDMETHODIMP MainProfiler::LoadAsNotficationOnly(BOOL *pbNotificationOnly) { ExpectedPtr(pbNotificationOnly); @@ -70,11 +133,61 @@ STDMETHODIMP MainProfiler::LoadAsNotficationOnly(BOOL *pbNotificationOnly) return S_OK; } +HRESULT MainProfiler::InitializeCommon() +{ + HRESULT hr = S_OK; + + // These are created in dependency order! + IfFailRet(InitializeEnvironment()); + IfFailRet(InitializeLogging()); + IfFailRet(InitializeEnvironmentHelper()); + + // Logging is initialized and can now be used + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + _threadDataManager = make_shared(m_pLogger); + IfNullRet(_threadDataManager); + _exceptionTracker.reset(new (nothrow) ExceptionTracker(m_pLogger, _threadDataManager, m_pCorProfilerInfo)); + IfNullRet(_exceptionTracker); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + + IfFailLogRet(InitializeCommandServer()); + + // Set product version environment variable to allow discovery of if the profiler + // as been applied to a target process. Diagnostic tools must use the diagnostic + // communication channel's GetProcessEnvironment command to get this value. + IfFailLogRet(_environmentHelper->SetProductVersion()); + + DWORD eventsLow = COR_PRF_MONITOR::COR_PRF_MONITOR_NONE; +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + ThreadDataManager::AddProfilerEventMask(eventsLow); + _exceptionTracker->AddProfilerEventMask(eventsLow); +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS + StackSampler::AddProfilerEventMask(eventsLow); + + IfFailRet(m_pCorProfilerInfo->SetEventMask2( + eventsLow, + COR_PRF_HIGH_MONITOR::COR_PRF_HIGH_MONITOR_NONE)); + + return S_OK; +} + HRESULT MainProfiler::InitializeEnvironment() { + if (m_pEnvironment) + { + return E_UNEXPECTED; + } m_pEnvironment = make_shared(m_pCorProfilerInfo); + return S_OK; +} + +HRESULT MainProfiler::InitializeEnvironmentHelper() +{ IfNullRet(m_pEnvironment); + _environmentHelper = make_shared(m_pEnvironment, m_pLogger); + return S_OK; } @@ -86,6 +199,10 @@ HRESULT MainProfiler::InitializeLogging() unique_ptr pAggregateLogger(new (nothrow) AggregateLogger()); IfNullRet(pAggregateLogger); + shared_ptr pStdErrLogger = make_shared(m_pEnvironment); + IfNullRet(pStdErrLogger); + pAggregateLogger->Add(pStdErrLogger); + #ifdef _DEBUG #ifdef TARGET_WINDOWS // Add the debug output logger for when debugging on Windows @@ -104,17 +221,8 @@ HRESULT MainProfiler::InitializeCommandServer() { HRESULT hr = S_OK; - //TODO For now we are using the process id to generate the unique server name. We should use the environment - //value with the runtime instance id once it's available. - unsigned long pid = -#if TARGET_WINDOWS - GetCurrentProcessId(); -#else - getpid(); -#endif - - tstring instanceId = to_tstring(to_string(pid)); - //IfFailRet(EnvironmentHelper::GetRuntimeInstanceId(m_pEnvironment, m_pLogger, instanceId)); + tstring instanceId; + IfFailRet(_environmentHelper->GetRuntimeInstanceId(instanceId)); #if TARGET_UNIX tstring separator = _T("/"); @@ -122,11 +230,11 @@ HRESULT MainProfiler::InitializeCommandServer() tstring separator = _T("\\"); #endif - tstring tmpDir; - IfFailRet(EnvironmentHelper::GetTempFolder(m_pEnvironment, m_pLogger, tmpDir)); + tstring sharedPath; + IfFailRet(_environmentHelper->GetSharedPath(sharedPath)); - _commandServer = std::unique_ptr(new CommandServer(m_pLogger)); - tstring socketPath = tmpDir + separator + instanceId + _T(".sock"); + _commandServer = std::unique_ptr(new CommandServer(m_pLogger, m_pCorProfilerInfo)); + tstring socketPath = sharedPath + separator + instanceId + _T(".sock"); IfFailRet(_commandServer->Start(to_string(socketPath), [this](const IpcMessage& message)-> HRESULT { return this->MessageCallback(message); })); @@ -135,6 +243,54 @@ HRESULT MainProfiler::InitializeCommandServer() HRESULT MainProfiler::MessageCallback(const IpcMessage& message) { - m_pLogger->Log(LogLevel::Information, _T("Message received from client: %d %d"), message.MessageType, message.Parameters); + m_pLogger->Log(LogLevel::Information, _LS("Message received from client: %d %d"), message.MessageType, message.Parameters); + + if (message.MessageType == MessageType::Callstack) + { + //Currently we do not have any options for this message + return ProcessCallstackMessage(); + } + + return S_OK; +} + +HRESULT MainProfiler::ProcessCallstackMessage() +{ + HRESULT hr; + + StackSampler stackSampler(m_pCorProfilerInfo); + std::vector> stackStates; + std::shared_ptr nameCache; + + IfFailLogRet(stackSampler.CreateCallstack(stackStates, nameCache)); + + std::unique_ptr eventProvider; + IfFailLogRet(StacksEventProvider::CreateProvider(m_pCorProfilerInfo, eventProvider)); + + for (auto& entry : nameCache->GetFunctions()) + { + IfFailLogRet(eventProvider->WriteFunctionData(entry.first, *entry.second.get())); + } + for (auto& entry : nameCache->GetClasses()) + { + IfFailLogRet(eventProvider->WriteClassData(entry.first, *entry.second.get())); + } + for (auto& entry : nameCache->GetModules()) + { + IfFailLogRet(eventProvider->WriteModuleData(entry.first, *entry.second.get())); + } + for (auto& entry : nameCache->GetTypeNames()) + { + //first: (Module,TypeDef) + IfFailLogRet(eventProvider->WriteTokenData(entry.first.first, entry.first.second, *entry.second.get())); + } + + for (std::unique_ptr& stackState : stackStates) + { + IfFailLogRet(eventProvider->WriteCallstack(stackState->GetStack())); + } + + IfFailLogRet(eventProvider->WriteEndEvent()); + return S_OK; } diff --git a/src/MonitorProfiler/MainProfiler/MainProfiler.h b/src/MonitorProfiler/MainProfiler/MainProfiler.h index 9836f0335f6..6da3f1a20af 100644 --- a/src/MonitorProfiler/MainProfiler/MainProfiler.h +++ b/src/MonitorProfiler/MainProfiler/MainProfiler.h @@ -6,29 +6,49 @@ #include "../ProfilerBase.h" #include "../Environment/Environment.h" +#include "../Environment/EnvironmentHelper.h" #include "../Logging/Logger.h" #include "../Communication/CommandServer.h" #include +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include "ThreadDataManager.h" +#include "ExceptionTracker.h" +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS class MainProfiler final : public ProfilerBase { private: std::shared_ptr m_pEnvironment; + std::shared_ptr _environmentHelper; std::shared_ptr m_pLogger; +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS + std::shared_ptr _threadDataManager; + std::unique_ptr _exceptionTracker; +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS public: static GUID GetClsid(); STDMETHOD(Initialize)(IUnknown* pICorProfilerInfoUnk) override; STDMETHOD(Shutdown)() override; + STDMETHOD(ThreadCreated)(ThreadID threadId) override; + STDMETHOD(ThreadDestroyed)(ThreadID threadId) override; + STDMETHOD(ExceptionThrown)(ObjectID thrownObjectId) override; + STDMETHOD(ExceptionSearchCatcherFound)(FunctionID functionId) override; + STDMETHOD(ExceptionUnwindFunctionEnter)(FunctionID functionId) override; + STDMETHOD(InitializeForAttach)(IUnknown* pCorProfilerInfoUnk, void* pvClientData, UINT cbClientData) override; STDMETHOD(LoadAsNotficationOnly)(BOOL *pbNotificationOnly) override; private: + HRESULT InitializeCommon(); HRESULT InitializeEnvironment(); + HRESULT InitializeEnvironmentHelper(); HRESULT InitializeLogging(); HRESULT InitializeCommandServer(); HRESULT MessageCallback(const IpcMessage& message); + HRESULT ProcessCallstackMessage(); private: std::unique_ptr _commandServer; }; + diff --git a/src/MonitorProfiler/MainProfiler/ThreadData.cpp b/src/MonitorProfiler/MainProfiler/ThreadData.cpp new file mode 100644 index 00000000000..a793deeabe4 --- /dev/null +++ b/src/MonitorProfiler/MainProfiler/ThreadData.cpp @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include "ThreadData.h" +#include "macros.h" + +using namespace std; + +#define IfFalseLogRet(EXPR, hr) IfFalseLogRet_(_logger, EXPR, hr) + +ThreadData::ThreadData(const shared_ptr& logger) : + _exceptionCatcherFunctionId(NoFunctionId), + _hasException(false), + _logger(logger) +{ +} + +void ThreadData::ClearException() +{ + _exceptionCatcherFunctionId = NoFunctionId; + _hasException = false; +} + +HRESULT ThreadData::GetException(bool* hasException, FunctionID* catcherFunctionId) +{ + ExpectedPtr(hasException); + ExpectedPtr(catcherFunctionId); + + *hasException = _hasException; + *catcherFunctionId = _exceptionCatcherFunctionId; + + return S_OK; +} + +HRESULT ThreadData::SetHasException() +{ + IfFalseLogRet(!_hasException, E_UNEXPECTED); + IfFalseLogRet(NoFunctionId == _exceptionCatcherFunctionId, E_UNEXPECTED); + + _hasException = true; + + return S_OK; +} + +HRESULT ThreadData::SetExceptionCatcherFunction(FunctionID functionId) +{ + IfFalseLogRet(NoFunctionId != functionId, E_INVALIDARG); + IfFalseLogRet(_hasException, E_UNEXPECTED); + + _exceptionCatcherFunctionId = functionId; + + return S_OK; +} +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS diff --git a/src/MonitorProfiler/MainProfiler/ThreadData.h b/src/MonitorProfiler/MainProfiler/ThreadData.h new file mode 100644 index 00000000000..583d91e7ef9 --- /dev/null +++ b/src/MonitorProfiler/MainProfiler/ThreadData.h @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include +#include "corhlpr.h" +#include "corprof.h" +#include "../Logging/Logger.h" + +/// +/// Class representing common data for a single thread. +/// +class ThreadData +{ +public: + static const FunctionID NoFunctionId = 0; + +private: + FunctionID _exceptionCatcherFunctionId; + bool _hasException; + std::shared_ptr _logger; + +public: + ThreadData(const std::shared_ptr& logger); + + // Exceptions + void ClearException(); + HRESULT GetException(bool* hasException, FunctionID* catcherFunctionId); + HRESULT SetHasException(); + HRESULT SetExceptionCatcherFunction(FunctionID functionId); +}; +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS diff --git a/src/MonitorProfiler/MainProfiler/ThreadDataManager.cpp b/src/MonitorProfiler/MainProfiler/ThreadDataManager.cpp new file mode 100644 index 00000000000..e0d53848f6a --- /dev/null +++ b/src/MonitorProfiler/MainProfiler/ThreadDataManager.cpp @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include "ThreadDataManager.h" +#include "macros.h" +#include + +using namespace std; + +#define IfFailLogRet(EXPR) IfFailLogRet_(_logger, EXPR) +#define IfFalseLogRet(EXPR, hr) IfFalseLogRet_(_logger, EXPR, hr) + +typedef unordered_map>::iterator DataMapIterator; + +ThreadDataManager::ThreadDataManager(const shared_ptr& logger) +{ + _logger = logger; +} + +void ThreadDataManager::AddProfilerEventMask(DWORD& eventsLow) +{ + eventsLow |= COR_PRF_MONITOR::COR_PRF_MONITOR_THREADS; + eventsLow |= COR_PRF_MONITOR::COR_PRF_MONITOR_EXCEPTIONS; +} + +HRESULT ThreadDataManager::ThreadCreated(ThreadID threadId) +{ + lock_guard lock(_dataMapMutex); + + _dataMap.insert(make_pair(threadId, make_shared(_logger))); + + return S_OK; +} + +HRESULT ThreadDataManager::ThreadDestroyed(ThreadID threadId) +{ + lock_guard lock(_dataMapMutex); + + _dataMap.erase(threadId); + + return S_OK; +} + +HRESULT ThreadDataManager::ClearException(ThreadID threadId) +{ + HRESULT hr = S_OK; + + shared_ptr threadData; + IfFailLogRet(GetThreadData(threadId, threadData)); + + threadData->ClearException(); + + return S_OK; +} + +HRESULT ThreadDataManager::GetException(ThreadID threadId, bool* hasException, FunctionID* catcherFunctionId) +{ + ExpectedPtr(hasException); + ExpectedPtr(catcherFunctionId); + + HRESULT hr = S_OK; + + shared_ptr threadData; + IfFailLogRet(GetThreadData(threadId, threadData)); + + IfFailLogRet(threadData->GetException(hasException, catcherFunctionId)); + + return *hasException ? S_FALSE : S_OK; +} + +HRESULT ThreadDataManager::SetHasException(ThreadID threadId) +{ + HRESULT hr = S_OK; + + shared_ptr threadData; + IfFailLogRet(GetThreadData(threadId, threadData)); + + IfFailLogRet(threadData->SetHasException()); + + return S_OK; +} + +HRESULT ThreadDataManager::SetExceptionCatcherFunction(ThreadID threadId, FunctionID catcherFunctionId) +{ + HRESULT hr = S_OK; + + shared_ptr threadData; + IfFailLogRet(GetThreadData(threadId, threadData)); + + IfFailLogRet(threadData->SetExceptionCatcherFunction(catcherFunctionId)); + + return S_OK; +} + +HRESULT ThreadDataManager::GetThreadData(ThreadID threadId, shared_ptr& threadData) +{ + lock_guard mapLock(_dataMapMutex); + + DataMapIterator iterator = _dataMap.find(threadId); + IfFalseLogRet(iterator != _dataMap.end(), E_UNEXPECTED); + + threadData = iterator->second; + + return S_OK; +} +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS diff --git a/src/MonitorProfiler/MainProfiler/ThreadDataManager.h b/src/MonitorProfiler/MainProfiler/ThreadDataManager.h new file mode 100644 index 00000000000..781f1fb25f5 --- /dev/null +++ b/src/MonitorProfiler/MainProfiler/ThreadDataManager.h @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#ifdef DOTNETMONITOR_FEATURE_EXCEPTIONS +#include +#include +#include +#include "corhlpr.h" +#include "corprof.h" +#include "ThreadData.h" +#include "../Logging/Logger.h" + +/// +/// Class for managing common thread information. +/// +class ThreadDataManager +{ +private: + std::unordered_map> _dataMap; + std::mutex _dataMapMutex; + std::shared_ptr _logger; + +public: + ThreadDataManager(const std::shared_ptr& logger); + + /// + /// Adds profiler event masks needed by class. + /// + static void AddProfilerEventMask(DWORD& eventsLow); + + // Threads + HRESULT ThreadCreated(ThreadID threadId); + HRESULT ThreadDestroyed(ThreadID threadId); + + // Exceptions + HRESULT ClearException(ThreadID threadId); + HRESULT GetException(ThreadID threadId, bool* hasException, FunctionID* catcherFunctionId); + HRESULT SetHasException(ThreadID threadId); + HRESULT SetExceptionCatcherFunction(ThreadID threadId, FunctionID catcherFunctionId); + +private: + HRESULT GetThreadData(ThreadID threadId, std::shared_ptr& threadData); +}; +#endif // DOTNETMONITOR_FEATURE_EXCEPTIONS diff --git a/src/MonitorProfiler/MonitorProfiler.def b/src/MonitorProfiler/MonitorProfiler.def index f215e572b0c..d32c363a556 100644 --- a/src/MonitorProfiler/MonitorProfiler.def +++ b/src/MonitorProfiler/MonitorProfiler.def @@ -4,3 +4,4 @@ EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE DllMain PRIVATE + TestHook PRIVATE diff --git a/src/MonitorProfiler/ProfilerBase.cpp b/src/MonitorProfiler/ProfilerBase.cpp index 38cb6bf41eb..43fa33e239b 100644 --- a/src/MonitorProfiler/ProfilerBase.cpp +++ b/src/MonitorProfiler/ProfilerBase.cpp @@ -18,7 +18,7 @@ STDMETHODIMP ProfilerBase::Initialize(IUnknown *pICorProfilerInfoUnk) HRESULT hr = S_OK; IfFailRet(pICorProfilerInfoUnk->QueryInterface( - IID_ICorProfilerInfo11, + IID_ICorProfilerInfo12, reinterpret_cast(&m_pCorProfilerInfo))); return S_OK; @@ -408,6 +408,14 @@ STDMETHODIMP ProfilerBase::HandleDestroyed(GCHandleID handleId) STDMETHODIMP ProfilerBase::InitializeForAttach(IUnknown *pCorProfilerInfoUnk, void *pvClientData, UINT cbClientData) { + ExpectedPtr(pCorProfilerInfoUnk); + + HRESULT hr = S_OK; + + IfFailRet(pCorProfilerInfoUnk->QueryInterface( + IID_ICorProfilerInfo12, + reinterpret_cast(&m_pCorProfilerInfo))); + return S_OK; } diff --git a/src/MonitorProfiler/ProfilerBase.h b/src/MonitorProfiler/ProfilerBase.h index dfc16f04545..9386b6139a4 100644 --- a/src/MonitorProfiler/ProfilerBase.h +++ b/src/MonitorProfiler/ProfilerBase.h @@ -14,7 +14,7 @@ class ProfilerBase : public ICorProfilerCallback11 { protected: - ComPtr m_pCorProfilerInfo; + ComPtr m_pCorProfilerInfo; public: ProfilerBase(); diff --git a/src/MonitorProfiler/Stacks/Stack.h b/src/MonitorProfiler/Stacks/Stack.h new file mode 100644 index 00000000000..4e748d9d206 --- /dev/null +++ b/src/MonitorProfiler/Stacks/Stack.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include +#include "cor.h" +#include "corprof.h" + +class Stack +{ +public: + const UINT32 GetThreadId() const { return _tid; } + void SetThreadId(UINT32 threadid) { _tid = threadid; } + const std::vector& GetFunctionIds() const { return _functionIds; } + const std::vector& GetOffsets() const { return _offsets; } + + void AddFrame(FunctionID functionID, UINT_PTR offset) + { + _functionIds.push_back(functionID); + _offsets.push_back(offset); + } +private: + UINT32 _tid = 0; + //We model these as two parallel arrays instead of objects to simplify conversion to the EventSource format of std::vector + std::vector _functionIds; + std::vector _offsets; +}; + + + diff --git a/src/MonitorProfiler/Stacks/StackSampler.cpp b/src/MonitorProfiler/Stacks/StackSampler.cpp new file mode 100644 index 00000000000..28723dee249 --- /dev/null +++ b/src/MonitorProfiler/Stacks/StackSampler.cpp @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "StackSampler.h" +#include "corhlpr.h" +#include "Stack.h" +#include +#include +#include "../Utilities/TypeNameUtilities.h" + +StackSamplerState::StackSamplerState(ICorProfilerInfo12* profilerInfo, std::shared_ptr nameCache) + : _profilerInfo(profilerInfo), _nameCache(nameCache) +{ +} + +Stack& StackSamplerState::GetStack() +{ + return _stack; +} + +std::shared_ptr StackSamplerState::GetNameCache() +{ + return _nameCache; +} + +ICorProfilerInfo12* StackSamplerState::GetProfilerInfo() +{ + return _profilerInfo; +} + +StackSampler::StackSampler(ICorProfilerInfo12* profilerInfo) : _profilerInfo(profilerInfo) +{ +} + +void StackSampler::AddProfilerEventMask(DWORD& eventsLow) +{ + eventsLow |= COR_PRF_MONITOR::COR_PRF_ENABLE_STACK_SNAPSHOT; +} + +HRESULT StackSampler::CreateCallstack(std::vector>& stackStates, std::shared_ptr& nameCache) +{ + HRESULT hr; + + IfFailRet(_profilerInfo->SuspendRuntime()); + auto resumeRuntime = [](ICorProfilerInfo12* profilerInfo) { profilerInfo->ResumeRuntime(); }; + std::unique_ptr resumeRuntimeHandle(static_cast(_profilerInfo), resumeRuntime); + + ComPtr threadEnum = nullptr; + IfFailRet(_profilerInfo->EnumThreads(&threadEnum)); + + ThreadID threadID; + ULONG numReturned; + + if (nameCache == nullptr) + { + nameCache = std::make_shared(); + } + + while ((hr = threadEnum->Next(1, &threadID, &numReturned)) == S_OK) + { + std::unique_ptr stackState = std::unique_ptr(new StackSamplerState(_profilerInfo, nameCache)); + DWORD nativeThreadId = 0; + IfFailRet(_profilerInfo->GetThreadInfo(threadID, &nativeThreadId)); + stackState->GetStack().SetThreadId(nativeThreadId); + + //TODO According to docs, need to block ThreadDestroyed while stack walking. Is this still a requirement? + hr = _profilerInfo->DoStackSnapshot(threadID, DoStackSnapshotCallbackWrapper, COR_PRF_SNAPSHOT_REGISTER_CONTEXT, stackState.get(), nullptr, 0); + + //Typically fails due to lack of managed frames. + //CONSIDER Do we want to report the thread and specify that it has no managed frames? + //TODO Log unexpected failures + if (SUCCEEDED(hr)) + { + stackStates.push_back(std::move(stackState)); + } + } + + return S_OK; +} + +HRESULT __stdcall StackSampler::DoStackSnapshotCallbackWrapper(FunctionID functionId, UINT_PTR ip, COR_PRF_FRAME_INFO frameInfo, ULONG32 contextSize, BYTE context[], void* clientData) +{ + HRESULT hr; + + StackSamplerState* state = reinterpret_cast(clientData); + Stack& stack = state->GetStack(); + stack.AddFrame(functionId, ip); + + //FunctionId of 0 indicates a native frame. + if (functionId != 0) + { + std::shared_ptr nameCache = state->GetNameCache(); + TypeNameUtilities nameUtilities(state->GetProfilerInfo()); + IfFailRet(nameUtilities.CacheNames(*nameCache, functionId, frameInfo)); + } + + return S_OK; +} diff --git a/src/MonitorProfiler/Stacks/StackSampler.h b/src/MonitorProfiler/Stacks/StackSampler.h new file mode 100644 index 00000000000..9035c9d7307 --- /dev/null +++ b/src/MonitorProfiler/Stacks/StackSampler.h @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "cor.h" +#include "corprof.h" +#include "com.h" +#include "tstring.h" +#include "Stack.h" +#include "../Utilities/NameCache.h" + +class StackSamplerState +{ + public: + StackSamplerState(ICorProfilerInfo12* profilerInfo, std::shared_ptr nameCache); + Stack& GetStack(); + std::shared_ptr GetNameCache(); + ICorProfilerInfo12* GetProfilerInfo(); + private: + ComPtr _profilerInfo; + Stack _stack; + std::shared_ptr _nameCache; +}; + +class StackSampler +{ + public: + StackSampler(ICorProfilerInfo12* profilerInfo); + HRESULT CreateCallstack(std::vector>& stackStates, std::shared_ptr& nameCache); + static void AddProfilerEventMask(DWORD& eventsLow); + private: + static HRESULT __stdcall DoStackSnapshotCallbackWrapper( + FunctionID functionId, + UINT_PTR ip, + COR_PRF_FRAME_INFO frameInfo, + ULONG32 contextSize, + BYTE context[], + void* clientData); + + ComPtr _profilerInfo; +}; diff --git a/src/MonitorProfiler/Stacks/StacksEventProvider.cpp b/src/MonitorProfiler/Stacks/StacksEventProvider.cpp new file mode 100644 index 00000000000..b80dc8a24e3 --- /dev/null +++ b/src/MonitorProfiler/Stacks/StacksEventProvider.cpp @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "StacksEventProvider.h" +#include "corhlpr.h" +#include "cor.h" + +const WCHAR* StacksEventProvider::ProviderName = _T("DotnetMonitorStacksEventProvider"); + +HRESULT StacksEventProvider::CreateProvider(ICorProfilerInfo12* profilerInfo, std::unique_ptr& eventProvider) +{ + std::unique_ptr provider; + HRESULT hr; + + IfFailRet(ProfilerEventProvider::CreateProvider(ProviderName, profilerInfo, provider)); + + eventProvider = std::unique_ptr(new StacksEventProvider(profilerInfo, provider)); + IfFailRet(eventProvider->DefineEvents()); + + return S_OK; +} + +HRESULT StacksEventProvider::DefineEvents() +{ + HRESULT hr; + + IfFailRet(_provider->DefineEvent(_T("Callstack"), _callstackEvent, CallstackPayloads)); + IfFailRet(_provider->DefineEvent(_T("FunctionDesc"), _functionEvent, FunctionPayloads)); + IfFailRet(_provider->DefineEvent(_T("ClassDesc"), _classEvent, ClassPayloads)); + IfFailRet(_provider->DefineEvent(_T("ModuleDesc"), _moduleEvent, ModulePayloads)); + IfFailRet(_provider->DefineEvent(_T("TokenDesc"), _tokenEvent, TokenPayloads)); + IfFailRet(_provider->DefineEvent(_T("End"), _endEvent, EndPayloads)); + + return S_OK; +} + +HRESULT StacksEventProvider::WriteCallstack(const Stack& stack) +{ + return _callstackEvent->WritePayload(stack.GetThreadId(), stack.GetFunctionIds(), stack.GetOffsets()); +} + +HRESULT StacksEventProvider::WriteClassData(ClassID classId, const ClassData& classData) +{ + return _classEvent->WritePayload( + static_cast(classId), + static_cast(classData.GetModuleId()), + classData.GetToken(), + static_cast(classData.GetFlags()), + classData.GetTypeArgs()); +} + +HRESULT StacksEventProvider::WriteFunctionData(FunctionID functionId, const FunctionData& functionData) +{ + return _functionEvent->WritePayload( + static_cast(functionId), + static_cast(functionData.GetClass()), + functionData.GetClassToken(), + static_cast(functionData.GetModuleId()), + functionData.GetName(), + functionData.GetTypeArgs()); +} + +HRESULT StacksEventProvider::WriteModuleData(ModuleID moduleId, const ModuleData& moduleData) +{ + return _moduleEvent->WritePayload(moduleId, moduleData.GetName()); +} + +HRESULT StacksEventProvider::WriteTokenData(ModuleID moduleId, mdTypeDef typeDef, const TokenData& tokenData) +{ + return _tokenEvent->WritePayload(moduleId, typeDef, tokenData.GetOuterToken(), tokenData.GetName()); +} + +HRESULT StacksEventProvider::WriteEndEvent() +{ + return _endEvent->WritePayload(0); +} diff --git a/src/MonitorProfiler/Stacks/StacksEventProvider.h b/src/MonitorProfiler/Stacks/StacksEventProvider.h new file mode 100644 index 00000000000..58b3e48c772 --- /dev/null +++ b/src/MonitorProfiler/Stacks/StacksEventProvider.h @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "../EventProvider/ProfilerEventProvider.h" +#include "../Utilities/ClrData.h" +#include +#include "Stack.h" + +/// +/// Represents callstack information. +/// Note that we serialize enough Clr metadata to reconstruct the names in dotnet-monitor. The stacks themselves are +/// offsets and FunctionId's. +/// +class StacksEventProvider +{ + public: + static HRESULT CreateProvider(ICorProfilerInfo12* profilerInfo, std::unique_ptr& eventProvider); + + HRESULT WriteCallstack(const Stack& stack); + HRESULT WriteClassData(ClassID classId, const ClassData& classData); + HRESULT WriteFunctionData(FunctionID functionId, const FunctionData& classData); + HRESULT WriteModuleData(ModuleID moduleId, const ModuleData& classData); + HRESULT WriteTokenData(ModuleID moduleId, mdTypeDef typeDef, const TokenData& tokenData); + HRESULT WriteEndEvent(); + + private: + StacksEventProvider(ICorProfilerInfo12* profilerInfo, std::unique_ptr & eventProvider) : + _profilerInfo(profilerInfo), _provider(std::move(eventProvider)) + { + } + + static const WCHAR* ProviderName; + + HRESULT DefineEvents(); + + ComPtr _profilerInfo; + std::unique_ptr _provider; + + const WCHAR* CallstackPayloads[3] = { _T("ThreadId"), _T("FunctionIds"), _T("IpOffsets") }; + std::unique_ptr, std::vector>> _callstackEvent; + + //Note we will either send a ClassId or a ClassToken. For Shared generic functions, there is no ClassID. + const WCHAR* FunctionPayloads[6] = { _T("FunctionId"), _T("ClassId"), _T("ClassToken"), _T("ModuleId"), _T("Name"), _T("TypeArgs") }; + std::unique_ptr>> _functionEvent; + + //We cannot retrieve detailed information for some ClassIds. Flags is used to indicate these conditions. + const WCHAR* ClassPayloads[5] = { _T("ClassId"), _T("ModuleId"), _T("Token"), _T("Flags"), _T("TypeArgs") }; + std::unique_ptr>> _classEvent; + + const WCHAR* TokenPayloads[4] = { _T("ModuleId"), _T("Token"), _T("OuterToken"), _T("Name") }; + std::unique_ptr> _tokenEvent; + + const WCHAR* ModulePayloads[2] = { _T("ModuleId"), _T("Name") }; + std::unique_ptr> _moduleEvent; + + //TODO Once ProfilerEvent supports it, use an event with no payload. + const WCHAR* EndPayloads[1] = { _T("Unused") }; + std::unique_ptr> _endEvent; +}; diff --git a/src/MonitorProfiler/Utilities/ClrData.h b/src/MonitorProfiler/Utilities/ClrData.h new file mode 100644 index 00000000000..208d52a50d4 --- /dev/null +++ b/src/MonitorProfiler/Utilities/ClrData.h @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include +#include "cor.h" +#include "corprof.h" +#include "tstring.h" + +class ModuleData +{ +public: + ModuleData(tstring&& name) : + _moduleName(name) + { + } + + const tstring& GetName() const { return _moduleName; } + +private: + tstring _moduleName; +}; + +enum class ClassFlags : UINT32 +{ + None = 0, + Array, + Composite, + IncompleteData, + Error = 0xff +}; + +class ClassData +{ +public: + ClassData(ModuleID moduleId, mdTypeDef token, ClassFlags flags) : + _moduleId(moduleId), _token(token), _flags(flags) + { + } + + const ModuleID GetModuleId() const { return _moduleId; } + const mdTypeDef GetToken() const { return _token; } + const ClassFlags GetFlags() const { return _flags; } + const std::vector& GetTypeArgs() const { return _typeArgs; } + void AddTypeArg(ClassID id) { _typeArgs.push_back(static_cast(id)); } + +private: + ModuleID _moduleId; + mdTypeDef _token; + ClassFlags _flags; + std::vector _typeArgs; +}; + +class TokenData +{ +public: + TokenData(tstring&& name, mdTypeDef outerClass) : _name(name), _outerClass(outerClass) + { + } + + const tstring& GetName() const { return _name; } + const mdTypeDef& GetOuterToken() const { return _outerClass; } +private: + tstring _name; + mdTypeDef _outerClass; +}; + +class FunctionData +{ +public: + FunctionData(ModuleID moduleId, ClassID containingClass, tstring&& name, mdTypeDef classToken) : + _moduleId(moduleId), _class(containingClass), _functionName(name), _classToken(classToken) + { + } + + const ModuleID GetModuleId() const { return _moduleId; } + const tstring& GetName() const { return _functionName; } + const ClassID GetClass() const { return _class; } + const mdTypeDef GetClassToken() const { return _classToken; } + const std::vector& GetTypeArgs() const { return _typeArgs; } + void AddTypeArg(ClassID classID) { _typeArgs.push_back(static_cast(classID)); } + +private: + ModuleID _moduleId; + ClassID _class; + tstring _functionName; + mdTypeDef _classToken; + std::vector _typeArgs; +}; \ No newline at end of file diff --git a/src/MonitorProfiler/Utilities/NameCache.cpp b/src/MonitorProfiler/Utilities/NameCache.cpp new file mode 100644 index 00000000000..564be417690 --- /dev/null +++ b/src/MonitorProfiler/Utilities/NameCache.cpp @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "NameCache.h" +#include "macros.h" +#include "corhlpr.h" + +const tstring NameCache::CompositeClassName = _T("_CompositeClass_"); +const tstring NameCache::ArrayClassName = _T("_ArrayClass_"); +const tstring NameCache::UnknownName = _T("_Unknown_"); +const tstring NameCache::ModuleSeparator = _T("!"); +const tstring NameCache::FunctionSeparator = _T("."); +const tstring NameCache::NestedSeparator = _T("+"); +const tstring NameCache::GenericBegin = _T("<"); +const tstring NameCache::GenericSeparator = _T(","); +const tstring NameCache::GenericEnd = _T(">"); + +bool NameCache::TryGetFunctionData(FunctionID id, std::shared_ptr& data) +{ + return GetData(_functionNames, id, data); +} +bool NameCache::TryGetClassData(ClassID id, std::shared_ptr& data) +{ + return GetData(_classNames, id, data); +} +bool NameCache::TryGetModuleData(ModuleID id, std::shared_ptr& data) +{ + return GetData(_moduleNames, id, data); +} +bool NameCache::TryGetTokenData(ModuleID modId, mdTypeDef token, std::shared_ptr& data) +{ + auto const& it = _names.find(std::make_pair(modId, token)); + + if (it != _names.end()) + { + data = it->second; + return true; + } + return false; +} + +void NameCache::AddModuleData(ModuleID moduleId, tstring&& name) +{ + _moduleNames.emplace(moduleId, std::make_shared(std::move(name))); +} + +HRESULT NameCache::GetFullyQualifiedName(FunctionID id, tstring& name) +{ + HRESULT hr; + + if (id == 0) + { + return E_INVALIDARG; + } + + std::shared_ptr functionData; + if (!TryGetFunctionData(id, functionData)) + { + return E_NOT_SET; + } + + //TODO Consider making each naming function append to a sstream and consider adding flags to disable certain parts of the name such as the module. + // Currently some functions append name information while others assign it. + if (functionData->GetClass() != 0) + { + IfFailRet(GetFullyQualifiedClassName(functionData->GetClass(), name)); + } + else + { + IfFailRet(GetFullyQualifiedClassName(functionData->GetModuleId(), functionData->GetClassToken(), name)); + } + + name += FunctionSeparator + functionData->GetName(); + + IfFailRet(GetGenericParameterNames(functionData->GetTypeArgs(), name)); + + std::shared_ptr moduleData; + if (TryGetModuleData(functionData->GetModuleId(), moduleData)) + { + name = moduleData->GetName() + ModuleSeparator + name; + } + + return S_OK; +} + +HRESULT NameCache::GetFullyQualifiedClassName(ClassID classId, tstring& name) +{ + HRESULT hr; + + if (classId == 0) + { + return E_INVALIDARG; + } + + std::shared_ptr classData; + if (!TryGetClassData(classId, classData)) + { + return E_NOT_SET; + } + + switch (classData->GetFlags()) + { + case ClassFlags::None: + IfFailRet(GetFullyQualifiedClassName(classData->GetModuleId(), classData->GetToken(), name)); + break; + case ClassFlags::Array: + name = ArrayClassName; + break; + case ClassFlags::Composite: + name = CompositeClassName; + break; + case ClassFlags::IncompleteData: + case ClassFlags::Error: + default: + name = UnknownName; + break; + } + + IfFailRet(GetGenericParameterNames(classData->GetTypeArgs(), name)); + + return S_OK; +} + +HRESULT NameCache::GetFullyQualifiedClassName(ModuleID moduleId, mdTypeDef token, tstring& name) +{ + while (token != 0) + { + std::shared_ptr tokenData; + if (TryGetTokenData(moduleId, token, tokenData)) + { + if (name.size() > 0) + { + name = NestedSeparator + name; + } + name = tokenData->GetName() + name; + token = tokenData->GetOuterToken(); + } + else + { + token = 0; + } + } + + return S_OK; +} + +HRESULT NameCache::GetGenericParameterNames(const std::vector& typeArgs, tstring& name) +{ + HRESULT hr; + + for (size_t i = 0; i < typeArgs.size(); i++) + { + if (i == 0) + { + name += GenericBegin; + } + + tstring genericParamName; + IfFailRet(GetFullyQualifiedClassName(static_cast(typeArgs[i]), genericParamName)); + name += genericParamName; + + if (i == (typeArgs.size() - 1)) + { + name += GenericEnd; + } + else + { + name += GenericSeparator; + } + } + + return S_OK; +} + +const std::unordered_map>& NameCache::GetClasses() +{ + return _classNames; +} + +const std::unordered_map>& NameCache::GetFunctions() +{ + return _functionNames; +} + +const std::unordered_map>& NameCache::GetModules() +{ + return _moduleNames; +} + +const std::unordered_map, std::shared_ptr, PairHash>& NameCache::GetTypeNames() +{ + return _names; +} + +void NameCache::AddFunctionData(ModuleID moduleId, FunctionID id, tstring&& name, ClassID parent, mdTypeDef parentToken, ClassID* typeArgs, int typeArgsCount) +{ + std::shared_ptr functionData = std::make_shared(moduleId, parent, std::move(name), parentToken); + for (int i = 0; i < typeArgsCount; i++) + { + functionData->AddTypeArg(typeArgs[i]); + } + _functionNames.emplace(id, functionData); +} + +void NameCache::AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, ClassFlags flags, ClassID* typeArgs, int typeArgsCount) +{ + std::shared_ptr classData = std::make_shared(moduleId, typeDef, flags); + for (int i = 0; i < typeArgsCount; i++) + { + classData->AddTypeArg(typeArgs[i]); + } + _classNames.emplace(id, classData); +} + +void NameCache::AddTokenData(ModuleID moduleId, mdTypeDef typeDef, mdTypeDef outerToken, tstring&& name) +{ + std::shared_ptr tokenData = std::make_shared(std::move(name), outerToken); + + _names.emplace(std::make_pair(moduleId, typeDef), tokenData); +} diff --git a/src/MonitorProfiler/Utilities/NameCache.h b/src/MonitorProfiler/Utilities/NameCache.h new file mode 100644 index 00000000000..448987876f6 --- /dev/null +++ b/src/MonitorProfiler/Utilities/NameCache.h @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "cor.h" +#include "corprof.h" +#include "tstring.h" +#include "ClrData.h" +#include "PairHash.h" +#include +#include +#include +#include + +/// +/// Stores mappings between Clr objects and their names. +/// +class NameCache +{ +public: + bool TryGetFunctionData(FunctionID id, std::shared_ptr& data); + bool TryGetClassData(ClassID id, std::shared_ptr& data); + bool TryGetModuleData(ModuleID id, std::shared_ptr& data); + bool TryGetTokenData(ModuleID modId, mdTypeDef token, std::shared_ptr& data); + + void AddModuleData(ModuleID moduleId, tstring&& name); + void AddFunctionData(ModuleID moduleId, FunctionID id, tstring&& name, ClassID parent, mdTypeDef parentToken, ClassID* typeArgs, int typeArgsCount); + void AddClassData(ModuleID moduleId, ClassID id, mdTypeDef typeDef, ClassFlags flags, ClassID* typeArgs, int typeArgsCount); + void AddTokenData(ModuleID moduleId, mdTypeDef typeDef, mdTypeDef outerToken, tstring&& name); + + HRESULT GetFullyQualifiedName(FunctionID id, tstring& name); + HRESULT GetFullyQualifiedClassName(ClassID classId, tstring& name); + HRESULT GetFullyQualifiedClassName(ModuleID moduleId, mdTypeDef token, tstring& name); + HRESULT GetGenericParameterNames(const std::vector& typeArgs, tstring& name); + + const std::unordered_map>& GetClasses(); + const std::unordered_map>& GetFunctions(); + const std::unordered_map>& GetModules(); + const std::unordered_map, std::shared_ptr, PairHash>& GetTypeNames(); + +private: + static const tstring CompositeClassName; + static const tstring ArrayClassName; + static const tstring UnknownName; + static const tstring ModuleSeparator; + static const tstring FunctionSeparator; + static const tstring NestedSeparator; + static const tstring GenericBegin; + static const tstring GenericSeparator; + static const tstring GenericEnd; + + template + bool GetData(std::unordered_map> map, T id, std::shared_ptr& name); + + std::unordered_map> _classNames; + std::unordered_map> _functionNames; + std::unordered_map> _moduleNames; + std::unordered_map, std::shared_ptr, PairHash> _names; +}; + +template +bool NameCache::GetData(std::unordered_map> map, T id, std::shared_ptr& name) +{ + typename std::unordered_map>::iterator it = map.find(id); + + if (it != map.end()) + { + name = it->second; + return true; + } + return false; +} \ No newline at end of file diff --git a/src/MonitorProfiler/Utilities/PairHash.h b/src/MonitorProfiler/Utilities/PairHash.h new file mode 100644 index 00000000000..316ad5009fc --- /dev/null +++ b/src/MonitorProfiler/Utilities/PairHash.h @@ -0,0 +1,21 @@ +// Licensed to the.NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +template +struct PairHash +{ + size_t operator()(const std::pair& pair) const + { + std::hash first; + size_t firstResult = first(pair.first); + + std::hash second; + size_t secondResult = second(pair.second); + + //TODO Use a better hash merging algorithm + return firstResult ^ secondResult; + } +}; \ No newline at end of file diff --git a/src/MonitorProfiler/Utilities/StringUtilities.h b/src/MonitorProfiler/Utilities/StringUtilities.h new file mode 100644 index 00000000000..101d2a18be2 --- /dev/null +++ b/src/MonitorProfiler/Utilities/StringUtilities.h @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include + +class StringUtilities +{ + public: + template + static HRESULT Copy(char (&destination)[DestinationSize], const char* source) + { + //Note both strncpy_s and strlcpy stop early if the source has a \0 terminator + +#if TARGET_WINDOWS + int result = strncpy_s(destination, DestinationSize, source, DestinationSize - 1); + if (result != 0) + { + return HRESULT_FROM_WIN32(result); + } +#elif defined(TARGET_LINUX) && !defined(TARGET_ALPINE_LINUX) + //TODO Glibc does not support the recommened string copy functions + strncpy(destination, source, DestinationSize); +#else + if (strlcpy(destination, source, DestinationSize) >= DestinationSize) + { + return E_INVALIDARG; + } +#endif + + return S_OK; + } +}; \ No newline at end of file diff --git a/src/MonitorProfiler/Utilities/TypeNameUtilities.cpp b/src/MonitorProfiler/Utilities/TypeNameUtilities.cpp new file mode 100644 index 00000000000..c9831c13e82 --- /dev/null +++ b/src/MonitorProfiler/Utilities/TypeNameUtilities.cpp @@ -0,0 +1,258 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#include "TypeNameUtilities.h" +#include "corhlpr.h" + +TypeNameUtilities::TypeNameUtilities(ICorProfilerInfo12* profilerInfo) : _profilerInfo(profilerInfo) +{ +} + +HRESULT TypeNameUtilities::CacheNames(NameCache& nameCache, ClassID classId) +{ + std::shared_ptr classData; + if (!nameCache.TryGetClassData(classId, classData)) + { + return GetClassInfo(nameCache, classId); + } + + return S_OK; +} + +HRESULT TypeNameUtilities::CacheNames(NameCache& nameCache, FunctionID functionId, COR_PRF_FRAME_INFO frameInfo) +{ + std::shared_ptr functionData; + if (!nameCache.TryGetFunctionData(functionId, functionData)) + { + return GetFunctionInfo(nameCache, functionId, frameInfo); + } + + return S_OK; +} + +HRESULT TypeNameUtilities::GetFunctionInfo(NameCache& nameCache, FunctionID id, COR_PRF_FRAME_INFO frameInfo) +{ + if (id == 0) + { + return E_INVALIDARG; + } + + ClassID classId = 0; + ModuleID moduleId = 0; + mdToken token = mdTokenNil; + ULONG32 typeArgsCount = 0; + ClassID typeArgs[32]; + HRESULT hr; + + IfFailRet(_profilerInfo->GetFunctionInfo2(id, + frameInfo, + &classId, + &moduleId, + &token, + sizeof(typeArgs) / sizeof(ClassID), + &typeArgsCount, + typeArgs)); + + ComPtr pIMDImport; + IfFailRet(_profilerInfo->GetModuleMetaData(moduleId, + ofRead, + IID_IMetaDataImport, + (IUnknown**)&pIMDImport)); + + //TODO Convert this to dynamically allocate the needed size. + WCHAR funcName[256]; + mdTypeDef classToken = mdTypeDefNil; + IfFailRet(pIMDImport->GetMethodProps(token, + &classToken, + funcName, + 256, + 0, + 0, + NULL, + NULL, + NULL, + NULL)); + + IfFailRet(GetModuleInfo(nameCache, moduleId)); + + nameCache.AddFunctionData(moduleId, id, tstring(funcName), classId, classToken, typeArgs, typeArgsCount); + + // If the ClassID returned from GetFunctionInfo is 0, then the function is a shared generic function. + if (classId != 0) + { + IfFailRet(GetClassInfo(nameCache, classId)); + } + else + { + IfFailRet(GetTypeDefName(nameCache, moduleId, classToken)); + } + + for (ULONG32 i = 0; i < typeArgsCount; i++) + { + if (typeArgs[i] != 0) + { + IfFailRet(GetClassInfo(nameCache, typeArgs[i])); + } + } + + return S_OK; +} + +HRESULT TypeNameUtilities::GetClassInfo(NameCache& nameCache, ClassID classId) +{ + if (classId == 0) + { + return E_INVALIDARG; + } + + std::shared_ptr classData; + if (nameCache.TryGetClassData(classId, classData)) + { + return S_OK; + } + + ModuleID modId = 0; + mdTypeDef classToken = mdTokenNil; + ULONG32 typeArgsCount = 0; + ClassID typeArgs[32]; + HRESULT hr = S_OK; + ClassFlags flags = ClassFlags::None; + + IfFailRet(_profilerInfo->GetClassIDInfo2(classId, + &modId, + &classToken, + nullptr, + 32, + &typeArgsCount, + typeArgs)); + + if (CORPROF_E_CLASSID_IS_ARRAY == hr) + { + flags = ClassFlags::Array; + } + else if (CORPROF_E_CLASSID_IS_COMPOSITE == hr) + { + // We have a composite class + flags = ClassFlags::Composite; + } + else if (CORPROF_E_DATAINCOMPLETE == hr) + { + // type-loading is not yet complete. Cannot do anything about it. + flags = ClassFlags::IncompleteData; + } + else if (FAILED(hr)) + { + flags = ClassFlags::Error; + } + + if (flags == ClassFlags::None) + { + IfFailRet(GetTypeDefName(nameCache, modId, classToken)); + + for (ULONG32 i = 0; i < typeArgsCount; i++) + { + if (typeArgs[i] != 0) + { + IfFailRet(GetClassInfo(nameCache, typeArgs[i])); + } + } + } + + nameCache.AddClassData(modId, classId, classToken, flags, typeArgs, typeArgsCount); + + return S_OK; +} + +HRESULT TypeNameUtilities::GetTypeDefName(NameCache& nameCache, ModuleID moduleId, mdTypeDef classToken) +{ + HRESULT hr; + ComPtr pMDImport; + IfFailRet(_profilerInfo->GetModuleMetaData(moduleId, + (ofRead | ofWrite), + IID_IMetaDataImport2, + (IUnknown**)&pMDImport)); + + mdToken tokenToProcess = classToken; + while (tokenToProcess != mdTokenNil) + { + std::shared_ptr tokenData; + if (nameCache.TryGetTokenData(moduleId, tokenToProcess, tokenData)) + { + //We already processed this type (and therefore all of its outer classes) + break; + } + + WCHAR wName[256]; + DWORD dwTypeDefFlags = 0; + IfFailRet(pMDImport->GetTypeDefProps(tokenToProcess, + wName, + 256, + NULL, + &dwTypeDefFlags, + NULL)); + + mdTypeDef outerTokenType = mdTokenNil; + if (IsTdNested(dwTypeDefFlags)) + { + IfFailRet(pMDImport->GetNestedClassProps(tokenToProcess, &outerTokenType)); + } + nameCache.AddTokenData(moduleId, tokenToProcess, outerTokenType, tstring(wName)); + tokenToProcess = outerTokenType; + } + + return S_OK; +} + +HRESULT TypeNameUtilities::GetModuleInfo(NameCache& nameCache, ModuleID moduleId) +{ + if (moduleId == 0) + { + return E_INVALIDARG; + } + + HRESULT hr; + + std::shared_ptr mod; + if (nameCache.TryGetModuleData(moduleId, mod)) + { + return S_OK; + } + + WCHAR moduleFullName[256]; + ULONG nameLength = 0; + AssemblyID assemID; + + IfFailRet(_profilerInfo->GetModuleInfo(moduleId, + nullptr, + 256, + &nameLength, + moduleFullName, + &assemID)); + + WCHAR* ptr = nullptr; + + int pathSeparatorIndex = nameLength - 1; + while (pathSeparatorIndex >= 0) + { + if (moduleFullName[pathSeparatorIndex] == '\\' || moduleFullName[pathSeparatorIndex] == '/') + { + break; + } + pathSeparatorIndex--; + } + + tstring moduleName; + if (pathSeparatorIndex < 0) + { + moduleName = moduleFullName; + } + else + { + moduleName = tstring(moduleFullName, pathSeparatorIndex + 1, nameLength - pathSeparatorIndex - 1); + } + + nameCache.AddModuleData(moduleId, std::move(moduleName)); + + return S_OK; +} diff --git a/src/MonitorProfiler/Utilities/TypeNameUtilities.h b/src/MonitorProfiler/Utilities/TypeNameUtilities.h new file mode 100644 index 00000000000..ca630aa5d74 --- /dev/null +++ b/src/MonitorProfiler/Utilities/TypeNameUtilities.h @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once + +#include "cor.h" +#include "corprof.h" +#include "com.h" +#include "tstring.h" +#include "NameCache.h" + +/// +/// Retrieves the names of functions and stores them into a cache. +/// +class TypeNameUtilities +{ + public: + TypeNameUtilities(ICorProfilerInfo12* profilerInfo); + HRESULT CacheNames(NameCache& nameCache, ClassID classId); + HRESULT CacheNames(NameCache& nameCache, FunctionID functionId, COR_PRF_FRAME_INFO frameInfo); + private: + HRESULT GetFunctionInfo(NameCache& nameCache, FunctionID id, COR_PRF_FRAME_INFO frameInfo); + HRESULT GetClassInfo(NameCache& nameCache, ClassID classId); + HRESULT GetModuleInfo(NameCache& nameCache, ModuleID moduleId); + HRESULT GetTypeDefName(NameCache& nameCache, ModuleID moduleId, mdTypeDef classToken); + private: + ComPtr _profilerInfo; +}; diff --git a/src/Tests/Directory.Build.props b/src/Tests/Directory.Build.props index 672cf750cf1..9bc549ebd25 100644 --- a/src/Tests/Directory.Build.props +++ b/src/Tests/Directory.Build.props @@ -1,14 +1,8 @@ - - - $(MicrosoftNETCoreApp31Version) - $(MicrosoftNETCoreApp50Version) - + + + + + \ No newline at end of file diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs index 7540213983c..f49a79e5ed2 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.UnitTests/SchemaGenerationTests.cs @@ -112,7 +112,7 @@ private static void PrintSection(ITestOutputHelper outputHelper, string header, int formatQty = (endLine + 1).ToString("D").Length; // Get the length of the biggest number (add 1 for the 1-based index) for (int i = startLine; i <= endLine; i++) { - outputHelper.WriteLine("{0}:{1}{2}", (i+1).ToString("D" + formatQty.ToString(CultureInfo.InvariantCulture)), (i == lineHighlighted) ? " >" : " ", lines[i]); + outputHelper.WriteLine("{0}:{1}{2}", (i + 1).ToString("D" + formatQty.ToString(CultureInfo.InvariantCulture)), (i == lineHighlighted) ? " >" : " ", lines[i]); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/ExperimentalSchemaProcessor.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/ExperimentalSchemaProcessor.cs new file mode 100644 index 00000000000..8bb28b61912 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/ExperimentalSchemaProcessor.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.Options; +using NJsonSchema.Generation; +using System.Reflection; + +namespace Microsoft.Diagnostics.Monitoring.ConfigurationSchema +{ + internal class ExperimentalSchemaProcessor : ISchemaProcessor + { + private const string ExperimentalPrefix = "[Experimental]"; + + public void Process(SchemaProcessorContext context) + { + foreach (PropertyInfo property in context.ContextualType.Type.GetProperties()) + { + if (null != property.GetCustomAttribute()) + { + string description = context.Schema.Properties[property.Name].Description; + if (string.IsNullOrEmpty(description)) + { + description = ExperimentalPrefix; + } + else + { + description = $"{ExperimentalPrefix} {description}"; + } + context.Schema.Properties[property.Name].Description = description; + } + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj index 6d7d540f7d9..f4e45fe8ec9 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Microsoft.Diagnostics.Monitoring.ConfigurationSchema.csproj @@ -8,19 +8,26 @@ + + + + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleFormatterOptions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleFormatterOptions.cs index 2c8d329b936..2b99b1e063b 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleFormatterOptions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleFormatterOptions.cs @@ -3,8 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.WebApi; -using System.ComponentModel.DataAnnotations; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Microsoft.Diagnostics.Monitoring.Options { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleLoggerOptions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleLoggerOptions.cs index da961a7c4d0..1ceb4c1ce14 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleLoggerOptions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/Options/ConsoleLoggerOptions.cs @@ -3,10 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.WebApi; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel; using Microsoft.Extensions.Logging; using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace Microsoft.Diagnostics.Monitoring.Options { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/SchemaGenerator.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/SchemaGenerator.cs index b5014d353f5..d8408340829 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/SchemaGenerator.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ConfigurationSchema/SchemaGenerator.cs @@ -125,7 +125,9 @@ private static void AddCollectionRuleSchemas(GenerationContext context) AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.CollectDump); AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.CollectGCDump); + AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.CollectLiveMetrics); AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.CollectLogs); + AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.CollectStacks); AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.CollectTrace); AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.Execute); AddCollectionRuleActionSchema(context, actionTypeSchema, KnownCollectionRuleActions.LoadProfiler); @@ -275,6 +277,7 @@ public GenerationContext(JsonSchema rootSchema) _settings = new JsonSchemaGeneratorSettings(); _settings.SerializerSettings = new JsonSerializerSettings(); _settings.SerializerSettings.Converters.Add(new StringEnumConverter()); + _settings.SchemaProcessors.Add(new ExperimentalSchemaProcessor()); _resolver = new JsonSchemaResolver(rootSchema, _settings); @@ -294,4 +297,4 @@ public void SetRoot() public JsonSchema Schema { get; } } } -} \ No newline at end of file +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.ExecuteActionApp/Microsoft.Diagnostics.Monitoring.ExecuteActionApp.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.ExecuteActionApp/Microsoft.Diagnostics.Monitoring.ExecuteActionApp.csproj index 74c01a46cdc..ff77797ad09 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.ExecuteActionApp/Microsoft.Diagnostics.Monitoring.ExecuteActionApp.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.ExecuteActionApp/Microsoft.Diagnostics.Monitoring.ExecuteActionApp.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs index 24ca743d2d0..6b329f727c3 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen.UnitTests/OpenApiGeneratorTests.cs @@ -103,7 +103,6 @@ private async Task GenerateDocumentAsync() string path = Path.GetTempFileName(); DotNetRunner runner = new(); - runner.FrameworkReference = DotNetFrameworkReference.Microsoft_AspNetCore_App; runner.EntrypointAssemblyPath = OpenApiGenPath; runner.Arguments = path; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs index 6ece8ac544e..b6e8a804ffd 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/Program.cs @@ -34,7 +34,7 @@ public static void Main(string[] args) throw new InvalidOperationException("Expected single argument for the output path."); } string outputPath = args[0]; - + // Create directory if it does not exist Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); @@ -43,7 +43,8 @@ public static void Main(string[] args) metricUrls: null, metrics: false, diagnosticPort: null, - authConfiguration: HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false)); + authConfiguration: HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false), + userProvidedConfigFilePath: null); // Create all of the same services as dotnet-monitor and add // OpenAPI generation in order to have it inspect the ASP.NET Core diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/RemoveFailureContentTypesOperationFilter.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/RemoveFailureContentTypesOperationFilter.cs index c137de5b7cd..6ae585a0f98 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/RemoveFailureContentTypesOperationFilter.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/RemoveFailureContentTypesOperationFilter.cs @@ -1,9 +1,7 @@ using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -using System; using System.Collections.Generic; -using System.Text; namespace Microsoft.Diagnostics.Monitoring.OpenApiGen { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseDocumentFilter.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseDocumentFilter.cs index 0903a0d755f..00376e059c3 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseDocumentFilter.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseDocumentFilter.cs @@ -7,10 +7,6 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -using System; -using System.Collections.Generic; -using System.Text; - namespace Microsoft.Diagnostics.Monitoring.OpenApiGen { internal sealed class TooManyRequestsResponseDocumentFilter : IDocumentFilter diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseOperationFilter.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseOperationFilter.cs index 21fd6ed54d5..12fe559988a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseOperationFilter.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.OpenApiGen/TooManyRequestsResponseOperationFilter.cs @@ -4,9 +4,6 @@ using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; -using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.Diagnostics.Monitoring.OpenApiGen { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.csproj new file mode 100644 index 00000000000..76abd0e10ce --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.csproj @@ -0,0 +1,12 @@ + + + + Exe + net6.0 + + + + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Program.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Program.cs new file mode 100644 index 00000000000..3131a90a4df --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Program.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp +{ + internal class Program + { + public static Task Main(string[] args) + { + return new CommandLineBuilder(new RootCommand() + { + ExceptionThrowCatchScenario.Command(), + ExceptionThrowCrashScenario.Command() + }) + .Build() + .InvokeAsync(args); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Scenarios/ExceptionThrowCatchScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Scenarios/ExceptionThrowCatchScenario.cs new file mode 100644 index 00000000000..efbc2f37dfd --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Scenarios/ExceptionThrowCatchScenario.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.CommandLine; + +namespace Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios +{ + /// + /// Async waits until it receives the Continue command. + /// + internal class ExceptionThrowCatchScenario + { + public static Command Command() + { + Command command = new("ExceptionThrowCatch"); + command.SetHandler(Execute); + return command; + } + + public static void Execute() + { + ThrowCatch(); + ThrowCatchDeep(); + ThrowCatchRethrowCatch(); + } + + private static void ThrowCatch() + { + try + { + throw new InvalidOperationException(); + } + catch + { + } + } + + private static void ThrowCatchDeep() + { + try + { + ThrowCatchDeepInner(); + } + catch + { + } + } + + private static void ThrowCatchDeepInner() + { + Throw(); + } + + private static void ThrowCatchRethrowCatch() + { + try + { + ThrowCatchRethrowCatchInner(); + } + catch + { + } + } + + private static void ThrowCatchRethrowCatchInner() + { + try + { + Throw(); + } + catch + { + throw; + } + } + + private static void Throw() + { + throw new InvalidOperationException(); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Scenarios/ExceptionThrowCrashScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Scenarios/ExceptionThrowCrashScenario.cs new file mode 100644 index 00000000000..d81a52c4d63 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp/Scenarios/ExceptionThrowCrashScenario.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.CommandLine; + +namespace Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios +{ + /// + /// Async waits until it receives the Continue command. + /// + internal class ExceptionThrowCrashScenario + { + public static Command Command() + { + Command command = new("ExceptionThrowCrash"); + command.SetHandler(Execute); + return command; + } + + public static void Execute() + { + throw new InvalidOperationException(); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Baselines/ExceptionThrowCatch.txt b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Baselines/ExceptionThrowCatch.txt new file mode 100644 index 00000000000..496d82ecab9 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Baselines/ExceptionThrowCatch.txt @@ -0,0 +1,12 @@ +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.ThrowCatch +[profiler]dbug: Exception handled: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.ThrowCatch +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.Throw +[profiler]dbug: Exception handled: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.ThrowCatchDeep +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.Throw +[profiler]dbug: Exception handled: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.ThrowCatchRethrowCatchInner +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.ThrowCatchRethrowCatchInner +[profiler]dbug: Exception handled: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCatchScenario.ThrowCatchRethrowCatch diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Baselines/ExceptionThrowCrash.txt b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Baselines/ExceptionThrowCrash.txt new file mode 100644 index 00000000000..d164bac90c7 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Baselines/ExceptionThrowCrash.txt @@ -0,0 +1,17 @@ +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Scenarios.ExceptionThrowCrashScenario.Execute +[profiler]dbug: Exception handled: System.CommandLine.dll!System.CommandLine.Invocation.AnonymousCommandHandler+d__6.MoveNext +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: System.Private.CoreLib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw +[profiler]dbug: Exception handled: System.CommandLine.dll!System.CommandLine.Parsing.ParseResultExtensions+d__0.MoveNext +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: System.Private.CoreLib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw +[profiler]dbug: Exception handled: System.CommandLine.dll!System.CommandLine.Parsing.ParserExtensions+d__3.MoveNext +[profiler]dbug: Exception thrown: System.InvalidOperationException +[profiler]dbug: Exception method: System.Private.CoreLib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw +[ignore]The following are expected but are not being produced in CI builds at this time. +[ignore][profiler]info: Exception unhandled: System.Private.CoreLib.dll!System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw +[ignore][profiler]info: Exception unhandled: System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess +[ignore][profiler]info: Exception unhandled: System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification +[ignore][profiler]info: Exception unhandled: System.Private.CoreLib.dll!System.Runtime.CompilerServices.TaskAwaiter`1.GetResult +[ignore][profiler]info: Exception unhandled: Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.dll!Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp.Program.
diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/ExceptionTrackingTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/ExceptionTrackingTests.cs new file mode 100644 index 00000000000..14478b8d258 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/ExceptionTrackingTests.cs @@ -0,0 +1,179 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.Diagnostics.Monitoring.Profiler.UnitTests +{ + public sealed class ExceptionTrackingTests + { + private const string IgnoreOutputPrefix = "[ignore]"; + private const string ProfilerOutputPrefix = "[profiler]"; + + private const string BaselinesFolderName = "Baselines"; + private const string OutputsFolderName = "Outputs"; + + private readonly ITestOutputHelper _outputHelper; + + public ExceptionTrackingTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Theory(Skip = "Exception tracking via profiler is currently disabled")] + [MemberData(nameof(ProfilerHelper.GetArchitectureProfilerPath), MemberType = typeof(ProfilerHelper))] + public Task ExceptionThrowCatch(Architecture architecture, string profilerPath) + { + return RunAndCompare(nameof(ExceptionThrowCatch), architecture, profilerPath); + } + + [Theory(Skip = "Exception tracking via profiler is currently disabled")] + [MemberData(nameof(ProfilerHelper.GetArchitectureProfilerPath), MemberType = typeof(ProfilerHelper))] + public Task ExceptionThrowCrash(Architecture architecture, string profilerPath) + { + return RunAndCompare(nameof(ExceptionThrowCrash), architecture, profilerPath); + } + + private async Task RunAndCompare(string scenarioName, Architecture architecture, string profilerPath) + { + ITestOutputHelper appOutputHelper = new PrefixedOutputHelper(_outputHelper, FormattableString.Invariant($"[App] ")); + + using DotNetRunner runner = new(); + runner.Architecture = architecture; + await using LoggingRunnerAdapter adapter = new(appOutputHelper, runner); + + runner.EntrypointAssemblyPath = AssemblyHelper.GetAssemblyArtifactBinPath( + Assembly.GetExecutingAssembly(), + "Microsoft.Diagnostics.Monitoring.Profiler.UnitTestApp"); + runner.Arguments = scenarioName; + + // Environment variables necessary for running the profiler + enable all logging to stderr + adapter.Environment.Add(ProfilerHelper.ClrEnvVarEnableNotificationProfilers, ProfilerHelper.ClrEnvVarEnabledValue); + adapter.Environment.Add(ProfilerHelper.ClrEnvVarEnableProfiling, ProfilerHelper.ClrEnvVarEnabledValue); + adapter.Environment.Add(ProfilerHelper.ClrEnvVarProfiler, ProfilerIdentifiers.Clsid.StringWithBraces); + adapter.Environment.Add(ProfilerHelper.ClrEnvVarProfilerPath, profilerPath); + adapter.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.RuntimeInstanceId, Guid.NewGuid().ToString("D")); + adapter.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.StdErrLogger_Level, LogLevel.Trace.ToString("G")); + + List outputLines = new(); + + Action receivedStdErrLine = (line) => + { + if (!string.IsNullOrEmpty(line)) + { + // Only care to capture lines that start with "[profiler]". Other lines + // will have "[ignore]" prepended to allow for capture, but ignored during + // analysis. + if (line.StartsWith(ProfilerOutputPrefix, StringComparison.Ordinal)) + { + outputLines.Add(line); + } + else + { + outputLines.Add($"{IgnoreOutputPrefix}{line}"); + } + } + }; + + adapter.ReceivedStandardErrorLine += receivedStdErrLine; + + using CancellationTokenSource startTokenSource = new(CommonTestTimeouts.StartProcess); + await adapter.StartAsync(startTokenSource.Token); + + using CancellationTokenSource waitForExitSource = new(CommonTestTimeouts.WaitForExit); + await adapter.ReadToEnd(waitForExitSource.Token); + + adapter.ReceivedStandardErrorLine -= receivedStdErrLine; + + string fileName = $"{scenarioName}.txt"; + + Directory.CreateDirectory(OutputsFolderName); + File.WriteAllLines(Path.Combine(OutputsFolderName, fileName), outputLines); + + string[] baselineLines = File.ReadAllLines(Path.Combine(BaselinesFolderName, fileName)); + + try + { + // The current index in the list of lines + int baselineIndex = 0; + int outputIndex = 0; + + // The count of non-ignored lines + int baselineCount = 0; + int outputCount = 0; + + // The current line value + string baselineLine = null; + string outputLine = null; + + // Try to read a line from each list and compare them + while (TryReadNextLine(baselineLines, out baselineLine, ref baselineIndex, ref baselineCount) && + TryReadNextLine(outputLines, out outputLine, ref outputIndex, ref outputCount)) + { + try + { + Assert.Equal(baselineLine, outputLine); + } + catch (XunitException) + { + // baselineIndex is already incremented, thus in terms of line numbers, it is already correct + _outputHelper.WriteLine($"Difference on line {baselineIndex} of baseline."); + + throw; + } + } + + // Read the remaining lines from each in order to get the total line count + while (TryReadNextLine(baselineLines, out baselineLine, ref baselineIndex, ref baselineCount)) { } + while (TryReadNextLine(outputLines, out outputLine, ref outputIndex, ref outputCount)) { } + + // The total unignored line count should be equal + Assert.Equal(baselineCount, outputCount); + } + catch (XunitException) + { + _outputHelper.WriteLine("=== Begin Output ==="); + for (int index = 0; index < outputLines.Count; index++) + { + _outputHelper.WriteLine(outputLines[index]); + } + _outputHelper.WriteLine("=== End Output ====="); + + throw; + } + } + + private static bool TryReadNextLine(IReadOnlyList lines, out string line, ref int index, ref int lineCount) + { + line = null; + while (index < lines.Count) + { + string candidate = lines[index]; + index++; + + // Anything that doesn't start with "[ignore]" is considered a valid line for analysis + if (string.IsNullOrEmpty(candidate) || !candidate.StartsWith(IgnoreOutputPrefix)) + { + lineCount++; + + line = candidate; + return true; + } + } + return false; + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests.csproj new file mode 100644 index 00000000000..11ca0631ce6 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests.csproj @@ -0,0 +1,20 @@ + + + + net6.0;net7.0 + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/ProfilerInitializationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/ProfilerInitializationTests.cs new file mode 100644 index 00000000000..0510da5d0e2 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Profiler.UnitTests/ProfilerInitializationTests.cs @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Profiler.UnitTests +{ + public sealed class ProfilerInitializationTests + { + private readonly ITestOutputHelper _outputHelper; + + public ProfilerInitializationTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitectureProfilerPath), MemberType = typeof(ProfilerHelper))] + public async Task LoadAtStart(Architecture architecture, string profilerPath) + { + await using AppRunner runner = new(_outputHelper, Assembly.GetExecutingAssembly()); + runner.Architecture = architecture; + runner.ScenarioName = TestAppScenarios.AsyncWait.Name; + + // Environment variables necessary for running the profiler + enable all logging to stderr + string runtimeInstanceId = Guid.NewGuid().ToString("D"); + runner.Environment.Add(ProfilerHelper.ClrEnvVarEnableNotificationProfilers, ProfilerHelper.ClrEnvVarEnabledValue); + runner.Environment.Add(ProfilerHelper.ClrEnvVarEnableProfiling, ProfilerHelper.ClrEnvVarEnabledValue); + runner.Environment.Add(ProfilerHelper.ClrEnvVarProfiler, ProfilerIdentifiers.Clsid.StringWithBraces); + runner.Environment.Add(ProfilerHelper.ClrEnvVarProfilerPath, profilerPath); + runner.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.RuntimeInstanceId, runtimeInstanceId); + runner.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.StdErrLogger_Level, LogLevel.Trace.ToString("G")); + + await runner.ExecuteAsync(async () => + { + // At this point, the profiler has already been initialized and managed code is already running. + // Use any of the initialization state of the profiler to validate that it is loaded. + await ProfilerHelper.VerifyProductVersionEnvironmentVariableAsync(runner, _outputHelper); + + VerifySocketPath(Path.GetTempPath(), runtimeInstanceId); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }); + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitectureProfilerPath), MemberType = typeof(ProfilerHelper))] + public async Task AttachAfterStarted(Architecture architecture, string profilerPath) + { + await using AppRunner runner = new(_outputHelper, Assembly.GetExecutingAssembly()); + runner.Architecture = architecture; + runner.ScenarioName = TestAppScenarios.AsyncWait.Name; + + string runtimeInstanceId = Guid.NewGuid().ToString("D"); + await runner.ExecuteAsync(async () => + { + DiagnosticsClient client = new(await runner.ProcessIdTask); + + client.SetEnvironmentVariable( + ProfilerIdentifiers.EnvironmentVariables.RuntimeInstanceId, + runtimeInstanceId); + + client.SetEnvironmentVariable( + ProfilerIdentifiers.EnvironmentVariables.StdErrLogger_Level, + LogLevel.Trace.ToString("G")); + + // Profiler will attach and initialize before this returns. + // All settings must be applied before issuing attach profiler call. + client.AttachProfiler( + TimeSpan.FromSeconds(10), + ProfilerIdentifiers.Clsid.Guid, + profilerPath); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + + // At this point, the profiler has already been initialized and managed code is already running. + // Use any of the initialization state of the profiler to validate that it is loaded. + await ProfilerHelper.VerifyProductVersionEnvironmentVariableAsync(runner, _outputHelper); + + VerifySocketPath(Path.GetTempPath(), runtimeInstanceId); + }); + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitectureProfilerPath), MemberType = typeof(ProfilerHelper))] + public async Task VerifyCustomSharedPath(Architecture architecture, string profilerPath) + { + using TemporaryDirectory tempDir = new(_outputHelper); + + await using AppRunner runner = new(_outputHelper, Assembly.GetExecutingAssembly()); + runner.Architecture = architecture; + runner.ScenarioName = TestAppScenarios.AsyncWait.Name; + + // Environment variables necessary for running the profiler + enable all logging to stderr + string runtimeInstanceId = Guid.NewGuid().ToString("D"); + runner.Environment.Add(ProfilerHelper.ClrEnvVarEnableNotificationProfilers, ProfilerHelper.ClrEnvVarEnabledValue); + runner.Environment.Add(ProfilerHelper.ClrEnvVarEnableProfiling, ProfilerHelper.ClrEnvVarEnabledValue); + runner.Environment.Add(ProfilerHelper.ClrEnvVarProfiler, ProfilerIdentifiers.Clsid.StringWithBraces); + runner.Environment.Add(ProfilerHelper.ClrEnvVarProfilerPath, profilerPath); + runner.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.RuntimeInstanceId, runtimeInstanceId); + runner.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.SharedPath, tempDir.FullName); + runner.Environment.Add(ProfilerIdentifiers.EnvironmentVariables.StdErrLogger_Level, LogLevel.Trace.ToString("G")); + + await runner.ExecuteAsync(async () => + { + // At this point, the profiler has already been initialized and managed code is already running. + VerifySocketPath(tempDir.FullName, runtimeInstanceId); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }); + } + + private static void VerifySocketPath(string directoryPath, string runtimeInstanceId) + { + string expectedPath = Path.Combine(directoryPath, $"{runtimeInstanceId}.sock"); + Assert.True(File.Exists(expectedPath), $"Expected socket file at '{expectedPath}'."); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs index c279d938f9a..be2971a9f0e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/AssemblyHelper.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Reflection; namespace Microsoft.Diagnostics.Monitoring.TestCommon diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs index ee180365c7d..cedf1749f13 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CommonTestTimeouts.cs @@ -28,6 +28,11 @@ public static class CommonTestTimeouts /// public static readonly TimeSpan TraceTimeout = TimeSpan.FromMinutes(2); + /// + /// Default timeout for live metrics collection. + /// + public static readonly TimeSpan LiveMetricsTimeout = TimeSpan.FromMinutes(2); + /// Default timeout for gcdump collection. /// /// @@ -62,5 +67,15 @@ public static class CommonTestTimeouts /// Default timeout for loading a profiler into a target process. /// public static readonly TimeSpan LoadProfilerTimeout = TimeSpan.FromSeconds(10); + + /// + /// Default timeout for waiting for Azurite to fully initialize. + /// + public static readonly TimeSpan AzuriteInitializationTimeout = TimeSpan.FromSeconds(30); + + /// + /// Default timeout for waiting for Azurite to fully stop. + /// + public static readonly TimeSpan AzuriteTeardownTimeout = TimeSpan.FromSeconds(30); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/ConsoleOutputHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/ConsoleOutputHelper.cs new file mode 100644 index 00000000000..98ae0dcce1e --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/ConsoleOutputHelper.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + public sealed class ConsoleOutputHelper : ITestOutputHelper + { + public ConsoleOutputHelper() + { + } + + public void WriteLine(string message) + { + Console.WriteLine(message); + } + + public void WriteLine(string format, params object[] args) + { + Console.WriteLine(format, args); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Models/CounterPayload.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CounterPayload.cs similarity index 76% rename from src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Models/CounterPayload.cs rename to src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CounterPayload.cs index 8c5cab983ea..c2d1d4bf597 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Models/CounterPayload.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/CounterPayload.cs @@ -2,12 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Text; using System.Text.Json.Serialization; -namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Models +namespace Microsoft.Diagnostics.Monitoring.TestCommon { internal class CounterPayload { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs index 5d196ccfd30..d5f99f87d98 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/DotNetHost.cs @@ -10,6 +10,9 @@ namespace Microsoft.Diagnostics.Monitoring.TestCommon { public partial class DotNetHost { + private static Lazy s_HasHostInRepositoryLazy = + new(() => File.Exists(GetHostFromRepository())); + // The version is in the Major.Minor.Patch-label format; remove the label // and only parse the Major.Minor.Patch part. private static Lazy s_runtimeVersionLazy = @@ -18,13 +21,51 @@ public partial class DotNetHost public static Version RuntimeVersion => s_runtimeVersionLazy.Value; - public static string HostExeNameWithoutExtension => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? - Path.GetFileNameWithoutExtension(HostExePath) : - Path.GetFileName(HostExePath); + public static string ExeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + + public static string ExeNameWithoutExtension => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + Path.GetFileNameWithoutExtension(ExeName) : + ExeName; + + public static bool HasHostInRepository => s_HasHostInRepositoryLazy.Value; + + public static string GetPath(Architecture? arch = null) + { + string hostInRepositoryPath = GetHostFromRepository(arch); + + if (File.Exists(hostInRepositoryPath)) + { + return hostInRepositoryPath; + } + + // If the current repo enlistment has only ever been built and tested with Visual Studio, + // the repo's private copy of dotnet will have never been setup. + // + // In this scenario fall back to the system's copy. + // Limit this fallback behavior to only happen when running under Visual Studio. + // (i.e. when on Windows and a well-defined VS-specific environment variable set) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSAPPIDDIR"))) + { + Console.WriteLine($"'{hostInRepositoryPath}' does not exist, falling back to the system's version."); + return ExeName; + } - public static string HostExePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? - @"..\..\..\..\..\.dotnet\dotnet.exe" : - "../../../../../.dotnet/dotnet"; + throw new InvalidOperationException("Could not locate the dotnet host executable."); + } + + public static string GetHostFromRepository(Architecture? arch = null) + { + // e.g. /.dotnet + string dotnetDirPath = Path.Combine("..", "..", "..", "..", "..", ".dotnet"); + if (arch.HasValue && arch.Value != RuntimeInformation.OSArchitecture) + { + // e.g. Append "\x86" to the path + dotnetDirPath = Path.Combine(dotnetDirPath, arch.Value.ToString("G").ToLowerInvariant()); + } + + return Path.GetFullPath(Path.Combine(dotnetDirPath, ExeName)); + } public static TargetFrameworkMoniker BuiltTargetFrameworkMoniker { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/EnumerableExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/EnumerableExtensions.cs index 79e4969f20f..b41d1479bcb 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/EnumerableExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/EnumerableExtensions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.TestCommon diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Fixtures/AzuriteFixture.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Fixtures/AzuriteFixture.cs new file mode 100644 index 00000000000..a37e43ca6e6 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Fixtures/AzuriteFixture.cs @@ -0,0 +1,259 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// This file and its classes are derived from: +// https://github.com/Azure/azure-sdk-for-net/blob/Azure.ResourceManager_1.3.1/sdk/storage/Azure.Storage.Common/tests/Shared/AzuriteFixture.cs +// + +using Microsoft.DotNet.XUnitExtensions; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon.Fixtures +{ + public class AzuriteAccount + { + public string Name { get; set; } + public string Key { get; set; } + public int BlobsPort { get; set; } + public int QueuesPort { get; set; } + public int TablesPort { get; set; } + + public string BlobEndpoint => $"http://127.0.0.1:{BlobsPort}/{Name}"; + public string QueueEndpoint => $"http://127.0.0.1:{QueuesPort}/{Name}"; + public string TableEndpoint => $"http://127.0.0.1:{TablesPort}/{Name}"; + + public string ConnectionString => $"DefaultEndpointsProtocol=http;AccountName={Name};AccountKey={Key};BlobEndpoint={BlobEndpoint};QueueEndpoint={QueueEndpoint};TableEndpoint={TableEndpoint}"; + } + + /// + /// This class manages Azurite Lifecycle for a test class. + /// It requires Azurite V3. See installation instructions here https://github.com/Azure/Azurite. + /// + public class AzuriteFixture : IDisposable + { + private Process _azuriteProcess; + + private readonly TemporaryDirectory _workspaceDirectory; + private readonly CountdownEvent _startupCountdownEvent = new CountdownEvent(initialCount: 3); // Wait for the Blob, Queue, and Table services to start + + private readonly StringBuilder _azuriteStartupStdout = new(); + private readonly StringBuilder _azuriteStartupStderr = new(); + private readonly string _startupErrorMessage; + + public AzuriteAccount Account { get; } + + public AzuriteFixture() + { + // Check if the tests are running on a pipeline build machine. + // If so, Azurite must successfully initialize otherwise mark the dependent tests as failed + // to avoid hiding failures in our CI. + // + // Workaround: for now allow Windows environments to skip Azurite based tests due to configuration + // issues in the Pipeline environment. + bool mustInitialize = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TF_BUILD")); + + byte[] key = new byte[32]; + RandomNumberGenerator.Fill(key); + Account = new AzuriteAccount() + { + Name = Guid.NewGuid().ToString("N"), + Key = Convert.ToBase64String(key), + }; + + _workspaceDirectory = new TemporaryDirectory(new ConsoleOutputHelper()); + _azuriteProcess = new Process() + { + StartInfo = ConstructAzuriteProcessStartInfo(Account, _workspaceDirectory.FullName) + }; + + _azuriteProcess.OutputDataReceived += ParseAzuriteStartupOutput; + _azuriteProcess.ErrorDataReceived += ParseAzuriteStartupError; + + try + { + _azuriteProcess.Start(); + } + catch (Exception ex) + { + _startupErrorMessage = ErrorMessage($"failed to start azurite with exception: {ex.Message}"); + + if (mustInitialize) + { + throw new InvalidOperationException(_startupErrorMessage, ex); + } + + _azuriteProcess = null; + return; + } + + _azuriteProcess.BeginOutputReadLine(); + _azuriteProcess.BeginErrorReadLine(); + + bool didAzuriteStart = _startupCountdownEvent.Wait(CommonTestTimeouts.AzuriteInitializationTimeout); + if (!didAzuriteStart) + { + // If we were able to launch the azurite process but initialization failed, mark the tests as failed + // even for non-pipeline machines. + if (_azuriteProcess.HasExited) + { + throw new InvalidOperationException($"azurite could not start with following output:\n{_azuriteStartupStdout}\nerror:\n{_azuriteStartupStderr}\nexit code:{_azuriteProcess.ExitCode}"); + } + else + { + _azuriteProcess.Kill(); + _azuriteProcess.WaitForExit(CommonTestTimeouts.AzuriteTeardownTimeout.Milliseconds); + throw new InvalidOperationException($"azurite could not initialize within timeout with following output:\n{_azuriteStartupStdout}\nerror:\n{_azuriteStartupStderr}"); + } + } + } + + private ProcessStartInfo ConstructAzuriteProcessStartInfo(AzuriteAccount authorizedAccount, string workspaceDirectory) + { + bool isVSCopy = false; + string azuriteFolder = null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string vsAppDir = Environment.GetEnvironmentVariable("VSAPPIDDIR"); + if (vsAppDir != null) + { + string vsAzuriteFolder = Path.Combine(vsAppDir, "Extensions", "Microsoft", "Azure Storage Emulator"); + if (Directory.Exists(vsAzuriteFolder)) + { + azuriteFolder = vsAzuriteFolder; + isVSCopy = true; + } + } + } + + ProcessStartInfo startInfo = new() + { + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardInput = true, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + string azuriteExecutable; + if (isVSCopy) + { + azuriteExecutable = "azurite.exe"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + azuriteExecutable = "azurite.cmd"; + } + else + { + azuriteExecutable = "azurite"; + } + + startInfo.FileName = Path.Combine(azuriteFolder ?? string.Empty, azuriteExecutable); + + startInfo.ArgumentList.Add("--skipApiVersionCheck"); + + // Use a temporary directory to store data + startInfo.ArgumentList.Add("--location"); + startInfo.ArgumentList.Add(workspaceDirectory); + + // Auto pick port + startInfo.ArgumentList.Add("--blobPort"); + startInfo.ArgumentList.Add("0"); + + // Auto pick port + startInfo.ArgumentList.Add("--queuePort"); + startInfo.ArgumentList.Add("0"); + + // Auto pick port + startInfo.ArgumentList.Add("--tablePort"); + startInfo.ArgumentList.Add("0"); + + startInfo.EnvironmentVariables.Add("AZURITE_ACCOUNTS", $"{authorizedAccount.Name}:{authorizedAccount.Key}"); + + return startInfo; + } + + public void SkipTestIfNotAvailable() + { + if (_startupErrorMessage != null) + { + throw new SkipTestException(_startupErrorMessage); + } + } + + private void ParseAzuriteStartupOutput(object sender, DataReceivedEventArgs e) + { + if (e.Data == null || _startupCountdownEvent.IsSet) + { + return; + } + + _azuriteStartupStdout.AppendLine(e.Data); + + if (e.Data.Contains("Azurite Blob service is successfully listening at")) + { + Account.BlobsPort = ParseAzuritePort(e.Data); + _startupCountdownEvent.Signal(); + } + else if (e.Data.Contains("Azurite Queue service is successfully listening at")) + { + Account.QueuesPort = ParseAzuritePort(e.Data); + _startupCountdownEvent.Signal(); + } + else if (e.Data.Contains("Azurite Table service is successfully listening at")) + { + Account.TablesPort = ParseAzuritePort(e.Data); + _startupCountdownEvent.Signal(); + } + } + + private void ParseAzuriteStartupError(object sender, DataReceivedEventArgs e) + { + if (e.Data == null || _startupCountdownEvent.IsSet) + { + return; + } + + _azuriteStartupStderr.AppendLine(e.Data); + } + + private int ParseAzuritePort(string outputLine) + { + int portDelimiterIndex = outputLine.LastIndexOf(':') + 1; + if (portDelimiterIndex == 0 || portDelimiterIndex >= outputLine.Length) + { + throw new InvalidOperationException($"azurite stdout did not follow the expected format, cannot parse port information. Unexpected output: {outputLine}"); + } + + return int.Parse(outputLine[portDelimiterIndex..]); + } + + private string ErrorMessage(string specificReason) + { + return $"Could not run Azurite based test: {specificReason}.\n" + + "Make sure that:\n" + + "- Azurite V3 is installed either via Visual Studio 2022 (or later) or NPM (see https://docs.microsoft.com/azure/storage/common/storage-use-azurite#install-azurite for instructions)\n" + + "- Ensure that the directory that has 'azurite' executable is in the 'PATH' environment variable if not launching tests through Test Explorer in Visual Studio\n"; + } + + public void Dispose() + { + if (_azuriteProcess?.HasExited == false) + { + _azuriteProcess.Kill(); + _azuriteProcess.WaitForExit(CommonTestTimeouts.AzuriteTeardownTimeout.Milliseconds); + } + + _azuriteProcess?.Dispose(); + _workspaceDirectory?.Dispose(); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LiveMetricsTestUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LiveMetricsTestUtilities.cs new file mode 100644 index 00000000000..feac173258b --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LiveMetricsTestUtilities.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.WebApi; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + internal class LiveMetricsTestUtilities + { + internal static async Task ValidateMetrics(IEnumerable expectedProviders, IEnumerable expectedNames, + IAsyncEnumerable actualMetrics, bool strict) + { + HashSet actualProviders = new(); + HashSet actualNames = new(); + + await AggregateMetrics(actualMetrics, actualProviders, actualNames); + + CompareSets(new HashSet(expectedProviders), actualProviders, strict); + CompareSets(new HashSet(expectedNames), actualNames, strict); + } + + private static void CompareSets(HashSet expected, HashSet actual, bool strict) + { + bool matched = true; + if (strict && !expected.SetEquals(actual)) + { + expected.SymmetricExceptWith(actual); + matched = false; + } + else if (!strict && !expected.IsSubsetOf(actual)) + { + //actual must contain at least the elements in expected, but can contain more + expected.ExceptWith(actual); + matched = false; + } + Assert.True(matched, "Missing or unexpected elements: " + string.Join(",", expected)); + } + + private static async Task AggregateMetrics(IAsyncEnumerable actualMetrics, + HashSet providers, + HashSet names) + { + await foreach (CounterPayload counter in actualMetrics) + { + providers.Add(counter.Provider); + names.Add(counter.Name); + } + } + + internal static async IAsyncEnumerable GetAllMetrics(Stream liveMetricsStream) + { + using var reader = new StreamReader(liveMetricsStream); + + string entry = string.Empty; + while ((entry = await reader.ReadLineAsync()) != null) + { + Assert.Equal(StreamingLogger.JsonSequenceRecordSeparator, (byte)entry[0]); + yield return JsonSerializer.Deserialize(entry.Substring(1)); + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LogsTestUtilities.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LogsTestUtilities.cs index 3773eae1fa7..d753d7e6c18 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LogsTestUtilities.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/LogsTestUtilities.cs @@ -3,13 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.Options; -using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Channels; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj index c32f8f41870..6b7a6c6f7ae 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Microsoft.Diagnostics.Monitoring.TestCommon.csproj @@ -7,24 +7,30 @@ + + + + - + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/NativeLibraryHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/NativeLibraryHelper.cs index 957d15158bb..d13481276a3 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/NativeLibraryHelper.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/NativeLibraryHelper.cs @@ -18,12 +18,7 @@ internal static class NativeLibraryHelper "Release"; #endif - public static readonly Guid MonitorProfilerClsid = new Guid("6A494330-5848-4A23-9D87-0E57BBF6DE79"); - - public static string GetMonitorProfilerPath(Architecture architecture) => - GetSharedLibraryPath(architecture, "MonitorProfiler"); - - private static string GetSharedLibraryPath(Architecture architecture, string rootName) + public static string GetSharedLibraryPath(Architecture architecture, string rootName) { string artifactsBinPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "..", "..")); return Path.Combine(artifactsBinPath, GetNativeBinDirectoryName(architecture), GetSharedLibraryName(rootName)); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/ProfilerHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/ProfilerHelper.cs new file mode 100644 index 00000000000..81ec85c2c60 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/ProfilerHelper.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.TestCommon +{ + public static class ProfilerHelper + { + private const string ClrEnvVarPrefix = "CORECLR_"; + + public const string ClrEnvVarEnabledValue = "1"; + public const string ClrEnvVarEnableNotificationProfilers = ClrEnvVarPrefix + "ENABLE_NOTIFICATION_PROFILERS"; + public const string ClrEnvVarEnableProfiling = ClrEnvVarPrefix + "ENABLE_PROFILING"; + public const string ClrEnvVarProfiler = ClrEnvVarPrefix + "PROFILER"; + public const string ClrEnvVarProfilerPath = ClrEnvVarPrefix + "PROFILER_PATH"; + + public static string GetPath(Architecture architecture) => + NativeLibraryHelper.GetSharedLibraryPath(architecture, ProfilerIdentifiers.LibraryRootFileName); + + private const string OSReleasePath = "/etc/os-release"; + + public static string GetTargetRuntimeIdentifier(Architecture? architecture) + { + string architectureString = (architecture ?? RuntimeInformation.OSArchitecture) + .ToString("G") + .ToLowerInvariant(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return FormattableString.Invariant($"win-{architectureString}"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return FormattableString.Invariant($"osx-{architectureString}"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + if (File.Exists(OSReleasePath) && File.ReadAllText(OSReleasePath).Contains("Alpine", StringComparison.OrdinalIgnoreCase)) + { + return FormattableString.Invariant($"linux-musl-{architectureString}"); + } + else + { + return FormattableString.Invariant($"linux-{architectureString}"); + } + } + + throw new PlatformNotSupportedException("Unable to determine OS platform."); + } + + public static IEnumerable GetArchitecture() + { + // There isn't a good way to check which architecture to use when running unit tests. + // Each build job builds one specific architecture, but from a test perspective, + // it cannot tell which one was built. Gather all of the profilers for every architecture + // so long as they exist. + List arguments = new(); + AddTestCases(arguments, Architecture.X64); + AddTestCases(arguments, Architecture.X86); + AddTestCases(arguments, Architecture.Arm64); + return arguments; + + static void AddTestCases(List arguments, Architecture architecture) + { + string profilerPath = GetPath(architecture); + if (File.Exists(profilerPath)) + { + arguments.Add(new object[] { architecture }); + } + } + } + + public static IEnumerable GetArchitectureProfilerPath() + { + // There isn't a good way to check which architecture to use when running unit tests. + // Each build job builds one specific architecture, but from a test perspective, + // it cannot tell which one was built. Gather all of the profilers for every architecture + // so long as they exist. + List arguments = new(); + AddTestCases(arguments, Architecture.X64); + AddTestCases(arguments, Architecture.X86); + AddTestCases(arguments, Architecture.Arm64); + return arguments; + + static void AddTestCases(List arguments, Architecture architecture) + { + string profilerPath = GetPath(architecture); + if (File.Exists(profilerPath)) + { + arguments.Add(new object[] { architecture, profilerPath }); + } + } + } + + public static async Task VerifyProductVersionEnvironmentVariableAsync(AppRunner runner, ITestOutputHelper outputHelper) + { + string productVersion = await runner.GetEnvironmentVariable(ProfilerIdentifiers.EnvironmentVariables.ProductVersion, CommonTestTimeouts.EnvVarsTimeout); + Assert.False(string.IsNullOrEmpty(productVersion), "Expected product version to not be null or empty."); + outputHelper.WriteLine("{0} = {1}", ProfilerIdentifiers.EnvironmentVariables.ProductVersion, productVersion); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs index 819a2a947da..07887646644 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/AppRunner.cs @@ -3,11 +3,11 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Diagnostics.Tools.Monitor; using System; using System.Collections.Generic; using System.IO; using System.Reflection; +using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -38,6 +38,12 @@ public sealed class AppRunner : IAsyncDisposable private bool _isDiposed; + public Architecture? Architecture + { + get => _runner.Architecture; + set => _runner.Architecture = value; + } + /// /// The mode of the diagnostic port connection. Default is /// (the application is listening for connections). @@ -65,6 +71,10 @@ public sealed class AppRunner : IAsyncDisposable public int AppId { get; } + public bool SetRuntimeIdentifier { get; set; } = true; + + public string ProfilerLogLevel { get; set; } = null; + public AppRunner(ITestOutputHelper outputHelper, Assembly testAssembly, int appId = 1, TargetFrameworkMoniker tfm = TargetFrameworkMoniker.Current) { AppId = appId; @@ -76,8 +86,6 @@ public AppRunner(ITestOutputHelper outputHelper, Assembly testAssembly, int appI "Microsoft.Diagnostics.Monitoring.UnitTestApp", tfm); - _runner.TargetFramework = tfm; - _waitingForEnvironmentVariables = new Dictionary>(); _adapter = new LoggingRunnerAdapter(_outputHelper, _runner); @@ -132,6 +140,18 @@ public async Task StartAsync(CancellationToken token) _adapter.Environment.Add("DOTNET_DiagnosticPorts", DiagnosticPortPath); } + if (SetRuntimeIdentifier) + { + _adapter.Environment.Add( + ToolIdentifiers.EnvironmentVariables.RuntimeIdentifier, + ProfilerHelper.GetTargetRuntimeIdentifier(Architecture)); + } + if (ProfilerLogLevel != null) + { + _adapter.Environment.Add( + ProfilerIdentifiers.EnvironmentVariables.StdErrLogger_Level, ProfilerLogLevel); + } + await _adapter.StartAsync(token).ConfigureAwait(false); await _readySource.WithCancellation(token); @@ -189,10 +209,10 @@ private void HandleProgramEvent(ConsoleLogEvent logEvent) switch ((TestAppLogEventIds)logEvent.EventId) { case TestAppLogEventIds.ScenarioState: - Assert.True(logEvent.State.TryGetValue("state", out TestAppScenarios.SenarioState state)); + Assert.True(logEvent.State.TryGetValue("state", out TestAppScenarios.ScenarioState state)); switch (state) { - case TestAppScenarios.SenarioState.Ready: + case TestAppScenarios.ScenarioState.Ready: Assert.True(_readySource.TrySetResult(null)); break; } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs index 8ab09e9292d..ab572c8954d 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/DotNetRunner.cs @@ -27,6 +27,11 @@ public sealed class DotNetRunner : IDisposable // The process object of the started process private readonly Process _process; + /// + /// The architecture of the dotnet host. + /// + public Architecture? Architecture { get; set; } = null; + /// /// The arguments to the entrypoint method. /// @@ -57,11 +62,6 @@ public sealed class DotNetRunner : IDisposable /// public Task ExitedTask => _exitedSource.Task; - /// - /// The framework reference of the app to run. - /// - public DotNetFrameworkReference FrameworkReference { get; set; } = DotNetFrameworkReference.Microsoft_NetCore_App; - /// /// Determines if the process has exited. /// @@ -87,11 +87,6 @@ public sealed class DotNetRunner : IDisposable /// public StreamReader StandardOutput => _process.StandardOutput; - /// - /// Get or set the target framework on which the application should run. - /// - public TargetFrameworkMoniker TargetFramework { get; set; } = TargetFrameworkMoniker.Current; - /// /// Determines if should wait for the diagnostic pipe to be available. /// @@ -100,7 +95,6 @@ public sealed class DotNetRunner : IDisposable public DotNetRunner() { _process = new Process(); - _process.StartInfo.FileName = DotNetHost.HostExePath; _process.StartInfo.UseShellExecute = false; _process.StartInfo.RedirectStandardError = true; _process.StartInfo.RedirectStandardInput = true; @@ -125,40 +119,19 @@ public void Dispose() /// public async Task StartAsync(CancellationToken token) { - string frameworkVersion = null; - switch (FrameworkReference) - { - case DotNetFrameworkReference.Microsoft_AspNetCore_App: - // Starting in .NET 6, the .NET SDK is emitting two framework references - // into the .runtimeconfig.json file. This is preventing the --fx-version - // parameter from having the correct effect of using the exact framework version - // that we want. Disabling this forced version usage for ASP.NET 6+ applications - // until it can be resolved. - if (!TargetFramework.IsEffectively(TargetFrameworkMoniker.Net60) && - !TargetFramework.IsEffectively(TargetFrameworkMoniker.Net70)) - { - frameworkVersion = TargetFramework.GetAspNetCoreFrameworkVersionString(); - } - break; - case DotNetFrameworkReference.Microsoft_NetCore_App: - frameworkVersion = TargetFramework.GetNetCoreAppFrameworkVersionString(); - break; - default: - throw new InvalidOperationException($"Unsupported framework reference: {FrameworkReference}"); - } - StringBuilder argsBuilder = new(); - if (!string.IsNullOrEmpty(frameworkVersion)) + if (DotNetHost.HasHostInRepository) { - argsBuilder.Append("--fx-version "); - argsBuilder.Append(frameworkVersion); - argsBuilder.Append(" "); + argsBuilder.Append("exec --runtimeconfig \""); + argsBuilder.Append(Path.ChangeExtension(EntrypointAssemblyPath, ".runtimeconfig.test.json")); + argsBuilder.Append("\" "); } argsBuilder.Append("\""); argsBuilder.Append(EntrypointAssemblyPath); argsBuilder.Append("\" "); argsBuilder.Append(Arguments); + _process.StartInfo.FileName = DotNetHost.GetPath(Architecture); _process.StartInfo.Arguments = argsBuilder.ToString(); if (!_process.Start()) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs index 6da15a4276d..96dde81541e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/Runners/LoggingRunnerAdapter.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostics.Tools.Monitor; using System; using System.Collections.Generic; using System.IO; @@ -120,7 +119,7 @@ public async Task WaitForExitAsync(CancellationToken token) if (!_runner.HasStarted) { _outputHelper.WriteLine("Runner Never Started."); - throw new InvalidOperationException("The has runner has never been started, call StartAsync first."); + throw new InvalidOperationException("The runner has never been started, call StartAsync first."); } else if (_runner.HasExited) { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/StreamExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/StreamExtensions.cs index aee147d848f..4ae4bcb6b49 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/StreamExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/StreamExtensions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TemporaryDirectory.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TemporaryDirectory.cs index 2081b61be3d..a8d4a9b4f97 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TemporaryDirectory.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TemporaryDirectory.cs @@ -17,7 +17,7 @@ public TemporaryDirectory(ITestOutputHelper outputhelper) { _outputHelper = outputhelper; - _directoryInfo = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))); + _directoryInfo = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); _directoryInfo.Create(); _outputHelper.WriteLine("Created temporary directory '{0}'", FullName); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs index 36f7556494c..83a8a5777bf 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.TestCommon/TestAppScenarios.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.Diagnostics.Tracing; + namespace Microsoft.Diagnostics.Monitoring.TestCommon { public static class TestAppScenarios @@ -13,7 +15,7 @@ public static class Commands public const string PrintEnvironmentVariables = nameof(PrintEnvironmentVariables); } - public enum SenarioState + public enum ScenarioState { Waiting, Ready, @@ -31,6 +33,16 @@ public static class Commands } } + public static class Stacks + { + public const string Name = nameof(Stacks); + + public static class Commands + { + public const string Continue = nameof(Continue); + } + } + public static class EnvironmentVariables { public const string Name = nameof(EnvironmentVariables); @@ -71,5 +83,22 @@ public static class Commands public const string StopSpin = nameof(StopSpin); } } + + public static class TraceEvents + { + public const string Name = nameof(TraceEvents); + public const string UniqueEventName = "UniqueEvent"; + public const string EventProviderName = "TestScenario"; + public const string UniqueEventMessage = "FooBar"; + public const string UniqueEventPayloadField = "message"; + + public const TraceEventOpcode UniqueEventOpcode = TraceEventOpcode.Reply; + + public static class Commands + { + public const string EmitUniqueEvent = nameof(EmitUniqueEvent); + public const string ShutdownScenario = nameof(ShutdownScenario); + } + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs index c29d05ba254..ad374332171 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/AuthenticationTests.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; -using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; -using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Options; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; @@ -17,14 +19,10 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Security.Claims; -using System.Security.Cryptography; using System.Text.Json; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -using Microsoft.Diagnostics.Tools.Monitor; -using System.Threading; -using Microsoft.Diagnostics.Monitoring.WebApi; namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests { @@ -50,7 +48,7 @@ public AuthenticationTests(ITestOutputHelper outputHelper, ServiceProviderFixtur public async Task DefaultAddressTest() { await using MonitorCollectRunner toolRunner = new(_outputHelper); - + await toolRunner.StartAsync(); using HttpClient httpClient = await toolRunner.CreateHttpClientDefaultAddressAsync(_httpClientFactory); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs new file mode 100644 index 00000000000..f1cead807a5 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectTraceTests.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + [Collection(DefaultCollectionFixture.Name)] + public class CollectTraceTests + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly ITestOutputHelper _outputHelper; + + public CollectTraceTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) + { + _httpClientFactory = serviceProviderFixture.ServiceProvider.GetService(); + _outputHelper = outputHelper; + } + +#if NET5_0_OR_GREATER + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingOpcode() + { + return StopOnEventTestCore(expectStoppingEvent: true); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingOpcodeAndNoRundown() + { + return StopOnEventTestCore(expectStoppingEvent: true, collectRundown: false); + } + + [Fact] + public Task StopOnEvent_Succeeds_WithMatchingPayload() + { + return StopOnEventTestCore(expectStoppingEvent: true, payloadFilter: new Dictionary() + { + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage } + }); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenOpcodeDoesNotMatch() + { + return StopOnEventTestCore(expectStoppingEvent: false, opcode: TraceEventOpcode.Resume); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenPayloadFieldNamesMismatch() + { + return StopOnEventTestCore(expectStoppingEvent: false, payloadFilter: new Dictionary() + { + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage }, + { "foobar", "baz" } + }); + } + + [Fact] + public Task StopOnEvent_DoesNotStop_WhenPayloadFieldValueMismatch() + { + return StopOnEventTestCore(expectStoppingEvent: false, payloadFilter: new Dictionary() + { + { TestAppScenarios.TraceEvents.UniqueEventPayloadField, TestAppScenarios.TraceEvents.UniqueEventMessage.ToUpperInvariant() } + }); + } + + private string ConstructQualifiedEventName(string eventName, TraceEventOpcode opcode) + { + return (opcode == TraceEventOpcode.Info) + ? eventName + : FormattableString.Invariant($"{eventName}/{opcode}"); + } + + private async Task StopOnEventTestCore(bool expectStoppingEvent, TraceEventOpcode opcode = TestAppScenarios.TraceEvents.UniqueEventOpcode, bool collectRundown = true, IDictionary payloadFilter = null, TimeSpan? duration = null) + { + TimeSpan DefaultCollectTraceTimeout = TimeSpan.FromSeconds(10); + const string DefaultRuleName = "FunctionalTestRule"; + const string EgressProvider = "TmpEgressProvider"; + + using TemporaryDirectory tempDirectory = new(_outputHelper); + + Task ruleCompletedTask = null; + + TraceEventFilter traceEventFilter = new() + { + ProviderName = TestAppScenarios.TraceEvents.EventProviderName, + EventName = ConstructQualifiedEventName(TestAppScenarios.TraceEvents.UniqueEventName, opcode), + PayloadFilter = payloadFilter + }; + + await ScenarioRunner.SingleTarget(_outputHelper, + _httpClientFactory, + DiagnosticPortConnectionMode.Listen, + TestAppScenarios.TraceEvents.Name, + appValidate: async (appRunner, apiClient) => + { + await appRunner.SendCommandAsync(TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent); + await ruleCompletedTask; + await appRunner.SendCommandAsync(TestAppScenarios.TraceEvents.Commands.ShutdownScenario); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.AddFileSystemEgress(EgressProvider, tempDirectory.FullName); + runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction( + new EventPipeProvider[] { + new EventPipeProvider() + { + Name = TestAppScenarios.TraceEvents.EventProviderName, + Keywords = "-1" + } + }, + EgressProvider, options => + { + options.Duration = duration ?? DefaultCollectTraceTimeout; + options.StoppingEvent = traceEventFilter; + options.RequestRundown = collectRundown; + }); + + ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(DefaultRuleName); + }); + + string[] files = Directory.GetFiles(tempDirectory.FullName, "*.nettrace", SearchOption.TopDirectoryOnly); + string traceFile = Assert.Single(files); + + var (hasStoppingEvent, hasRundown) = await ValidateNettraceFile(traceFile, traceEventFilter); + Assert.Equal(expectStoppingEvent, hasStoppingEvent); + Assert.Equal(collectRundown, hasRundown); + } + + private Task<(bool hasStoppingEvent, bool hasRundown)> ValidateNettraceFile(string filePath, TraceEventFilter eventFilter) + { + return Task.Run(() => + { + using FileStream fs = File.OpenRead(filePath); + using EventPipeEventSource eventSource = new(fs); + + bool didSeeRundownEvents = false; + bool didSeeStoppingEvent = false; + + eventSource.Dynamic.AddCallbackForProviderEvent(eventFilter.ProviderName, eventFilter.EventName, (obj) => + { + if (eventFilter.PayloadFilter != null) + { + foreach (var (fieldName, fieldValue) in eventFilter.PayloadFilter) + { + object payloadValue = obj.PayloadByName(fieldName); + if (!string.Equals(fieldValue, payloadValue?.ToString(), StringComparison.Ordinal)) + { + return; + } + } + } + + didSeeStoppingEvent = true; + }); + + ClrRundownTraceEventParser rundown = new(eventSource); + rundown.RuntimeStart += (data) => + { + didSeeRundownEvents = true; + }; + + eventSource.Process(); + return (didSeeStoppingEvent, didSeeRundownEvents); + }); + } +#endif // NET5_0_OR_GREATER + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleDescriptionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleDescriptionTests.cs new file mode 100644 index 00000000000..f1bf3d163a0 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleDescriptionTests.cs @@ -0,0 +1,364 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + [Collection(DefaultCollectionFixture.Name)] + public class CollectionRuleDescriptionTests + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly ITestOutputHelper _outputHelper; + + public CollectionRuleDescriptionTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) + { + _httpClientFactory = serviceProviderFixture.ServiceProvider.GetService(); + _outputHelper = outputHelper; + } + +#if NET5_0_OR_GREATER + private const string NonStartupRuleName = "NonStartupTestRule"; + private const string StartupRuleName = "StartupTestRule"; + + // These should be identical to the messages found in Strings.resx + private const string FinishedStartup = "The collection rule will no longer trigger because the Startup trigger only executes once."; + private const string FinishedActionCount = "The collection rule will no longer trigger because the ActionCount was reached."; + private const string Running = "This collection rule is active and waiting for its triggering conditions to be satisfied."; + + /// + /// Validates that a startup rule will execute and complete with the correct collection rule descriptions + /// + [Theory] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRuleDescription_StartupTriggerTest(DiagnosticPortConnectionMode mode) + { + using TemporaryDirectory tempDirectory = new(_outputHelper); + + Task ruleCompletedTask = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.AsyncWait.Name, + appValidate: async (runner, client) => + { + await ruleCompletedTask; + + // Validate detailed description for the Startup rule + + CollectionRuleDetailedDescription actualDetailedDescription = await client.GetCollectionRuleDetailedDescriptionAsync(StartupRuleName, await runner.ProcessIdTask, null, null); + CollectionRuleDetailedDescription expectedDetailedDescription = new() + { + ActionCountLimit = CollectionRuleLimitsOptionsDefaults.ActionCount, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.Finished, + StateReason = FinishedStartup + }; + Assert.Equal(expectedDetailedDescription, actualDetailedDescription); + + // Validate brief descriptions for all rules + + Dictionary actualDescriptions = await client.GetCollectionRulesDescriptionAsync(await runner.ProcessIdTask, null, null); + Dictionary expectedDescriptions = new() + { + { + StartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription.State, + StateReason = expectedDetailedDescription.StateReason + } + } + }; + + ValidateCollectionRuleDescriptions(expectedDescriptions, actualDescriptions); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.CreateCollectionRule(StartupRuleName) + .SetStartupTrigger(); + + ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(StartupRuleName); + }); + } + + /// + /// Validates that a non-startup rule will complete when it has an action limit specified + /// without a sliding window duration. + /// + [Theory] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRuleDescription_ActionLimitTest(DiagnosticPortConnectionMode mode) + { + using TemporaryDirectory tempDirectory = new(_outputHelper); + string ExpectedFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); + string ExpectedFileContent = Guid.NewGuid().ToString("N"); + + const int ExpectedActionCountLimit = 1; + + Task ruleCompletedTask = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.SpinWait.Name, + appValidate: async (runner, client) => + { + // Validate detailed description for the NonStartup rule before spinning the CPU + + CollectionRuleDetailedDescription actualDetailedDescription_Before = await client.GetCollectionRuleDetailedDescriptionAsync(NonStartupRuleName, await runner.ProcessIdTask, null, null); + CollectionRuleDetailedDescription expectedDetailedDescription_Before = new() + { + ActionCountLimit = ExpectedActionCountLimit, + LifetimeOccurrences = 0, + SlidingWindowOccurrences = 0, + State = CollectionRuleState.Running, + StateReason = Running + }; + Assert.Equal(expectedDetailedDescription_Before, actualDetailedDescription_Before); + + // Validate brief descriptions for all rules before spinning the CPU + + Dictionary actualDescriptions_Before = await client.GetCollectionRulesDescriptionAsync(await runner.ProcessIdTask, null, null); + Dictionary expectedDescriptions_Before = new() + { + { + NonStartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription_Before.State, + StateReason = expectedDetailedDescription_Before.StateReason + } + } + }; + + ValidateCollectionRuleDescriptions(expectedDescriptions_Before, actualDescriptions_Before); + + await runner.SendCommandAsync(TestAppScenarios.SpinWait.Commands.StartSpin); + + await ruleCompletedTask; + + await runner.SendCommandAsync(TestAppScenarios.SpinWait.Commands.StopSpin); + + // Validate detailed description for the NonStartup rule after spinning the CPU + + CollectionRuleDetailedDescription actualDetailedDescription_After = await client.GetCollectionRuleDetailedDescriptionAsync(NonStartupRuleName, await runner.ProcessIdTask, null, null); + CollectionRuleDetailedDescription expectedDetailedDescription_After = new() + { + ActionCountLimit = ExpectedActionCountLimit, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.Finished, + StateReason = FinishedActionCount + }; + Assert.Equal(expectedDetailedDescription_After, actualDetailedDescription_After); + + // Validate brief descriptions for all rules after spinning the CPU + + Dictionary actualDescriptions_After = await client.GetCollectionRulesDescriptionAsync(await runner.ProcessIdTask, null, null); + Dictionary expectedDescriptions_After = new() + { + { + NonStartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription_After.State, + StateReason = expectedDetailedDescription_After.StateReason + } + } + }; + + ValidateCollectionRuleDescriptions(expectedDescriptions_After, actualDescriptions_After); + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.CreateCollectionRule(NonStartupRuleName) + .SetEventCounterTrigger(options => + { + // cpu usage greater that 5% for 2 seconds + options.ProviderName = "System.Runtime"; + options.CounterName = "cpu-usage"; + options.GreaterThan = 5; + options.SlidingWindowDuration = TimeSpan.FromSeconds(2); + }) + .AddExecuteActionAppAction("TextFileOutput", ExpectedFilePath, ExpectedFileContent) + .SetActionLimits(count: ExpectedActionCountLimit); + + ruleCompletedTask = runner.WaitForCollectionRuleCompleteAsync(NonStartupRuleName); + }); + } + + /// + /// Validates the CollectionRuleDescriptions for two rules running on the same process + /// + [Theory] + [InlineData(DiagnosticPortConnectionMode.Listen)] + public async Task CollectionRuleDescription_MultipleRulesTest(DiagnosticPortConnectionMode mode) + { + using TemporaryDirectory tempDirectory = new(_outputHelper); + string ExpectedFilePath = Path.Combine(tempDirectory.FullName, "file.txt"); + string ExpectedFileContent = Guid.NewGuid().ToString("N"); + + const int ExpectedActionCountLimit = 1; + + Task ruleCompletedTask_Startup = null; + Task ruleCompletedTask_NonStartup = null; + + await ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + mode, + TestAppScenarios.SpinWait.Name, + appValidate: async (runner, client) => + { + await ruleCompletedTask_Startup; + + // Validate detailed description for the NonStartup rule + CollectionRuleDetailedDescription actualDetailedDescription_NonStartup = await client.GetCollectionRuleDetailedDescriptionAsync(NonStartupRuleName, await runner.ProcessIdTask, null, null); + CollectionRuleDetailedDescription expectedDetailedDescription_NonStartup = new() + { + ActionCountLimit = ExpectedActionCountLimit, + LifetimeOccurrences = 0, + SlidingWindowOccurrences = 0, + State = CollectionRuleState.Running, + StateReason = Running + }; + Assert.Equal(expectedDetailedDescription_NonStartup, actualDetailedDescription_NonStartup); + + // Validate detailed description for the Startup rule + + CollectionRuleDetailedDescription actualDetailedDescription_Startup = await client.GetCollectionRuleDetailedDescriptionAsync(StartupRuleName, await runner.ProcessIdTask, null, null); + CollectionRuleDetailedDescription expectedDetailedDescription_Startup = new() + { + ActionCountLimit = CollectionRuleLimitsOptionsDefaults.ActionCount, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.Finished, + StateReason = FinishedStartup + }; + Assert.Equal(expectedDetailedDescription_Startup, actualDetailedDescription_Startup); + + // Validate brief descriptions for all rules + + Dictionary actualDescriptions = await client.GetCollectionRulesDescriptionAsync(await runner.ProcessIdTask, null, null); + Dictionary expectedDescriptions = new() + { + { + NonStartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription_NonStartup.State, + StateReason = expectedDetailedDescription_NonStartup.StateReason + } + }, + { + StartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription_Startup.State, + StateReason = expectedDetailedDescription_Startup.StateReason + } + } + }; + + ValidateCollectionRuleDescriptions(expectedDescriptions, actualDescriptions); + + await runner.SendCommandAsync(TestAppScenarios.SpinWait.Commands.StartSpin); + + await ruleCompletedTask_NonStartup; + + await runner.SendCommandAsync(TestAppScenarios.SpinWait.Commands.StopSpin); + + // Validate detailed description for the NonStartup rule after spinning the CPU + + CollectionRuleDetailedDescription actualDetailedDescription_After = await client.GetCollectionRuleDetailedDescriptionAsync(NonStartupRuleName, await runner.ProcessIdTask, null, null); + CollectionRuleDetailedDescription expectedDetailedDescription_After = new() + { + ActionCountLimit = ExpectedActionCountLimit, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.Finished, + StateReason = FinishedActionCount + }; + Assert.Equal(expectedDetailedDescription_After, actualDetailedDescription_After); + + // Validate brief descriptions for all rules after spinning the CPU + + Dictionary actualDescriptions_After = await client.GetCollectionRulesDescriptionAsync(await runner.ProcessIdTask, null, null); + Dictionary expectedDescriptions_After = new() + { + { + NonStartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription_After.State, + StateReason = expectedDetailedDescription_After.StateReason + } + }, + { + StartupRuleName, new CollectionRuleDescription() + { + State = expectedDetailedDescription_Startup.State, + StateReason = expectedDetailedDescription_Startup.StateReason + } + } + }; + + ValidateCollectionRuleDescriptions(expectedDescriptions_After, actualDescriptions_After); + + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.CreateCollectionRule(NonStartupRuleName) + .SetEventCounterTrigger(options => + { + // cpu usage greater that 5% for 2 seconds + options.ProviderName = "System.Runtime"; + options.CounterName = "cpu-usage"; + options.GreaterThan = 5; + options.SlidingWindowDuration = TimeSpan.FromSeconds(2); + }) + .AddExecuteActionAppAction("TextFileOutput", ExpectedFilePath, ExpectedFileContent) + .SetActionLimits(count: ExpectedActionCountLimit); + + runner.ConfigurationFromEnvironment.CreateCollectionRule(StartupRuleName) + .SetStartupTrigger(); + + ruleCompletedTask_Startup = runner.WaitForCollectionRuleCompleteAsync(StartupRuleName); + ruleCompletedTask_NonStartup = runner.WaitForCollectionRuleCompleteAsync(NonStartupRuleName); + }); + } + + private void ValidateCollectionRuleDescriptions(Dictionary expectedCollectionRuleDescriptions, Dictionary actualCollectionRuleDescriptions) + { + Assert.Equal(actualCollectionRuleDescriptions.Keys.Count, expectedCollectionRuleDescriptions.Keys.Count); + + foreach (var key in actualCollectionRuleDescriptions.Keys) + { + CollectionRuleDescription actualDescription = actualCollectionRuleDescriptions[key]; + CollectionRuleDescription expectedDescription = expectedCollectionRuleDescriptions[key]; + + Assert.Equal(expectedDescription, actualDescription); + } + } +#endif + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs index ba0cae77ee5..a97e8f89014 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/CollectionRuleTests.cs @@ -9,14 +9,12 @@ using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Tools.Monitor; -using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers; using Microsoft.Extensions.DependencyInjection; using System; using System.IO; using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -214,7 +212,7 @@ await ScenarioRunner.SingleTarget( { runner.ConfigurationFromEnvironment.CreateCollectionRule(DefaultRuleName) .SetStartupTrigger() - .AddProcessNameFilter(DotNetHost.HostExeNameWithoutExtension); + .AddProcessNameFilter(DotNetHost.ExeNameWithoutExtension); startedTask = runner.WaitForCollectionRuleStartedAsync(DefaultRuleName); }); @@ -267,7 +265,7 @@ public async Task CollectionRule_ConfigurationChangeTest(DiagnosticPortConnectio out string diagnosticPortPath); await using MonitorCollectRunner toolRunner = new(_outputHelper); - toolRunner.ConnectionMode = mode; + toolRunner.ConnectionModeViaCommandLine = mode; toolRunner.DiagnosticPortPath = diagnosticPortPath; toolRunner.DisableAuthentication = true; @@ -325,7 +323,7 @@ public async Task CollectionRule_StoppedOnExitTest(DiagnosticPortConnectionMode out string diagnosticPortPath); await using MonitorCollectRunner toolRunner = new(_outputHelper); - toolRunner.ConnectionMode = mode; + toolRunner.ConnectionModeViaCommandLine = mode; toolRunner.DiagnosticPortPath = diagnosticPortPath; toolRunner.DisableAuthentication = true; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DiagnosticPortTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DiagnosticPortTests.cs new file mode 100644 index 00000000000..77508ef2f19 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/DiagnosticPortTests.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + public sealed class DiagnosticPortTests + { + private readonly ITestOutputHelper _outputHelper; + + public DiagnosticPortTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + /// + /// When setting the default shared path in connect mode, a server socket should not be created. + /// + [Fact] + public async Task DefaultDiagnosticPort_NotSupported_ConnectMode() + { + using TemporaryDirectory defaultSharedTempDir = new(_outputHelper); + + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.ConnectionModeViaCommandLine = WebApi.DiagnosticPortConnectionMode.Connect; + toolRunner.ConfigurationFromEnvironment.SetDefaultSharedPath(defaultSharedTempDir.FullName); + + await toolRunner.StartAsync(); + + AssertDefaultDiagnosticPortNotExists(defaultSharedTempDir); + } + + /// + /// When setting the default shared path in listen mode, a server socket should not be created + /// under the default shared path if the endpoint for the diagnostic port is specified. + /// + [Fact] + public async Task DefaultDiagnosticPort_NotSupported_ListenModeWithSpecifiedPort() + { + using TemporaryDirectory defaultSharedTempDir = new(_outputHelper); + + DiagnosticPortHelper.Generate( + DiagnosticPortConnectionMode.Listen, + out _, + out string diagnosticPortPath); + + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.ConnectionModeViaCommandLine = DiagnosticPortConnectionMode.Listen; + toolRunner.DiagnosticPortPath = diagnosticPortPath; + toolRunner.ConfigurationFromEnvironment.SetDefaultSharedPath(defaultSharedTempDir.FullName); + + await toolRunner.StartAsync(); + + AssertDefaultDiagnosticPortNotExists(defaultSharedTempDir); + } + + /// + /// When setting the default shared path in listen mode on non-Windows platform, + /// a server socket should be created under the default shared path. + /// + [ConditionalFact(typeof(TestConditions), nameof(TestConditions.IsNotWindows))] + public async Task DefaultDiagnosticPort_Supported_ListenModeOnNonWindows() + { + using TemporaryDirectory defaultSharedTempDir = new(_outputHelper); + + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.ConfigurationFromEnvironment.SetConnectionMode(DiagnosticPortConnectionMode.Listen); + toolRunner.ConfigurationFromEnvironment.SetDefaultSharedPath(defaultSharedTempDir.FullName); + + await toolRunner.StartAsync(); + + AssertDefaultDiagnosticPortExists(defaultSharedTempDir); + } + + /// + /// When setting the default shared path in listen mode on Windows platform, + /// a server socket should not be created under the default shared path. + /// + [ConditionalFact(typeof(TestConditions), nameof(TestConditions.IsWindows))] + public async Task DefaultDiagnosticPort_NotSupported_ListenModeOnWindows() + { + using TemporaryDirectory defaultSharedTempDir = new(_outputHelper); + + await using MonitorCollectRunner toolRunner = new(_outputHelper); + toolRunner.ConfigurationFromEnvironment.SetConnectionMode(DiagnosticPortConnectionMode.Listen); + toolRunner.ConfigurationFromEnvironment.SetDefaultSharedPath(defaultSharedTempDir.FullName); + + // dotnet-monitor will fail to start due to misconfigured diagnostic port + await Assert.ThrowsAsync(() => toolRunner.StartAsync()); + + AssertDefaultDiagnosticPortNotExists(defaultSharedTempDir); + } + + private static string GetDefaultSharedSocketPath(string defaultSharedPath) + { + return Path.Combine(defaultSharedPath, ToolIdentifiers.DefaultSocketName); + } + + private static void AssertDefaultDiagnosticPortExists(TemporaryDirectory dir) + { + string diagnosticPort = GetDefaultSharedSocketPath(dir.FullName); + Assert.True(File.Exists(diagnosticPort), $"Expected socket to exist at '{diagnosticPort}'."); + } + + private static void AssertDefaultDiagnosticPortNotExists(TemporaryDirectory dir) + { + string diagnosticPort = GetDefaultSharedSocketPath(dir.FullName); + Assert.False(File.Exists(diagnosticPort), $"Expected socket to not exist at '{diagnosticPort}'."); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs index 3389b5b7d44..a0063520647 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/EgressTests.cs @@ -347,4 +347,4 @@ public void Dispose() _tempDirectory.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs index c60d335b211..5cc7bffb987 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClient.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Diagnostics.Monitoring.Options; -using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -134,7 +133,7 @@ private async Task GetProcessAsync(string processQuery, Cancellatio /// public Task> GetProcessEnvironmentAsync(int pid, CancellationToken token) { - return GetProcessEnvironmentAsync(GetProcessQuery(pid:pid), token); + return GetProcessEnvironmentAsync(GetProcessQuery(pid: pid), token); } /// @@ -142,7 +141,7 @@ public Task> GetProcessEnvironmentAsync(int pid, Canc /// public Task> GetProcessEnvironmentAsync(Guid uid, CancellationToken token) { - return GetProcessEnvironmentAsync(GetProcessQuery(uid:uid), token); + return GetProcessEnvironmentAsync(GetProcessQuery(uid: uid), token); } private async Task> GetProcessEnvironmentAsync(string processQuery, CancellationToken token) @@ -204,7 +203,7 @@ public async Task GetInfoAsync(CancellationToken token) /// public Task CaptureDumpAsync(int pid, DumpType dumpType, CancellationToken token) { - return CaptureDumpAsync(GetProcessQuery(pid:pid), dumpType, token); + return CaptureDumpAsync(GetProcessQuery(pid: pid), dumpType, token); } /// @@ -212,7 +211,7 @@ public Task CaptureDumpAsync(int pid, DumpType dumpType, C /// public Task CaptureDumpAsync(Guid uid, DumpType dumpType, CancellationToken token) { - return CaptureDumpAsync(GetProcessQuery(uid:uid), dumpType, token); + return CaptureDumpAsync(GetProcessQuery(uid: uid), dumpType, token); } private async Task CaptureDumpAsync(string processQuery, DumpType dumpType, CancellationToken token) @@ -244,12 +243,84 @@ await SendAndLogAsync( throw await CreateUnexpectedStatusCodeExceptionAsync(responseBox.Value).ConfigureAwait(false); } + /// + /// Capable of getting every combination of process query: PID, UID, and/or Name + /// Get /collectionrules?pid={pid}&uid={uid}&name={name} + /// + public Task> GetCollectionRulesDescriptionAsync(int? pid, Guid? uid, string name, CancellationToken token) + { + return GetCollectionRulesDescriptionAsync(GetProcessQuery(pid: pid, uid: uid, name: name), token); + } + + private async Task> GetCollectionRulesDescriptionAsync(string processQuery, CancellationToken token) + { + using HttpRequestMessage request = new(HttpMethod.Get, $"/collectionRules?" + processQuery); + request.Headers.Add(HeaderNames.Accept, ContentTypes.ApplicationJson); + + using HttpResponseMessage response = await SendAndLogAsync( + request, + HttpCompletionOption.ResponseContentRead, + token).ConfigureAwait(false); + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + ValidateContentType(response, ContentTypes.ApplicationJson); + return await ReadContentAsync>(response).ConfigureAwait(false); + case HttpStatusCode.BadRequest: + ValidateContentType(response, ContentTypes.ApplicationProblemJson); + throw await CreateValidationProblemDetailsExceptionAsync(response).ConfigureAwait(false); + case HttpStatusCode.Unauthorized: + case HttpStatusCode.NotFound: + ThrowIfNotSuccess(response); + break; + } + + throw await CreateUnexpectedStatusCodeExceptionAsync(response).ConfigureAwait(false); + } + + /// + /// Capable of getting every combination of process query: PID, UID, and/or Name + /// GET /collectionrules/{collectionrulename}?pid={pid}&uid={uid}&name={name} + /// + public Task GetCollectionRuleDetailedDescriptionAsync(string collectionRuleName, int? pid, Guid? uid, string name, CancellationToken token) + { + return GetCollectionRuleDetailedDescriptionAsync(collectionRuleName, GetProcessQuery(pid: pid, uid: uid, name: name), token); + } + + private async Task GetCollectionRuleDetailedDescriptionAsync(string collectionRuleName, string processQuery, CancellationToken token) + { + using HttpRequestMessage request = new(HttpMethod.Get, $"/collectionRules/" + collectionRuleName + "?" + processQuery); + request.Headers.Add(HeaderNames.Accept, ContentTypes.ApplicationJson); + + using HttpResponseMessage response = await SendAndLogAsync( + request, + HttpCompletionOption.ResponseContentRead, + token).ConfigureAwait(false); + + switch (response.StatusCode) + { + case HttpStatusCode.OK: + ValidateContentType(response, ContentTypes.ApplicationJson); + return await ReadContentAsync(response).ConfigureAwait(false); + case HttpStatusCode.BadRequest: + ValidateContentType(response, ContentTypes.ApplicationProblemJson); + throw await CreateValidationProblemDetailsExceptionAsync(response).ConfigureAwait(false); + case HttpStatusCode.Unauthorized: + case HttpStatusCode.NotFound: + ThrowIfNotSuccess(response); + break; + } + + throw await CreateUnexpectedStatusCodeExceptionAsync(response).ConfigureAwait(false); + } + /// /// GET /logs?pid={pid}&level={logLevel}&durationSeconds={duration} /// public Task CaptureLogsAsync(int pid, TimeSpan duration, LogLevel? logLevel, LogFormat logFormat, CancellationToken token) { - return CaptureLogsAsync(GetProcessQuery(pid:pid), duration, logLevel, logFormat, token); + return CaptureLogsAsync(GetProcessQuery(pid: pid), duration, logLevel, logFormat, token); } /// @@ -257,7 +328,7 @@ public Task CaptureLogsAsync(int pid, TimeSpan duration, L /// public Task CaptureLogsAsync(Guid uid, TimeSpan duration, LogLevel? logLevel, LogFormat logFormat, CancellationToken token) { - return CaptureLogsAsync(GetProcessQuery(uid:uid), duration, logLevel, logFormat, token); + return CaptureLogsAsync(GetProcessQuery(uid: uid), duration, logLevel, logFormat, token); } private Task CaptureLogsAsync(string processQuery, TimeSpan duration, LogLevel? logLevel, LogFormat logFormat, CancellationToken token) @@ -275,7 +346,7 @@ private Task CaptureLogsAsync(string processQuery, TimeSpa /// public Task CaptureLogsAsync(int pid, TimeSpan duration, LogsConfiguration configuration, LogFormat logFormat, CancellationToken token) { - return CaptureLogsAsync(GetProcessQuery(pid:pid), duration, configuration, logFormat, token); + return CaptureLogsAsync(GetProcessQuery(pid: pid), duration, configuration, logFormat, token); } private Task CaptureLogsAsync(string processQuery, TimeSpan duration, LogsConfiguration configuration, LogFormat logFormat, CancellationToken token) @@ -405,6 +476,37 @@ await SendAndLogAsync( throw await CreateUnexpectedStatusCodeExceptionAsync(responseBox.Value).ConfigureAwait(false); } + public async Task CaptureStacksAsync(int processId, bool plainText, CancellationToken token) + { + string uri = FormattableString.Invariant($"/stacks?pid={processId}"); + var contentType = plainText ? ContentTypes.TextPlain : ContentTypes.ApplicationJson; + using HttpRequestMessage request = new(HttpMethod.Get, uri); + request.Headers.Add(HeaderNames.Accept, contentType); + + using DisposableBox responseBox = new( + await SendAndLogAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + token).ConfigureAwait(false)); + + switch (responseBox.Value.StatusCode) + { + case HttpStatusCode.OK: + ValidateContentType(responseBox.Value, contentType); + return await ResponseStreamHolder.CreateAsync(responseBox).ConfigureAwait(false); + case HttpStatusCode.BadRequest: + ValidateContentType(responseBox.Value, ContentTypes.ApplicationProblemJson); + throw await CreateValidationProblemDetailsExceptionAsync(responseBox.Value).ConfigureAwait(false); + case HttpStatusCode.Unauthorized: + case HttpStatusCode.NotFound: + case HttpStatusCode.TooManyRequests: + ThrowIfNotSuccess(responseBox.Value); + break; + } + + throw await CreateUnexpectedStatusCodeExceptionAsync(responseBox.Value).ConfigureAwait(false); + } + public async Task ApiCall(string routeAndQuery, CancellationToken token) { using HttpRequestMessage request = new(HttpMethod.Get, routeAndQuery); @@ -481,7 +583,7 @@ public async Task CancelEgressOperation(Uri operation, Cancellat { using HttpRequestMessage request = new(HttpMethod.Delete, operation.ToString()); using HttpResponseMessage response = await SendAndLogAsync(request, HttpCompletionOption.ResponseContentRead, token).ConfigureAwait(false); - + switch (response.StatusCode) { case HttpStatusCode.OK: @@ -637,4 +739,4 @@ private static JsonSerializerOptions CreateJsonSerializeOptions() return options; } } -} \ No newline at end of file +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs index bd74103f6ff..544eda6c592 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/ApiClientExtensions.cs @@ -4,7 +4,6 @@ using Microsoft.Diagnostics.Monitoring.Options; using Microsoft.Diagnostics.Monitoring.TestCommon; -using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.Logging; using System; @@ -317,6 +316,23 @@ public static async Task CaptureMetricsAsync(this ApiClien return await client.CaptureMetricsAsync(processId, durationSeconds, metricsConfiguration, token: timeoutSource.Token).ConfigureAwait(false); } + /// + /// GET /stacks + /// + public static Task CaptureStacksAsync(this ApiClient client, int pid, bool plainText) + { + return client.CaptureStacksAsync(pid, plainText, TestTimeouts.HttpApi); + } + + /// + /// GET /stacks + /// + public static async Task CaptureStacksAsync(this ApiClient client, int pid, bool plainText, TimeSpan timeout) + { + using CancellationTokenSource timeoutSource = new(timeout); + return await client.CaptureStacksAsync(pid, plainText, timeoutSource.Token).ConfigureAwait(false); + } + /// /// GET /info /// @@ -334,6 +350,44 @@ public static async Task GetInfoAsync(this ApiClient client, return await client.GetInfoAsync(timeoutSource.Token).ConfigureAwait(false); } + /// + /// Capable of getting every combination of process query: PID, UID, and/or Name + /// GET /collectionrules?pid={pid}&uid={uid}&name={name} + /// + public static Task> GetCollectionRulesDescriptionAsync(this ApiClient client, int? pid, Guid? uid, string name) + { + return client.GetCollectionRulesDescriptionAsync(pid, uid, name, TestTimeouts.HttpApi); + } + + /// + /// Capable of getting every combination of process query: PID, UID, and/or Name + /// GET /collectionrules?pid={pid}&uid={uid}&name={name} + /// + public static async Task> GetCollectionRulesDescriptionAsync(this ApiClient client, int? pid, Guid? uid, string name, TimeSpan timeout) + { + using CancellationTokenSource timeoutSource = new(timeout); + return await client.GetCollectionRulesDescriptionAsync(pid, uid, name, timeoutSource.Token).ConfigureAwait(false); + } + + /// + /// Capable of getting every combination of process query: PID, UID, and/or Name + /// GET /collectionrules/{collectionrulename}?pid={pid}&uid={uid}&name={name} + /// + public static Task GetCollectionRuleDetailedDescriptionAsync(this ApiClient client, string collectionRuleName, int? pid, Guid? uid, string name) + { + return client.GetCollectionRuleDetailedDescriptionAsync(collectionRuleName, pid, uid, name, TestTimeouts.HttpApi); + } + + /// + /// Capable of getting every combination of process query: PID, UID, and/or Name + /// GET /collectionrules/{collectionrulename}?pid={pid}&uid={uid}&name={name} + /// + public static async Task GetCollectionRuleDetailedDescriptionAsync(this ApiClient client, string collectionRuleName, int? pid, Guid? uid, string name, TimeSpan timeout) + { + using CancellationTokenSource timeoutSource = new(timeout); + return await client.GetCollectionRuleDetailedDescriptionAsync(collectionRuleName, pid, uid, name, timeoutSource.Token).ConfigureAwait(false); + } + public static async Task EgressTraceAsync(this ApiClient client, int processId, int durationSeconds, string egressProvider) { using CancellationTokenSource timeoutSource = new(TestTimeouts.HttpApi); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/OperationResponse.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/OperationResponse.cs index 0108dac93d4..3ac2369ed7b 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/OperationResponse.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/HttpApi/OperationResponse.cs @@ -3,9 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Net; -using System.Text; namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/KestrelTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/KestrelTests.cs new file mode 100644 index 00000000000..cdd2c8f3193 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/KestrelTests.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + [Collection(DefaultCollectionFixture.Name)] + public sealed class KestrelTests + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly ITestOutputHelper _outputHelper; + + public KestrelTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) + { + _httpClientFactory = serviceProviderFixture.ServiceProvider.GetService(); + _outputHelper = outputHelper; + } + + [Fact] + public Task UrlBinding_NoOverrideWarning_CmdLine() + { + return ExecuteNoOverrideWarning(); + } + + [Fact] + public Task UrlBinding_NoOverrideWarning_DotNet() + { + return ExecuteNoOverrideWarning((runner, url) => + { + runner.DotNetUrls = url; + }); + } + + [Fact] + public Task UrlBinding_NoOverrideWarning_AspNetCore() + { + return ExecuteNoOverrideWarning((runner, url) => + { + // This should be overriden by the ASPNETCORE_Urls entry. If it is not, + // it will cause dotnet-monitor to not bind correctly and the /info route + // check will fail. + runner.DotNetUrls = "dotnet_invalid"; + runner.AspNetCoreUrls = url; + }); + } + + [Fact] + public Task UrlBinding_NoOverrideWarning_DotNetMonitor() + { + return ExecuteNoOverrideWarning((runner, url) => + { + // These should be overriden by the DotnetMonitor_Urls entry. If it is not, + // it will cause dotnet-monitor to not bind correctly and the /info route + // check will fail. + runner.DotNetUrls = "dotnet_invalid"; + runner.AspNetCoreUrls = "aspnetcore_invalid"; + + runner.DotNetMonitorUrls = url; + }); + } + + private async Task ExecuteNoOverrideWarning(Action configure = null) + { + await using MonitorCollectRunner runner = new(_outputHelper); + + runner.DisableAuthentication = true; + + configure?.Invoke(runner, "http://+:0"); + + await runner.StartAsync(); + + string address = await runner.GetDefaultAddressAsync(CancellationToken.None); + UriBuilder builder = new(address); + builder.Host = "localhost"; + + using HttpClient httpClient = await runner.CreateHttpClientAsync(_httpClientFactory, builder.Uri.ToString()); + ApiClient apiClient = new(_outputHelper, httpClient); + + // Test that the route (thus the URL) is viable + DotnetMonitorInfo info = await apiClient.GetInfoAsync(); + Assert.NotNull(info); + + // Test that the URL override warning is not present + Assert.False(runner.OverrodeServerUrls, "Override URL warning should not be present."); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LiveMetricsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LiveMetricsTests.cs index f339052d8d9..8887d7a6106 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LiveMetricsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LiveMetricsTests.cs @@ -2,27 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.AspNetCore.Http; using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; -using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Models; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Monitoring.WebApi.Models; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Channels; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -53,10 +41,10 @@ public Task TestDefaultMetrics() { using ResponseStreamHolder holder = await apiClient.CaptureMetricsAsync(await appRunner.ProcessIdTask, durationSeconds: 10); - - var metrics = GetAllMetrics(holder); - await ValidateMetrics(new []{ EventPipe.MonitoringSourceConfiguration.SystemRuntimeEventSourceName }, - new [] + + var metrics = LiveMetricsTestUtilities.GetAllMetrics(holder.Stream); + await LiveMetricsTestUtilities.ValidateMetrics(new[] { EventPipe.MonitoringSourceConfiguration.SystemRuntimeEventSourceName }, + new[] { "cpu-usage", "working-set", @@ -96,8 +84,8 @@ public Task TestCustomMetrics() } }); - var metrics = GetAllMetrics(holder); - await ValidateMetrics(new []{ EventPipe.MonitoringSourceConfiguration.SystemRuntimeEventSourceName }, + var metrics = LiveMetricsTestUtilities.GetAllMetrics(holder.Stream); + await LiveMetricsTestUtilities.ValidateMetrics(new[] { EventPipe.MonitoringSourceConfiguration.SystemRuntimeEventSourceName }, counterNames, metrics, strict: true); @@ -105,57 +93,5 @@ await ValidateMetrics(new []{ EventPipe.MonitoringSourceConfiguration.SystemRunt await appRunner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }); } - - private static async Task ValidateMetrics(IEnumerable expectedProviders, IEnumerable expectedNames, - IAsyncEnumerable actualMetrics, bool strict) - { - HashSet actualProviders = new(); - HashSet actualNames = new(); - - await AggregateMetrics(actualMetrics, actualProviders, actualNames); - - CompareSets(new HashSet(expectedProviders), actualProviders, strict); - CompareSets(new HashSet(expectedNames), actualNames, strict); - } - - private static void CompareSets(HashSet expected, HashSet actual, bool strict) - { - bool matched = true; - if (strict && !expected.SetEquals(actual)) - { - expected.SymmetricExceptWith(actual); - matched = false; - } - else if (!strict && !expected.IsSubsetOf(actual)) - { - //actual must contain at least the elements in expected, but can contain more - expected.ExceptWith(actual); - matched = false; - } - Assert.True(matched, "Missing or unexpected elements: " + string.Join(",", expected)); - } - - private static async Task AggregateMetrics(IAsyncEnumerable actualMetrics, - HashSet providers, - HashSet names) - { - await foreach (CounterPayload counter in actualMetrics) - { - providers.Add(counter.Provider); - names.Add(counter.Name); - } - } - - private static async IAsyncEnumerable GetAllMetrics(ResponseStreamHolder holder) - { - using var reader = new StreamReader(holder.Stream); - - string entry = string.Empty; - while ((entry = await reader.ReadLineAsync()) != null) - { - Assert.Equal(StreamingLogger.JsonSequenceRecordSeparator, (byte)entry[0]); - yield return JsonSerializer.Deserialize(entry.Substring(1)); - } - } } -} \ No newline at end of file +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LogsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LogsTests.cs index ced51bb3a31..6c8dc980339 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LogsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/LogsTests.cs @@ -390,37 +390,16 @@ public Task LogsWildcardTest(DiagnosticPortConnectionMode mode, LogFormat logFor }, logFormat); } - private Task ValidateLogsAsync( DiagnosticPortConnectionMode mode, LogLevel? logLevel, Func, Task> callback, LogFormat logFormat) { - return Retry(() => ValidateLogsAsyncCore(mode, logLevel, callback, logFormat)); - } + Func> captureLogs = + (client, pid) => client.CaptureLogsAsync(pid, CommonTestTimeouts.LogsDuration, logLevel, logFormat); - private Task ValidateLogsAsyncCore( - DiagnosticPortConnectionMode mode, - LogLevel? logLevel, - Func, Task> callback, - LogFormat logFormat) - { - return ScenarioRunner.SingleTarget( - _outputHelper, - _httpClientFactory, - mode, - TestAppScenarios.Logger.Name, - appValidate: async (runner, client) => - await ValidateResponseStream( - runner, - client.CaptureLogsAsync( - await runner.ProcessIdTask, - CommonTestTimeouts.LogsDuration, - logLevel, - logFormat), - callback, - logFormat)); + return Retry(() => ValidateLogsAsyncCore(mode, captureLogs, callback, logFormat)); } private Task ValidateLogsAsync( @@ -429,12 +408,15 @@ private Task ValidateLogsAsync( Func, Task> callback, LogFormat logFormat) { - return Retry(() => ValidateLogsAsyncCore(mode, configuration, callback, logFormat)); + Func> captureLogs = + (client, pid) => client.CaptureLogsAsync(pid, CommonTestTimeouts.LogsDuration, configuration, logFormat); + + return Retry(() => ValidateLogsAsyncCore(mode, captureLogs, callback, logFormat)); } private Task ValidateLogsAsyncCore( DiagnosticPortConnectionMode mode, - LogsConfiguration configuration, + Func> captureLogs, Func, Task> callback, LogFormat logFormat) { @@ -444,41 +426,28 @@ private Task ValidateLogsAsyncCore( mode, TestAppScenarios.Logger.Name, appValidate: async (runner, client) => - await ValidateResponseStream( - runner, - client.CaptureLogsAsync( - await runner.ProcessIdTask, - CommonTestTimeouts.LogsDuration, - configuration, - logFormat), - callback, - logFormat)); - } - - private async Task ValidateResponseStream(AppRunner runner, Task holderTask, Func, Task> callback, LogFormat logFormat) - { - Assert.NotNull(runner); - Assert.NotNull(holderTask); - Assert.NotNull(callback); + { + Task holderTask = captureLogs(client, await runner.ProcessIdTask); - // CONSIDER: Give dotnet-monitor some time to start the logs pipeline before having the target - // application start logging. It would be best if dotnet-monitor could write a console event - // (at Debug or Trace level) for when the pipeline has started. This would require dotnet-monitor - // to know when the pipeline started and is waiting for logging data. - await Task.Delay(TimeSpan.FromSeconds(3)); + // CONSIDER: Give dotnet-monitor some time to start the logs pipeline before having the target + // application start logging. It would be best if dotnet-monitor could write a console event + // (at Debug or Trace level) for when the pipeline has started. This would require dotnet-monitor + // to know when the pipeline started and is waiting for logging data. + await Task.Delay(TimeSpan.FromSeconds(3)); - // Start logging in the target application - await runner.SendCommandAsync(TestAppScenarios.Logger.Commands.StartLogging); + // Start logging in the target application + await runner.SendCommandAsync(TestAppScenarios.Logger.Commands.StartLogging); - // Await the holder after sending the message to start logging so that ASP.NET can send chunked responses. - // If awaited before sending the message, ASP.NET will not send the complete set of headers because no data - // is written into the response stream. Since HttpClient.SendAsync has to wait for the complete set of headers, - // the /logs invocation would run and complete with no log events. To avoid this, the /logs invocation is started, - // then the StartLogging message is sent, and finally the holder is awaited. - using ResponseStreamHolder holder = await holderTask; - Assert.NotNull(holder); + // Await the holder after sending the message to start logging so that ASP.NET can send chunked responses. + // If awaited before sending the message, ASP.NET will not send the complete set of headers because no data + // is written into the response stream. Since HttpClient.SendAsync has to wait for the complete set of headers, + // the /logs invocation would run and complete with no log events. To avoid this, the /logs invocation is started, + // then the StartLogging message is sent, and finally the holder is awaited. + using ResponseStreamHolder holder = await holderTask; + Assert.NotNull(holder); - await LogsTestUtilities.ValidateLogsEquality(holder.Stream, callback, logFormat, _outputHelper); + await LogsTestUtilities.ValidateLogsEquality(holder.Stream, callback, logFormat, _outputHelper); + }); } private async Task Retry(Func func, int attemptCount = 5) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs index 2f42a8544df..03393011b45 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/MetricsTests.cs @@ -4,20 +4,18 @@ using Microsoft.AspNetCore.Http; using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; -using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Extensions.DependencyInjection; -using System; using System.Net; using System.Net.Http; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; -using Microsoft.Diagnostics.Tools.Monitor; -using Microsoft.Diagnostics.Monitoring.WebApi; namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj index 6354b675bbe..14778811d62 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.csproj @@ -16,13 +16,19 @@ + + + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs index cbadb86f7d4..600e426c773 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Options/OptionsExtensions.cs @@ -36,6 +36,16 @@ public static RootOptions AddFileSystemEgress(this RootOptions options, string n return options; } + public static RootOptions AddGlobalCounter(this RootOptions options, int intervalSeconds) + { + options.GlobalCounter = new GlobalCounterOptions + { + IntervalSeconds = intervalSeconds + }; + + return options; + } + public static CollectionRuleOptions CreateCollectionRule(this RootOptions rootOptions, string name) { CollectionRuleOptions options = new(); @@ -43,6 +53,42 @@ public static CollectionRuleOptions CreateCollectionRule(this RootOptions rootOp return options; } + public static RootOptions EnableInProcessFeatures(this RootOptions options) + { + if (null == options.InProcessFeatures) + { + options.InProcessFeatures = new Monitoring.Options.InProcessFeaturesOptions(); + } + + options.InProcessFeatures.Enabled = true; + + return options; + } + + public static RootOptions SetConnectionMode(this RootOptions options, DiagnosticPortConnectionMode connectionMode) + { + if (null == options.DiagnosticPort) + { + options.DiagnosticPort = new DiagnosticPortOptions(); + } + + options.DiagnosticPort.ConnectionMode = connectionMode; + + return options; + } + + public static RootOptions SetDefaultSharedPath(this RootOptions options, string directoryPath) + { + if (null == options.Storage) + { + options.Storage = new StorageOptions(); + } + + options.Storage.DefaultSharedPath = directoryPath; + + return options; + } + public static RootOptions SetDumpTempFolder(this RootOptions options, string directoryPath) { if (null == options.Storage) @@ -123,7 +169,7 @@ public static RootOptions UseApiKey(this RootOptions options, string algorithmNa case SecurityAlgorithms.HmacSha256: case SecurityAlgorithms.HmacSha384: case SecurityAlgorithms.HmacSha512: - HMAC hmac = HMAC.Create(GetHmacAlgorithmFromName(algorithmName)); + HMAC hmac = GetHmacAlgorithmFromName(algorithmName); SymmetricSecurityKey hmacSecKey = new SymmetricSecurityKey(hmac.Key); signingCreds = new SigningCredentials(hmacSecKey, algorithmName); exportableJwk = JsonWebKeyConverter.ConvertFromSymmetricSecurityKey(hmacSecKey); @@ -152,16 +198,16 @@ public static RootOptions UseApiKey(this RootOptions options, string algorithmNa return options; } - private static string GetHmacAlgorithmFromName(string algorithmName) + private static HMAC GetHmacAlgorithmFromName(string algorithmName) { switch (algorithmName) { case SecurityAlgorithms.HmacSha256: - return typeof(HMACSHA256).FullName; + return new HMACSHA256(); case SecurityAlgorithms.HmacSha384: - return typeof(HMACSHA384).FullName; + return new HMACSHA384(); case SecurityAlgorithms.HmacSha512: - return typeof(HMACSHA512).FullName; + return new HMACSHA512(); default: throw new ArgumentException($"Algorithm name '{algorithmName}' not supported", nameof(algorithmName)); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs index 99c45ebe192..3f4ad532806 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/ProcessTests.cs @@ -104,7 +104,7 @@ public async Task MultiProcessIdentificationTest(DiagnosticPortConnectionMode mo out string diagnosticPortPath); await using MonitorCollectRunner toolRunner = new(_outputHelper); - toolRunner.ConnectionMode = mode; + toolRunner.ConnectionModeViaCommandLine = mode; toolRunner.DiagnosticPortPath = diagnosticPortPath; toolRunner.DisableAuthentication = true; await toolRunner.StartAsync(); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs index 60a2a7e57dc..90aa9a984c6 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorCollectRunner.cs @@ -45,16 +45,16 @@ internal sealed partial class MonitorCollectRunner : MonitorRunner public event Action WarnPrivateKey; /// - /// The mode of the diagnostic port connection. Default is - /// (the tool is searching for apps that are in listen mode). + /// The mode of the diagnostic port connection. /// /// + /// Set to if tool needs to discover the diagnostic port for each target process. /// Set to if tool needs to establish the diagnostic port listener. /// - public DiagnosticPortConnectionMode ConnectionMode { get; set; } = DiagnosticPortConnectionMode.Connect; + public DiagnosticPortConnectionMode? ConnectionModeViaCommandLine { get; set; } /// - /// Path of the diagnostic port to establish when is . + /// Path of the diagnostic port to establish when is . /// public string DiagnosticPortPath { get; set; } @@ -78,6 +78,26 @@ internal sealed partial class MonitorCollectRunner : MonitorRunner /// public bool DisableMetricsViaCommandLine { get; set; } + /// + /// Reports whether the server URLs were overriden by UseKestrel/ConfigureKestrel code. + /// + public bool OverrodeServerUrls { get; private set; } + + /// + /// Urls used for the DOTNET_Urls environment variable. + /// + public string DotNetUrls { get; set; } + + /// + /// Urls used for the ASPNETCORE_Urls environment variable. + /// + public string AspNetCoreUrls { get; set; } + + /// + /// Urls used for the DOTNETMONITOR_Urls environment variable. + /// + public string DotNetMonitorUrls { get; set; } + public MonitorCollectRunner(ITestOutputHelper outputHelper) : base(outputHelper) @@ -109,6 +129,19 @@ public async Task StartAsync(CancellationToken token) argsList.Add("--urls"); argsList.Add("http://127.0.0.1:0"); + if (!string.IsNullOrEmpty(DotNetUrls)) + { + SetEnvironmentVariable("DOTNET_Urls", DotNetUrls); + } + if (!string.IsNullOrEmpty(AspNetCoreUrls)) + { + SetEnvironmentVariable("ASPNETCORE_Urls", AspNetCoreUrls); + } + if (!string.IsNullOrEmpty(DotNetMonitorUrls)) + { + SetEnvironmentVariable("DOTNETMONITOR_Urls", DotNetMonitorUrls); + } + if (DisableMetricsViaCommandLine) { argsList.Add("--metrics:false"); @@ -119,7 +152,7 @@ public async Task StartAsync(CancellationToken token) argsList.Add("http://127.0.0.1:0"); } - if (ConnectionMode == DiagnosticPortConnectionMode.Listen) + if (ConnectionModeViaCommandLine == DiagnosticPortConnectionMode.Listen) { argsList.Add("--diagnostic-port"); if (string.IsNullOrEmpty(DiagnosticPortPath)) @@ -133,7 +166,7 @@ public async Task StartAsync(CancellationToken token) { argsList.Add("--no-auth"); } - + if (DisableHttpEgress) { argsList.Add("--no-http-egress"); @@ -161,22 +194,33 @@ public async Task StartAsync(CancellationToken token) protected override void StandardOutputCallback(string line) { - ConsoleLogEvent logEvent = JsonSerializer.Deserialize(line); + try + { + ConsoleLogEvent logEvent = JsonSerializer.Deserialize(line); - switch (logEvent.Category) + switch (logEvent.Category) + { + case "Microsoft.AspNetCore.Server.Kestrel": + HandleKestrelEvent(logEvent); + break; + case "Microsoft.Hosting.Lifetime": + HandleLifetimeEvent(logEvent); + break; + case "Microsoft.Diagnostics.Tools.Monitor.Startup": + HandleStartupEvent(logEvent); + break; + case "Microsoft.Diagnostics.Tools.Monitor.CollectionRules.CollectionRuleService": + HandleCollectionRuleEvent(logEvent); + break; + default: + HandleGenericLogEvent(logEvent); + break; + } + } + catch (JsonException) { - case "Microsoft.Hosting.Lifetime": - HandleLifetimeEvent(logEvent); - break; - case "Microsoft.Diagnostics.Tools.Monitor.Startup": - HandleStartupEvent(logEvent); - break; - case "Microsoft.Diagnostics.Tools.Monitor.CollectionRules.CollectionRuleService": - HandleCollectionRuleEvent(logEvent); - break; - default: - HandleGenericLogEvent(logEvent); - break; + // Unable to parse the output. These could be lines writen to stdout that are not JSON formatted. + _outputHelper.WriteLine("Unable to JSON parse stdout line: {0}", line); } } @@ -246,6 +290,14 @@ private void HandleStartupEvent(ConsoleLogEvent logEvent) } } + private void HandleKestrelEvent(ConsoleLogEvent logEvent) + { + if (logEvent.Message.StartsWith("Overriding address(es)")) + { + OverrodeServerUrls = true; + } + } + private void HandleGenericLogEvent(ConsoleLogEvent logEvent) { switch ((LoggingEventIds)logEvent.EventId) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs index 86449459ede..82e702d8750 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorGenerateKeyRunner.cs @@ -25,7 +25,7 @@ internal sealed class MonitorGenerateKeyRunner : MonitorRunner // Completion source containing the bearer token emitted by the generatekey command private readonly TaskCompletionSource _bearerTokenTaskSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly Regex _bearerTokenRegex = + private readonly Regex _bearerTokenRegex = new Regex("^Authorization: Bearer (?[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+)$", RegexOptions.Compiled); private readonly Regex _authorizationHeaderRegex = new Regex("^Bearer (?[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+)$", RegexOptions.Compiled); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs index 2060c3a2f71..7322f0c0bbf 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunner.cs @@ -21,6 +21,9 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners /// internal class MonitorRunner : IAsyncDisposable { + private const string TestHostingStartupAssemblyName = "Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup"; + private const string TestStartupHookAssemblyName = "Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook"; + protected readonly object _lock = new(); protected readonly ITestOutputHelper _outputHelper; @@ -36,6 +39,11 @@ internal class MonitorRunner : IAsyncDisposable /// public RootOptions ConfigurationFromEnvironment { get; } = new(); + /// + /// Determines whether the stacks feaure is enabled. + /// + public bool EnableCallStacksFeature { get; set; } + /// /// Gets the task for the underlying 's /// which is used to wait for process exit. @@ -56,6 +64,17 @@ internal class MonitorRunner : IAsyncDisposable #endif ); + private static string TestStartupHookPath => + AssemblyHelper.GetAssemblyArtifactBinPath( + Assembly.GetExecutingAssembly(), + TestStartupHookAssemblyName, +#if NET7_0_OR_GREATER + TargetFrameworkMoniker.Net70 +#else + TargetFrameworkMoniker.Net60 +#endif + ); + private string SharedConfigDirectoryPath => Path.Combine(TempPath, "SharedConfig"); @@ -72,12 +91,6 @@ public MonitorRunner(ITestOutputHelper outputHelper) { _outputHelper = new PrefixedOutputHelper(outputHelper, "[Monitor] "); - // Must tell runner this is an ASP.NET Core app so that it can choose - // the correct ASP.NET Core version (which can be different than the .NET - // version, especially for prereleases). - _runner.FrameworkReference = DotNetFrameworkReference.Microsoft_AspNetCore_App; - _runner.TargetFramework = TargetFrameworkMoniker.Net60; - _adapter = new LoggingRunnerAdapter(_outputHelper, _runner); _adapter.ReceivedStandardOutputLine += StandardOutputCallback; @@ -142,6 +155,18 @@ public virtual async Task StartAsync(string command, string[] args, Cancellation // Override the user config directory _adapter.Environment.Add("DotnetMonitorTestSettings__UserConfigDirectoryOverride", UserConfigDirectoryPath); + // Enable experimental stacks feature + if (EnableCallStacksFeature) + { + _adapter.Environment.Add(ExperimentalFlags.Feature_CallStacks, "true"); + } + + // Ensures that the TestStartupHook is loaded early so it helps resolve other test assemblies + _adapter.Environment.Add("DOTNET_STARTUP_HOOKS", TestStartupHookPath); + + // Allow TestHostingStartup to participate in host building in the tool + _adapter.Environment.Add("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", TestHostingStartupAssemblyName); + // Set configuration via environment variables var configurationViaEnvironment = ConfigurationFromEnvironment.ToEnvironmentConfiguration(useDotnetMonitorPrefix: true); if (configurationViaEnvironment.Count > 0) @@ -189,5 +214,10 @@ public async Task WriteUserSettingsAsync(RootOptions options) _outputHelper.WriteLine("Wrote user settings."); } + + protected void SetEnvironmentVariable(string name, string value) + { + _adapter.Environment[name] = value; + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs index 8ebdf965eac..c336ddd8a8c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/MonitorRunnerExtensions.cs @@ -14,6 +14,42 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners { internal static class MonitorCollectRunnerExtensions { + /// + /// Creates a over the address of the . + /// + public static Task CreateHttpClientAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, string address) + { + return runner.CreateHttpClientAsync(factory, address, TestTimeouts.HttpApi); + } + + /// + /// Creates a over the address of the . + /// + public static async Task CreateHttpClientAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, string address, TimeSpan timeout) + { + using CancellationTokenSource cancellation = new(timeout); + + return await runner.CreateHttpClientAsync(factory, address, Extensions.Options.Options.DefaultName, cancellation.Token); + } + + /// + /// Creates a named over the address of the . + /// + public static async Task CreateHttpClientAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, string address, string name, CancellationToken token) + { + HttpClient client = factory.CreateClient(name); + + client.BaseAddress = new Uri(address, UriKind.Absolute); + + if (runner.UseTempApiKey) + { + string monitorApiKey = await runner.GetMonitorApiKey(token); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, monitorApiKey); + } + + return client; + } + /// /// Creates a over the default address of the . /// @@ -43,18 +79,11 @@ public static Task CreateHttpClientDefaultAddressAsync(this MonitorC /// public static async Task CreateHttpClientDefaultAddressAsync(this MonitorCollectRunner runner, IHttpClientFactory factory, string name, TimeSpan timeout) { - HttpClient client = factory.CreateClient(name); - using CancellationTokenSource cancellation = new(timeout); - client.BaseAddress = new Uri(await runner.GetDefaultAddressAsync(cancellation.Token), UriKind.Absolute); - if (runner.UseTempApiKey) - { - string monitorApiKey = await runner.GetMonitorApiKey(cancellation.Token); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(AuthConstants.ApiKeySchema, monitorApiKey); - } + string address = await runner.GetDefaultAddressAsync(cancellation.Token); - return client; + return await runner.CreateHttpClientAsync(factory, address, name, cancellation.Token); } /// diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs index 8eecddcf045..76f84a473e8 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/Runners/ScenarioRunner.cs @@ -26,7 +26,8 @@ public static async Task SingleTarget( Func postAppValidate = null, Action configureApp = null, Action configureTool = null, - bool disableHttpEgress = false) + bool disableHttpEgress = false, + string profilerLogLevel = null) { DiagnosticPortHelper.Generate( mode, @@ -34,7 +35,7 @@ public static async Task SingleTarget( out string diagnosticPortPath); await using MonitorCollectRunner toolRunner = new(outputHelper); - toolRunner.ConnectionMode = mode; + toolRunner.ConnectionModeViaCommandLine = mode; toolRunner.DiagnosticPortPath = diagnosticPortPath; toolRunner.DisableAuthentication = true; toolRunner.DisableHttpEgress = disableHttpEgress; @@ -47,6 +48,7 @@ public static async Task SingleTarget( ApiClient apiClient = new(outputHelper, httpClient); AppRunner appRunner = new(outputHelper, Assembly.GetExecutingAssembly()); + appRunner.ProfilerLogLevel = profilerLogLevel; appRunner.ConnectionMode = appConnectionMode; appRunner.DiagnosticPortPath = diagnosticPortPath; appRunner.ScenarioName = scenarioName; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs new file mode 100644 index 00000000000..509197e4af3 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/StacksTests.cs @@ -0,0 +1,357 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Fixtures; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.HttpApi; +using Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests.Runners; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests +{ + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + [Collection(DefaultCollectionFixture.Name)] + public class StacksTests + { +#if NET6_0_OR_GREATER + + private readonly IHttpClientFactory _httpClientFactory; + private readonly ITestOutputHelper _outputHelper; + private readonly TemporaryDirectory _tempDirectory; + + private const string ExpectedModule = @"Microsoft.Diagnostics.Monitoring.UnitTestApp.dll"; + private const string ExpectedClass = @"Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios.StacksWorker+StacksWorkerNested`1[System.Int32]"; + private const string ExpectedFunction = @"DoWork[System.Int64]"; + private const string ExpectedCallbackFunction = @"Callback"; + private const string NativeFrame = "[NativeFrame]"; + + public StacksTests(ITestOutputHelper outputHelper, ServiceProviderFixture serviceProviderFixture) + { + _httpClientFactory = serviceProviderFixture.ServiceProvider.GetService(); + _outputHelper = outputHelper; + _tempDirectory = new TemporaryDirectory(_outputHelper); + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public Task TestPlainTextStacks(Architecture targetArchitecture) + { + return ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + WebApi.DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Stacks.Name, + appValidate: async (runner, client) => + { + int processId = await runner.ProcessIdTask; + + using ResponseStreamHolder holder = await client.CaptureStacksAsync(processId, plainText: true); + Assert.NotNull(holder); + + using StreamReader reader = new StreamReader(holder.Stream); + string line = null; + + string[] expectedFrames = + { + FormatFrame(ExpectedModule, ExpectedClass, ExpectedCallbackFunction), + NativeFrame, + FormatFrame(ExpectedModule, ExpectedClass, ExpectedFunction), + }; + + var actualFrames = new List(); + + while ((line = reader.ReadLine()) != null) + { + line = line.TrimStart(); + if (actualFrames.Count == expectedFrames.Length) + { + break; + } + if ((line == expectedFrames.First()) || (actualFrames.Count > 0)) + { + actualFrames.Add(line); + } + } + + Assert.Equal(expectedFrames, actualFrames); + + await runner.SendCommandAsync(TestAppScenarios.Stacks.Commands.Continue); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.EnableInProcessFeatures(); + runner.EnableCallStacksFeature = true; + }); + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public Task TestJsonStacks(Architecture targetArchitecture) + { + return ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + WebApi.DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Stacks.Name, + appValidate: async (runner, client) => + { + int processId = await runner.ProcessIdTask; + + using ResponseStreamHolder holder = await client.CaptureStacksAsync(processId, plainText: false); + Assert.NotNull(holder); + + WebApi.Models.CallStackResult result = await JsonSerializer.DeserializeAsync(holder.Stream); + WebApi.Models.CallStackFrame[] expectedFrames = ExpectedFrames(); + IList actualFrames = GetActualFrames(result, expectedFrames.First(), expectedFrames.Length); + + Assert.Equal(expectedFrames.Length, actualFrames.Count); + for (int i = 0; i < expectedFrames.Length; i++) + { + Assert.True(AreFramesEqual(expectedFrames[i], actualFrames[i])); + } + + await runner.SendCommandAsync(TestAppScenarios.Stacks.Commands.Continue); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.EnableInProcessFeatures(); + runner.EnableCallStacksFeature = true; + }); + } + + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public Task TestRepeatStackCalls(Architecture targetArchitecture) + { + return ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + WebApi.DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Stacks.Name, + appValidate: async (runner, client) => + { + int processId = await runner.ProcessIdTask; + + using ResponseStreamHolder holder1 = await client.CaptureStacksAsync(processId, plainText: false); + Assert.NotNull(holder1); + + WebApi.Models.CallStackResult result1 = await JsonSerializer.DeserializeAsync(holder1.Stream); + + using ResponseStreamHolder holder2 = await client.CaptureStacksAsync(processId, plainText: false); + Assert.NotNull(holder2); + + WebApi.Models.CallStackResult result2 = await JsonSerializer.DeserializeAsync(holder2.Stream); + + Assert.NotEmpty(result1.Stacks); + Assert.NotEmpty(result2.Stacks); + + Assert.NotEmpty(result1.Stacks.SelectMany(s => s.Frames)); + Assert.NotEmpty(result2.Stacks.SelectMany(s => s.Frames)); + + + await runner.SendCommandAsync(TestAppScenarios.Stacks.Commands.Continue); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.EnableInProcessFeatures(); + runner.EnableCallStacksFeature = true; + }); + } + + /// + /// Verifies that the /stacks route returns 404 if the stacks feature is not enabled. + /// + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public Task TestFeatureNotEnabled(Architecture targetArchitecture) + { + return ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + WebApi.DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Stacks.Name, + appValidate: async (runner, client) => + { + int processId = await runner.ProcessIdTask; + + ApiStatusCodeException ex = await Assert.ThrowsAsync(() => client.CaptureStacksAsync(processId, plainText: false)); + Assert.Equal(HttpStatusCode.NotFound, ex.StatusCode); + + await runner.SendCommandAsync(TestAppScenarios.Stacks.Commands.Continue); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + }, + configureTool: runner => + { + runner.ConfigurationFromEnvironment.EnableInProcessFeatures(); + // Note that the Stacks experimental feature is not enabled + }); + } + + /// + /// Verifies that the /stacks route returns 404 if the in-process features are not enabled. + /// + [Theory] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public Task TestInProcessFeaturesNotEnabled(Architecture targetArchitecture) + { + return ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + WebApi.DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Stacks.Name, + appValidate: async (runner, client) => + { + int processId = await runner.ProcessIdTask; + + ApiStatusCodeException ex = await Assert.ThrowsAsync(() => client.CaptureStacksAsync(processId, plainText: false)); + Assert.Equal(HttpStatusCode.NotFound, ex.StatusCode); + + await runner.SendCommandAsync(TestAppScenarios.Stacks.Commands.Continue); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + }, + configureTool: runner => + { + runner.EnableCallStacksFeature = true; + // Note that the in-process features are not enabled + }); + } + + [Theory(Skip = "Disable unstable tests.")] + [MemberData(nameof(ProfilerHelper.GetArchitecture), MemberType = typeof(ProfilerHelper))] + public Task TestCollectStacksAction(Architecture targetArchitecture) + { + Task ruleCompletedTask = null; + + return ScenarioRunner.SingleTarget( + _outputHelper, + _httpClientFactory, + WebApi.DiagnosticPortConnectionMode.Listen, + TestAppScenarios.Stacks.Name, + appValidate: async (runner, client) => + { + await ruleCompletedTask; + + string[] files = Directory.GetFiles(_tempDirectory.FullName, "*"); + Assert.Single(files); + using FileStream stream = File.OpenRead(files.First()); + + WebApi.Models.CallStackResult result = await JsonSerializer.DeserializeAsync(stream); + WebApi.Models.CallStackFrame[] expectedFrames = ExpectedFrames(); + IList actualFrames = GetActualFrames(result, expectedFrames.First(), expectedFrames.Length); + + Assert.Equal(expectedFrames.Length, actualFrames.Count); + for (int i = 0; i < expectedFrames.Length; i++) + { + Assert.True(AreFramesEqual(expectedFrames[i], actualFrames[i])); + } + + await runner.SendCommandAsync(TestAppScenarios.Stacks.Commands.Continue); + }, + configureApp: runner => + { + runner.Architecture = targetArchitecture; + }, + configureTool: runner => + { + const string fileEgress = nameof(fileEgress); + runner.EnableCallStacksFeature = true; + runner.ConfigurationFromEnvironment + .EnableInProcessFeatures() + .AddFileSystemEgress(fileEgress, _tempDirectory.FullName) + .CreateCollectionRule("StacksCounterRule") + .SetEventCounterTrigger(options => + { + options.ProviderName = "StackScenario"; + options.CounterName = "Ready"; + options.GreaterThan = 0.0; + options.SlidingWindowDuration = TimeSpan.FromSeconds(5); + }) + .AddCollectStacksAction(fileEgress, Tools.Monitor.CollectionRules.Options.Actions.CallStackFormat.Json); + + ruleCompletedTask = runner.WaitForCollectionRuleActionsCompletedAsync("StacksCounterRule"); + }); + } + + private static string FormatFrame(string module, string @class, string function) => + FormattableString.Invariant($"{module}!{@class}.{function}"); + + private static bool AreFramesEqual(WebApi.Models.CallStackFrame left, WebApi.Models.CallStackFrame right) => + (left.ModuleName == right.ModuleName) && (left.ClassName == right.ClassName) && (left.MethodName == right.MethodName); + + private static IList GetActualFrames(WebApi.Models.CallStackResult result, WebApi.Models.CallStackFrame expectedFirstFrame, int expectedFrameCount) + { + var actualFrames = new List(); + foreach (WebApi.Models.CallStack stack in result.Stacks) + { + actualFrames.Clear(); + foreach (var frame in stack.Frames) + { + if (AreFramesEqual(expectedFirstFrame, frame) || actualFrames.Count > 0) + { + actualFrames.Add(frame); + if (actualFrames.Count == expectedFrameCount) + { + return actualFrames; + } + } + } + } + return actualFrames; + } + + private static WebApi.Models.CallStackFrame[] ExpectedFrames() => new WebApi.Models.CallStackFrame[] + { + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + ClassName = ExpectedClass, + MethodName = ExpectedCallbackFunction + }, + new WebApi.Models.CallStackFrame + { + ModuleName = NativeFrame, + ClassName = NativeFrame, + MethodName = NativeFrame + }, + new WebApi.Models.CallStackFrame + { + ModuleName = ExpectedModule, + ClassName = ExpectedClass, + MethodName = ExpectedFunction + } + }; +#endif + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestConditions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestConditions.cs index f950d65a397..c681209793a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestConditions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.FunctionalTests/TestConditions.cs @@ -28,5 +28,7 @@ public static bool IsDumpSupported public static bool IsNetCore31 => DotNetHost.BuiltTargetFrameworkMoniker == TargetFrameworkMoniker.NetCoreApp31; public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + public static bool IsNotWindows => !IsWindows; } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/BuildOutputNativeFileProvider.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/BuildOutputNativeFileProvider.cs new file mode 100644 index 00000000000..8f7d75e3a3f --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/BuildOutputNativeFileProvider.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Microsoft.Extensions.Primitives; +using System; +using System.IO; + +namespace Microsoft.Diagnostics.Tools.Monitor.Profiler +{ + /// + /// An abstraction around how native library files are found in the build output. + /// + internal sealed class BuildOutputNativeFileProvider : IFileProvider + { + private readonly string _nativeFileBasePath; + + private BuildOutputNativeFileProvider(string nativeFileBasePath) + { + _nativeFileBasePath = nativeFileBasePath; + } + + /// + /// Creates an that can return native files from the build output of a + /// local or CI build from the dotnet-monitor repository. + /// The path of a returned file is {sharedLibraryPath}/{nativePlatformFolder}/{fileName}. + /// + public static IFileProvider Create(string runtimeIdentifier, string sharedLibraryPath) + { + int index = runtimeIdentifier.LastIndexOf('-'); + if (index < 0) + { + throw new ArgumentException(); + } + string osPlatform = runtimeIdentifier.Substring(0, index); + string architecture = runtimeIdentifier.Substring(index + 1); + + string nativePlatformFolderPrefix = null; + switch (osPlatform) + { + case "linux": + case "linux-musl": + nativePlatformFolderPrefix = "Linux"; + break; + case "osx": + nativePlatformFolderPrefix = "OSX"; + break; + case "win": + nativePlatformFolderPrefix = "Windows_NT"; + break; + default: + throw new PlatformNotSupportedException(); + } + + string configurationName = +#if DEBUG + "Debug"; +#else + "Release"; +#endif + + string nativeOutputPath = Path.Combine(sharedLibraryPath, $"{nativePlatformFolderPrefix}.{architecture}.{configurationName}"); + + return new BuildOutputNativeFileProvider(nativeOutputPath); + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + throw new NotSupportedException(); + } + + public IFileInfo GetFileInfo(string subpath) + { + FileInfo fileInfo = new FileInfo(Path.Combine(_nativeFileBasePath, subpath)); + if (fileInfo.Exists) + { + return new PhysicalFileInfo(fileInfo); + } + else + { + return new NotFoundFileInfo(fileInfo.FullName); + } + } + + public IChangeToken Watch(string filter) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/BuildOutputSharedLibraryInitializer.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/BuildOutputSharedLibraryInitializer.cs new file mode 100644 index 00000000000..3a93456dd05 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/BuildOutputSharedLibraryInitializer.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Tools.Monitor; +using Microsoft.Diagnostics.Tools.Monitor.Profiler; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using System.IO; +using System.Reflection; + +namespace Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup +{ + internal sealed class BuildOutputSharedLibraryInitializer : ISharedLibraryInitializer + { + // This is the binary output directory when built from the dotnet-monitor repo: /artifacts/bin + private static readonly string SharedLibrarySourcePath = + Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "..", "..", "..")); + + private readonly ILogger _logger; + + public BuildOutputSharedLibraryInitializer(ILogger logger) + { + _logger = logger; + } + + public INativeFileProviderFactory Initialize() + { + _logger.SharedLibraryPath(SharedLibrarySourcePath); + + return new Factory(); + } + + private class Factory : INativeFileProviderFactory + { + public IFileProvider Create(string runtimeIdentifier) + { + return BuildOutputNativeFileProvider.Create(runtimeIdentifier, SharedLibrarySourcePath); + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/HostingStartup.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/HostingStartup.cs new file mode 100644 index 00000000000..95aab8b3811 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/HostingStartup.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup; +using Microsoft.Diagnostics.Tools.Monitor.Profiler; +using Microsoft.Extensions.DependencyInjection; + +[assembly: HostingStartup(typeof(HostingStartup))] + +namespace Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup +{ + public class HostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => + { + services.AddSingleton(); + }); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup.csproj new file mode 100644 index 00000000000..861ee69717b --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup/Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup.csproj @@ -0,0 +1,15 @@ + + + + $(ToolTargetFrameworks) + + + + + + + + + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook.csproj new file mode 100644 index 00000000000..df2167466c7 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook.csproj @@ -0,0 +1,7 @@ + + + + $(ToolTargetFrameworks) + + + diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook/StartupHook.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook/StartupHook.cs new file mode 100644 index 00000000000..53d7c52a138 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.TestStartupHook/StartupHook.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Reflection; +using System.Runtime.Loader; + +public sealed class StartupHook +{ + private const string TestHostingStartupAssemblyName = "Microsoft.Diagnostics.Monitoring.Tool.TestHostingStartup"; + + public static void Initialize() + { + AssemblyLoadContext.Default.Resolving += AssemblyLoadContext_Resolving; + } + + private static Assembly AssemblyLoadContext_Resolving(AssemblyLoadContext context, AssemblyName assemblyName) + { + if (TestHostingStartupAssemblyName.Equals(assemblyName.Name, StringComparison.OrdinalIgnoreCase)) + { + string path = Assembly.GetExecutingAssembly().Location.Replace( + Assembly.GetExecutingAssembly().GetName().Name, + TestHostingStartupAssemblyName); + + return AssemblyLoadContext.Default.LoadFromAssemblyPath(path); + } + return null; + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionDependencyAnalyzerTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionDependencyAnalyzerTests.cs index ab3f636bda8..8ae4cf28bda 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionDependencyAnalyzerTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionDependencyAnalyzerTests.cs @@ -24,6 +24,28 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests { public sealed class ActionDependencyAnalyzerTests { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30); + + private sealed class TestEndpointInfo : WebApi.EndpointInfoBase + { + public TestEndpointInfo(Guid runtimeInstanceCookie, int processId = 0, string commandLine = null, string operatingSystem = null, string processArchitecture = null) + { + ProcessId = processId; + RuntimeInstanceCookie = runtimeInstanceCookie; + CommandLine = commandLine; + OperatingSystem = operatingSystem; + ProcessArchitecture = processArchitecture; + } + + public override int ProcessId { get; protected set; } + public override Guid RuntimeInstanceCookie { get; protected set; } + public override string CommandLine { get; protected set; } + public override string OperatingSystem { get; protected set; } + public override string ProcessArchitecture { get; protected set; } + + public override Version RuntimeVersion { get; protected set; } + } + private readonly ITestOutputHelper _outputHelper; private static readonly TimeSpan TimeoutMs = TimeSpan.FromMilliseconds(500); private const string DefaultRuleName = nameof(ActionDependencyAnalyzerTests); @@ -33,6 +55,66 @@ public ActionDependencyAnalyzerTests(ITestOutputHelper outputHelper) _outputHelper = outputHelper; } + [Fact] + public async Task DependenciesTest() + { + const string Output1 = nameof(Output1); + const string Output2 = nameof(Output2); + const string Output3 = nameof(Output3); + + string a2input1 = FormattableString.Invariant($"$(Actions.a1.{Output1}) with $(Actions.a1.{Output2})T"); + string a2input2 = FormattableString.Invariant($"$(Actions.a1.{Output2})"); + string a2input3 = FormattableString.Invariant($"Output $(Actions.a1.{Output3}) trail"); + + PassThroughOptions a2Settings = null; + + await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => + { + CollectionRuleOptions options = rootOptions.CreateCollectionRule(DefaultRuleName) + .AddPassThroughAction("a1", "a1input1", "a1input2", "a1input3") + .AddPassThroughAction("a2", a2input1, a2input2, a2input3) + .SetStartupTrigger(); + + a2Settings = (PassThroughOptions)options.Actions.Last().Settings; + }, async host => + { + ActionListExecutor executor = host.Services.GetService(); + + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout); + + CollectionRuleOptions ruleOptions = host.Services.GetRequiredService>().Get(DefaultRuleName); + ILogger logger = host.Services.GetRequiredService>(); + ISystemClock clock = host.Services.GetRequiredService(); + + CollectionRuleContext context = new(DefaultRuleName, ruleOptions, null, logger, clock); + + int callbackCount = 0; + Action startCallback = () => callbackCount++; + + IDictionary results = await executor.ExecuteActions(context, startCallback, cancellationTokenSource.Token); + + //Verify that the original settings were not altered during execution. + Assert.Equal(a2input1, a2Settings.Input1); + Assert.Equal(a2input2, a2Settings.Input2); + Assert.Equal(a2input3, a2Settings.Input3); + + Assert.Equal(1, callbackCount); + Assert.Equal(2, results.Count); + Assert.True(results.TryGetValue("a2", out CollectionRuleActionResult a2result)); + Assert.Equal(3, a2result.OutputValues.Count); + + Assert.True(a2result.OutputValues.TryGetValue(Output1, out string a2output1)); + Assert.Equal("a1input1 with a1input2T", a2output1); + Assert.True(a2result.OutputValues.TryGetValue(Output2, out string a2output2)); + Assert.Equal("a1input2", a2output2); + Assert.True(a2result.OutputValues.TryGetValue(Output3, out string a2output3)); + Assert.Equal("Output a1input3 trail", a2output3); + }, serviceCollection => + { + serviceCollection.RegisterCollectionRuleAction(nameof(PassThroughAction)); + }); + } + [Fact] public async Task InvalidTokenReferenceTest() { @@ -77,5 +159,100 @@ await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => loggingBuilder.AddProvider(new TestLoggerProvider(record)); }); } + + [Fact] + public async Task RuntimeIdReferenceTest() + { + PassThroughOptions settings = null; + await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => + { + CollectionRuleOptions options = rootOptions.CreateCollectionRule(DefaultRuleName) + .AddPassThroughAction("a1", ConfigurationTokenParser.RuntimeIdReference, "test", "test") + .SetStartupTrigger(); + + settings = (PassThroughOptions)options.Actions.Last().Settings; + }, host => + { + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeoutMs); + + CollectionRuleOptions ruleOptions = host.Services.GetRequiredService>().Get(DefaultRuleName); + ILogger logger = host.Services.GetRequiredService>(); + ISystemClock clock = host.Services.GetRequiredService(); + + Guid instanceId = Guid.NewGuid(); + CollectionRuleContext context = new(DefaultRuleName, ruleOptions, new TestEndpointInfo(instanceId), logger, clock); + + ActionOptionsDependencyAnalyzer analyzer = ActionOptionsDependencyAnalyzer.Create(context); + PassThroughOptions newSettings = (PassThroughOptions)analyzer.SubstituteOptionValues(new Dictionary(), 1, settings); + + Assert.Equal(instanceId.ToString("D"), newSettings.Input1); + + }, serviceCollection => + { + serviceCollection.RegisterCollectionRuleAction(nameof(PassThroughAction)); + }); + } + + [Fact] + public async Task ReplacementsAndDependencies() + { + const string Output1 = nameof(Output1); + const string Output2 = nameof(Output2); + const string Output3 = nameof(Output3); + + string a2input1 = FormattableString.Invariant($"$(Actions.a1.{Output1}) with rid: $(Process.RuntimeId) and $(Actions.a1.{Output2})"); + string a2input2 = FormattableString.Invariant($"$(Actions.a1.{Output2})"); + string a2input3 = FormattableString.Invariant($"Output $(Actions.a1.{Output3}) trail"); + + PassThroughOptions a2Settings = null; + + await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => + { + CollectionRuleOptions options = rootOptions.CreateCollectionRule(DefaultRuleName) + .AddPassThroughAction("a1", "a1input1", "a1input2", "a1input3") + .AddPassThroughAction("a2", a2input1, a2input2, a2input3) + .SetStartupTrigger(); + + a2Settings = (PassThroughOptions)options.Actions.Last().Settings; + }, async host => + { + ActionListExecutor executor = host.Services.GetService(); + + using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout); + + CollectionRuleOptions ruleOptions = host.Services.GetRequiredService>().Get(DefaultRuleName); + ILogger logger = host.Services.GetRequiredService>(); + ISystemClock clock = host.Services.GetRequiredService(); + + Guid instanceId = Guid.NewGuid(); + + CollectionRuleContext context = new(DefaultRuleName, ruleOptions, new TestEndpointInfo(instanceId), logger, clock); + + int callbackCount = 0; + Action startCallback = () => callbackCount++; + + IDictionary results = await executor.ExecuteActions(context, startCallback, cancellationTokenSource.Token); + + //Verify that the original settings were not altered during execution. + Assert.Equal(a2input1, a2Settings.Input1); + Assert.Equal(a2input2, a2Settings.Input2); + Assert.Equal(a2input3, a2Settings.Input3); + + Assert.Equal(1, callbackCount); + Assert.Equal(2, results.Count); + Assert.True(results.TryGetValue("a2", out CollectionRuleActionResult a2result)); + Assert.Equal(3, a2result.OutputValues.Count); + + Assert.True(a2result.OutputValues.TryGetValue(Output1, out string a2output1)); + Assert.Equal(FormattableString.Invariant($"a1input1 with rid: {instanceId.ToString("D")} and a1input2"), a2output1); + Assert.True(a2result.OutputValues.TryGetValue(Output2, out string a2output2)); + Assert.Equal("a1input2", a2output2); + Assert.True(a2result.OutputValues.TryGetValue(Output3, out string a2output3)); + Assert.Equal("Output a1input3 trail", a2output3); + }, serviceCollection => + { + serviceCollection.RegisterCollectionRuleAction(nameof(PassThroughAction)); + }); + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs index 143bc2f8914..88d771e19bf 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionListExecutorTests.cs @@ -14,8 +14,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -158,65 +156,7 @@ await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => }); } - [Fact] - public async Task ActionListExecutor_Dependencies() - { - const string Output1 = nameof(Output1); - const string Output2 = nameof(Output2); - const string Output3 = nameof(Output3); - - string a2input1 = FormattableString.Invariant($"$(Actions.a1.{Output1}) with $(Actions.a1.{Output2})T"); - string a2input2 = FormattableString.Invariant($"$(Actions.a1.{Output2})"); - string a2input3 = FormattableString.Invariant($"Output $(Actions.a1.{Output3}) trail"); - PassThroughOptions a2Settings = null; - - await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => - { - CollectionRuleOptions options = rootOptions.CreateCollectionRule(DefaultRuleName) - .AddPassThroughAction("a1", "a1input1", "a1input2", "a1input3") - .AddPassThroughAction("a2", a2input1, a2input2, a2input3) - .SetStartupTrigger(); - - a2Settings = (PassThroughOptions)options.Actions.Last().Settings; - }, async host => - { - ActionListExecutor executor = host.Services.GetService(); - - using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(DefaultTimeout); - - CollectionRuleOptions ruleOptions = host.Services.GetRequiredService>().Get(DefaultRuleName); - ILogger logger = host.Services.GetRequiredService>(); - ISystemClock clock = host.Services.GetRequiredService(); - - CollectionRuleContext context = new(DefaultRuleName, ruleOptions, null, logger, clock); - - int callbackCount = 0; - Action startCallback = () => callbackCount++; - - IDictionary results = await executor.ExecuteActions(context, startCallback, cancellationTokenSource.Token); - - //Verify that the original settings were not altered during execution. - Assert.Equal(a2input1, a2Settings.Input1); - Assert.Equal(a2input2, a2Settings.Input2); - Assert.Equal(a2input3, a2Settings.Input3); - - Assert.Equal(1, callbackCount); - Assert.Equal(2, results.Count); - Assert.True(results.TryGetValue("a2", out CollectionRuleActionResult a2result)); - Assert.Equal(3, a2result.OutputValues.Count); - - Assert.True(a2result.OutputValues.TryGetValue(Output1, out string a2output1)); - Assert.Equal("a1input1 with a1input2T", a2output1); - Assert.True(a2result.OutputValues.TryGetValue(Output2, out string a2output2)); - Assert.Equal("a1input2", a2output2); - Assert.True(a2result.OutputValues.TryGetValue(Output3, out string a2output3)); - Assert.Equal("Output a1input3 trail", a2output3); - }, serviceCollection => - { - serviceCollection.RegisterCollectionRuleAction(nameof(PassThroughAction)); - }); - } [Fact] public async Task DuplicateActionNamesTest() @@ -245,4 +185,4 @@ private static void VerifyStartCallbackCount(bool waitForCompletion, int callbac Assert.Equal(1, callbackCount); } } -} \ No newline at end of file +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionTestsHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionTestsHelper.cs index 76cbb55d7a0..b3ff7cc5d9c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionTestsHelper.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ActionTestsHelper.cs @@ -24,7 +24,6 @@ internal static class ActionTestsHelper { public static TargetFrameworkMoniker[] tfmsToTest = new TargetFrameworkMoniker[] { - TargetFrameworkMoniker.Net50, TargetFrameworkMoniker.Net60, #if INCLUDE_NEXT_DOTNET TargetFrameworkMoniker.Net70 @@ -105,7 +104,7 @@ public static IEnumerable GetTfmArchitectureProfilerPath() static void AddTestCases(List arguments, Architecture architecture) { - string profilerPath = NativeLibraryHelper.GetMonitorProfilerPath(architecture); + string profilerPath = ProfilerHelper.GetPath(architecture); if (File.Exists(profilerPath)) { foreach (TargetFrameworkMoniker tfm in ActionTestsHelper.tfms6PlusToTest) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectLiveMetricsActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectLiveMetricsActionTests.cs new file mode 100644 index 00000000000..ce396a63d68 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectLiveMetricsActionTests.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + public class CollectLiveMetricsActionTests + { + private readonly ITestOutputHelper _outputHelper; + private readonly EndpointUtilities _endpointUtilities; + + private const string DefaultRuleName = "LiveMetricsTestRule"; + private const int IntervalSeconds = 2; + private const int DurationSeconds = IntervalSeconds + 1; + + public CollectLiveMetricsActionTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + _endpointUtilities = new(_outputHelper); + } + + [Theory] + [MemberData(nameof(ActionTestsHelper.GetTfms), MemberType = typeof(ActionTestsHelper))] + public async Task CollectLiveMetricsAction_Custom(TargetFrameworkMoniker tfm) + { + using TemporaryDirectory tempDirectory = new(_outputHelper); + + const string providerName = EventPipe.MonitoringSourceConfiguration.SystemRuntimeEventSourceName; + + var counterNames = new[] { "cpu-usage", "working-set" }; + + await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => + { + rootOptions.AddGlobalCounter(IntervalSeconds); + + rootOptions.AddFileSystemEgress(ActionTestsConstants.ExpectedEgressProvider, tempDirectory.FullName); + + rootOptions.CreateCollectionRule(DefaultRuleName) + .AddCollectLiveMetricsAction(ActionTestsConstants.ExpectedEgressProvider, options => + { + options.Duration = TimeSpan.FromSeconds(DurationSeconds); + options.IncludeDefaultProviders = false; + options.Providers = new[] + { + new EventMetricsProvider + { + ProviderName = providerName, + CounterNames = counterNames, + } + }; + }) + .SetStartupTrigger(); + }, async host => + { + CollectLiveMetricsOptions options = ActionTestsHelper.GetActionOptions(host, DefaultRuleName); + + ICollectionRuleActionFactoryProxy factory; + Assert.True(host.Services.GetService().TryCreateFactory(KnownCollectionRuleActions.CollectLiveMetrics, out factory)); + + EndpointInfoSourceCallback callback = new(_outputHelper); + await using ServerSourceHolder sourceHolder = await _endpointUtilities.StartServerAsync(callback); + + AppRunner runner = _endpointUtilities.CreateAppRunner(sourceHolder.TransportName, tfm); + + Task newEndpointInfoTask = callback.WaitAddedEndpointInfoAsync(runner, CommonTestTimeouts.StartProcess); + + await runner.ExecuteAsync(async () => + { + IEndpointInfo endpointInfo = await newEndpointInfoTask; + + ICollectionRuleAction action = factory.Create(endpointInfo, options); + + CollectionRuleActionResult result = await ActionTestsHelper.ExecuteAndDisposeAsync(action, CommonTestTimeouts.LiveMetricsTimeout); + + string egressPath = ActionTestsHelper.ValidateEgressPath(result); + + using FileStream liveMetricsStream = new(egressPath, FileMode.Open, FileAccess.Read); + Assert.NotNull(liveMetricsStream); + + var metrics = LiveMetricsTestUtilities.GetAllMetrics(liveMetricsStream); + + await LiveMetricsTestUtilities.ValidateMetrics(new[] { providerName }, + counterNames, + metrics, + strict: true); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }); + }); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs index 527e2370f34..172617ac3b0 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectTraceActionTests.cs @@ -139,4 +139,4 @@ private static async Task ValidateTrace(Stream traceStream) Assert.True(foundTraceObject); } } -} \ No newline at end of file +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDefaultsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDefaultsTests.cs index 099429365ec..ff23498c565 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDefaultsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDefaultsTests.cs @@ -51,7 +51,7 @@ await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => { CollectDumpOptions options = ActionTestsHelper.GetActionOptions(host, DefaultRuleName); - Assert.Equal(options.Egress, ActionTestsConstants.ExpectedEgressProvider); + Assert.Equal(ActionTestsConstants.ExpectedEgressProvider, options.Egress); }); } @@ -97,7 +97,7 @@ await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => { CollectDumpOptions options = ActionTestsHelper.GetActionOptions(host, DefaultRuleName); - Assert.Equal(options.Egress, ActionTestsConstants.ExpectedEgressProvider); + Assert.Equal(ActionTestsConstants.ExpectedEgressProvider, options.Egress); }); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDescriptionPipelineTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDescriptionPipelineTests.cs new file mode 100644 index 00000000000..e793140cd1c --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleDescriptionPipelineTests.cs @@ -0,0 +1,439 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Options; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Actions; +using Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Triggers; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Monitoring.WebApi.Models; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + public class CollectionRuleDescriptionPipelineTests + { + private readonly TimeSpan DefaultPipelineTimeout = TimeSpan.FromSeconds(30); + private const string TestRuleName = "TestPipelineRule"; + const int ExpectedActionExecutionCount = 3; + + private readonly ITestOutputHelper _outputHelper; + + public CollectionRuleDescriptionPipelineTests(ITestOutputHelper outputHelper) + { + _outputHelper = outputHelper; + } + + /// + /// Test for the Finished (Startup) state. + /// + [Theory] + [MemberData(nameof(CollectionRulePipelineTests.GetTfmsSupportingPortListener), MemberType = typeof(CollectionRulePipelineTests))] + public Task CollectionRuleDescriptionPipeline_StartupTriggerTest(TargetFrameworkMoniker appTfm) + { + CallbackActionService callbackService = new(_outputHelper); + + return CollectionRulePipelineTestsHelper.ExecuteScenario( + appTfm, + TestAppScenarios.AsyncWait.Name, + TestRuleName, + options => + { + options.CreateCollectionRule(TestRuleName) + .SetStartupTrigger() + .AddAction(CallbackAction.ActionName); + }, + async (runner, pipeline, callbacks) => + { + using CancellationTokenSource cancellationSource = new(DefaultPipelineTimeout); + + Task startedTask = callbacks.StartWaitForPipelineStarted(); + + // Register first callback before pipeline starts. This callback should be completed before + // the pipeline finishes starting. + Task actionStartedTask = await callbackService.StartWaitForCallbackAsync(cancellationSource.Token); + + // Startup trigger will cause the the pipeline to complete the start phase + // after the action list has been completed. + Task runTask = pipeline.RunAsync(cancellationSource.Token); + + await startedTask.WithCancellation(cancellationSource.Token); + + // Since the action list was completed before the pipeline finished starting, + // the action should have invoked its callback. + await actionStartedTask.WithCancellation(cancellationSource.Token); + + // Pipeline should have completed shortly after finished starting. This should only + // wait for a very short time, if at all. + await runTask.WithCancellation(cancellationSource.Token); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = CollectionRuleLimitsOptionsDefaults.ActionCount, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.Finished, + StateReason = Strings.Message_CollectionRuleStateReason_Finished_Startup + }, TestRuleName); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + }, + _outputHelper, + services => + { + services.RegisterTestAction(callbackService); + }); + } + + /// + /// Test for Executing Action state. + /// + [Theory(Skip = "https://github.com/dotnet/dotnet-monitor/issues/2241")] + [MemberData(nameof(CollectionRulePipelineTests.GetTfmsSupportingPortListener), MemberType = typeof(CollectionRulePipelineTests))] + public Task CollectionRuleDescriptionPipeline_ExecutingAction(TargetFrameworkMoniker appTfm) + { + TimeSpan ClockIncrementDuration = TimeSpan.FromMilliseconds(10); + + MockSystemClock clock = new(); + ManualTriggerService triggerService = new(); + CallbackActionService callbackService = new(_outputHelper, clock); + + using TemporaryDirectory tempDirectory = new(_outputHelper); + + return CollectionRulePipelineTestsHelper.ExecuteScenario( + appTfm, + TestAppScenarios.AsyncWait.Name, + TestRuleName, + options => + { + options.CreateCollectionRule(TestRuleName) + .SetManualTrigger() + .AddAction(DelayedCallbackAction.ActionName) + .SetActionLimits(count: ExpectedActionExecutionCount); + + options.AddFileSystemEgress(ActionTestsConstants.ExpectedEgressProvider, tempDirectory.FullName); + }, + async (runner, pipeline, callbacks) => + { + using CancellationTokenSource cancellationSource = new(DefaultPipelineTimeout); + + Task startedTask = callbacks.StartWaitForPipelineStarted(); + + Task runTask = pipeline.RunAsync(cancellationSource.Token); + + await startedTask.WithCancellation(cancellationSource.Token); + + Task actionsThrottledTask = callbacks.StartWaitForActionsThrottled(); + + // Borrowed portions of this from ManualTriggerAsync implementation + TaskCompletionSource startedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + EventHandler startedHandler = (s, e) => startedSource.TrySetResult(null); + using var _ = cancellationSource.Token.Register(() => startedSource.TrySetCanceled(cancellationSource.Token)); + + Task actionStartedTask = await callbackService.StartWaitForCallbackAsync(cancellationSource.Token); + + triggerService.NotifyStarted += startedHandler; + + // Manually invoke the trigger. + triggerService.NotifyTriggerSubscribers(); + + // Wait until action has started. + await actionStartedTask.WithCancellation(cancellationSource.Token); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = ExpectedActionExecutionCount, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.ActionExecuting, + StateReason = Strings.Message_CollectionRuleStateReason_ExecutingActions + }, TestRuleName); + + clock.Increment(ClockIncrementDuration); + + await startedSource.WithCancellation(cancellationSource.Token); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = ExpectedActionExecutionCount, + LifetimeOccurrences = 1, + SlidingWindowOccurrences = 1, + State = CollectionRuleState.Running, + StateReason = Strings.Message_CollectionRuleStateReason_Running + }, TestRuleName); + + triggerService.NotifyStarted -= startedHandler; + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + + await pipeline.StopAsync(cancellationSource.Token); + }, + _outputHelper, + services => + { + services.AddSingleton(clock); + services.RegisterManualTrigger(triggerService); + services.RegisterDelayedTestAction(callbackService); + }); + } + + /// + /// Test for Throttled -> Running -> Throttled. + /// + [Theory] + [MemberData(nameof(CollectionRulePipelineTests.GetTfmsSupportingPortListener), MemberType = typeof(CollectionRulePipelineTests))] + public Task CollectionRuleDescriptionPipeline_Throttled(TargetFrameworkMoniker appTfm) + { + const int IterationCount = 5; + TimeSpan SlidingWindowDuration = TimeSpan.FromSeconds(2); // NOTE: A value greater than 1 second is necessary since the countdown trims precision to the nearest second (for user-readability) + TimeSpan ExpectedSlidingWindowDurationCountdown = TimeSpan.FromSeconds(1); + TimeSpan ClockIncrementDuration = TimeSpan.FromMilliseconds(10); + + MockSystemClock clock = new(); + ManualTriggerService triggerService = new(); + CallbackActionService callbackService = new(_outputHelper, clock); + + return CollectionRulePipelineTestsHelper.ExecuteScenario( + appTfm, + TestAppScenarios.AsyncWait.Name, + TestRuleName, + options => + { + options.CreateCollectionRule(TestRuleName) + .SetManualTrigger() + .AddAction(CallbackAction.ActionName) + .SetActionLimits( + count: ExpectedActionExecutionCount, + slidingWindowDuration: SlidingWindowDuration); + }, + async (runner, pipeline, callbacks) => + { + using CancellationTokenSource cancellationSource = new(DefaultPipelineTimeout); + + Task startedTask = callbacks.StartWaitForPipelineStarted(); + + Task runTask = pipeline.RunAsync(cancellationSource.Token); + + await startedTask.WithCancellation(cancellationSource.Token); + + await CollectionRulePipelineTestsHelper.ManualTriggerAsync( + triggerService, + callbackService, + callbacks, + IterationCount, + ExpectedActionExecutionCount, + clock, + ClockIncrementDuration, + completesOnLastExpectedIteration: false, + cancellationSource.Token); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = ExpectedActionExecutionCount, + ActionCountSlidingWindowDurationLimit = SlidingWindowDuration, + LifetimeOccurrences = ExpectedActionExecutionCount, + SlidingWindowOccurrences = ExpectedActionExecutionCount, + State = CollectionRuleState.Throttled, + StateReason = Strings.Message_CollectionRuleStateReason_Throttled, + SlidingWindowDurationCountdown = ExpectedSlidingWindowDurationCountdown + }, TestRuleName); + + clock.Increment(2 * SlidingWindowDuration); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = ExpectedActionExecutionCount, + ActionCountSlidingWindowDurationLimit = SlidingWindowDuration, + LifetimeOccurrences = ExpectedActionExecutionCount, + SlidingWindowOccurrences = 0, + State = CollectionRuleState.Running, + StateReason = Strings.Message_CollectionRuleStateReason_Running + }, TestRuleName); + + await CollectionRulePipelineTestsHelper.ManualTriggerAsync( + triggerService, + callbackService, + callbacks, + IterationCount, + ExpectedActionExecutionCount, + clock, + ClockIncrementDuration, + completesOnLastExpectedIteration: false, + cancellationSource.Token); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = ExpectedActionExecutionCount, + ActionCountSlidingWindowDurationLimit = SlidingWindowDuration, + LifetimeOccurrences = 2 * ExpectedActionExecutionCount, + SlidingWindowOccurrences = ExpectedActionExecutionCount, + State = CollectionRuleState.Throttled, + StateReason = Strings.Message_CollectionRuleStateReason_Throttled, + SlidingWindowDurationCountdown = ExpectedSlidingWindowDurationCountdown + }, TestRuleName); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + + await pipeline.StopAsync(cancellationSource.Token); + }, + _outputHelper, + services => + { + services.AddSingleton(clock); + services.RegisterManualTrigger(triggerService); + services.RegisterTestAction(callbackService); + }); + } + + /// + /// Test for the Finished (Rule Duration) state. + /// + [Theory] + [MemberData(nameof(CollectionRulePipelineTests.GetTfmsSupportingPortListener), MemberType = typeof(CollectionRulePipelineTests))] + public Task CollectionRuleDescriptionPipeline_ReachedRuleDuration(TargetFrameworkMoniker appTfm) + { + ManualTriggerService triggerService = new(); + CallbackActionService callbackService = new(_outputHelper); + + return CollectionRulePipelineTestsHelper.ExecuteScenario( + appTfm, + TestAppScenarios.AsyncWait.Name, + TestRuleName, + options => + { + options.CreateCollectionRule(TestRuleName) + .SetManualTrigger() + .AddAction(CallbackAction.ActionName) + .SetDurationLimit(TimeSpan.FromSeconds(3)); + }, + async (runner, pipeline, _) => + { + using CancellationTokenSource cancellationSource = new(DefaultPipelineTimeout); + + // Pipeline should run to completion due to rule duration limit. + await pipeline.RunAsync(cancellationSource.Token); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = CollectionRuleLimitsOptionsDefaults.ActionCount, + LifetimeOccurrences = 0, + SlidingWindowOccurrences = 0, + State = CollectionRuleState.Finished, + StateReason = Strings.Message_CollectionRuleStateReason_Finished_RuleDuration + }, TestRuleName); + }, + _outputHelper, + services => + { + services.RegisterManualTrigger(triggerService); + services.RegisterTestAction(callbackService); + }); + } + + /// + /// Test for the Finished (Action Count Limit) state. + /// + [Theory] + [MemberData(nameof(CollectionRulePipelineTests.GetTfmsSupportingPortListener), MemberType = typeof(CollectionRulePipelineTests))] + public Task CollectionRuleDescriptionPipeline_ActionCountLimitUnlimitedDurationTest(TargetFrameworkMoniker appTfm) + { + TimeSpan ClockIncrementDuration = TimeSpan.FromMilliseconds(10); + + MockSystemClock clock = new(); + ManualTriggerService triggerService = new(); + CallbackActionService callbackService = new(_outputHelper, clock); + + return CollectionRulePipelineTestsHelper.ExecuteScenario( + appTfm, + TestAppScenarios.AsyncWait.Name, + TestRuleName, + options => + { + options.CreateCollectionRule(TestRuleName) + .SetManualTrigger() + .AddAction(CallbackAction.ActionName) + .SetActionLimits(count: ExpectedActionExecutionCount); + }, + async (runner, pipeline, callbacks) => + { + using CancellationTokenSource cancellationSource = new(DefaultPipelineTimeout); + + Task startedTask = callbacks.StartWaitForPipelineStarted(); + + Task runTask = pipeline.RunAsync(cancellationSource.Token); + + await startedTask.WithCancellation(cancellationSource.Token); + + await CollectionRulePipelineTestsHelper.ManualTriggerAsync( + triggerService, + callbackService, + callbacks, + ExpectedActionExecutionCount, + ExpectedActionExecutionCount, + clock, + ClockIncrementDuration, + completesOnLastExpectedIteration: true, + cancellationSource.Token); + + // Pipeline should run to completion due to action count limit without sliding window. + await runTask.WithCancellation(cancellationSource.Token); + + await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); + + CompareCollectionRuleDetailedDescriptions(pipeline, new() + { + ActionCountLimit = ExpectedActionExecutionCount, + LifetimeOccurrences = ExpectedActionExecutionCount, + SlidingWindowOccurrences = ExpectedActionExecutionCount, + State = CollectionRuleState.Finished, + StateReason = Strings.Message_CollectionRuleStateReason_Finished_ActionCount + }, TestRuleName); + }, + _outputHelper, + services => + { + services.AddSingleton(clock); + services.RegisterManualTrigger(triggerService); + services.RegisterTestAction(callbackService); + }); + } + + private static void CompareCollectionRuleDescriptions(CollectionRulePipeline pipeline, CollectionRuleDescription expectedDescription) + { + CollectionRuleDescription actualDescription = CollectionRuleService.GetCollectionRuleDescription(pipeline); + + Assert.Equal(actualDescription, expectedDescription); + } + + private static void CompareCollectionRuleDescriptions(CollectionRulePipeline pipeline, CollectionRuleDetailedDescription expectedDetailedDescription) + { + CompareCollectionRuleDescriptions(pipeline, new CollectionRuleDescription() + { + State = expectedDetailedDescription.State, + StateReason = expectedDetailedDescription.StateReason + }); + } + + + private static void CompareCollectionRuleDetailedDescriptions(CollectionRulePipeline pipeline, CollectionRuleDetailedDescription expectedDetailedDescription, string collectionRuleName) + { + CompareCollectionRuleDescriptions(pipeline, expectedDetailedDescription); + + CollectionRuleDetailedDescription actualDetailedDescription = CollectionRuleService.GetCollectionRuleDetailedDescription(pipeline); + + Assert.Equal(expectedDetailedDescription, actualDetailedDescription); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs index c0e2c06a63e..95d45b0fc64 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRuleOptionsTests.cs @@ -1132,6 +1132,178 @@ public Task CollectionRuleOptions_CollectTraceAction_ProviderPropertyValidation( }); } + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const string ExpectedEventProviderName = "Microsoft-Extensions-Logging"; + List ExpectedProviders = new() + { + new() { Name = ExpectedEventProviderName } + }; + + TraceEventFilter expectedStoppingEvent = new() + { + EventName = "CustomEvent", + ProviderName = ExpectedEventProviderName + }; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider, (options) => + { + options.StoppingEvent = expectedStoppingEvent; + }); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + ruleOptions.VerifyCollectTraceAction(0, ExpectedProviders, ExpectedEgressProvider, expectedStoppingEvent); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_StopOnEvent_MissingProviderConfig() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const string ExpectedMissingEventProviderName = "Non-Existent-Provider"; + + List ExpectedProviders = new() + { + new() { Name = "Microsoft-Extensions-Logging" } + }; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(ExpectedProviders, ExpectedEgressProvider, (options) => + { + options.StoppingEvent = new TraceEventFilter() + { + EventName = "CustomEvent", + ProviderName = ExpectedMissingEventProviderName + }; + }); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyMissingStoppingEventProviderMessage( + failures, + 0, + nameof(CollectTraceOptions.StoppingEvent), + ExpectedMissingEventProviderName, + nameof(CollectTraceOptions.Providers)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectTraceAction_BothProfileAndStoppingEvent() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectTraceAction(TraceProfile.Metrics, ExpectedEgressProvider, options => + { + options.StoppingEvent = new TraceEventFilter() + { + EventName = "CustomEvent", + ProviderName = "CustomProvider" + }; + }); + + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.NotEmpty(failures); + VerifyBothCannotBeSpecifiedMessage( + failures, + 0, + nameof(CollectTraceOptions.Profile), + nameof(CollectTraceOptions.StoppingEvent)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectLiveMetricsAction_RoundTrip() + { + const string ExpectedEgressProvider = "TmpEgressProvider"; + const bool ExpectedIncludeDefaultProviders = false; + + TimeSpan ExpectedDuration = TimeSpan.FromSeconds(45); + + const string providerName = EventPipe.MonitoringSourceConfiguration.SystemRuntimeEventSourceName; + var counterNames = new[] { "cpu-usage", "working-set" }; + + EventMetricsProvider[] ExpectedProviders = new[] + { + new EventMetricsProvider + { + ProviderName = providerName, + CounterNames = counterNames, + } + }; + + return ValidateSuccess( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectLiveMetricsAction(ExpectedEgressProvider, options => + { + options.Duration = ExpectedDuration; + options.IncludeDefaultProviders = ExpectedIncludeDefaultProviders; + options.Providers = ExpectedProviders; + }); + rootOptions.AddFileSystemEgress(ExpectedEgressProvider, "/tmp"); + }, + ruleOptions => + { + CollectLiveMetricsOptions collectLiveMetricsOptions = ruleOptions.VerifyCollectLiveMetricsAction(0, ExpectedEgressProvider); + + Assert.Equal(ExpectedDuration, collectLiveMetricsOptions.Duration); + Assert.Equal(ExpectedIncludeDefaultProviders, collectLiveMetricsOptions.IncludeDefaultProviders); + Assert.Equal(ExpectedProviders.Select(x => x.CounterNames.ToHashSet()), collectLiveMetricsOptions.Providers.Select(x => x.CounterNames.ToHashSet())); + Assert.Equal(ExpectedProviders.Select(x => x.ProviderName), collectLiveMetricsOptions.Providers.Select(x => x.ProviderName)); + }); + } + + [Fact] + public Task CollectionRuleOptions_CollectLiveMetricsAction_PropertyValidation() + { + return ValidateFailure( + rootOptions => + { + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectLiveMetricsAction(UnknownEgressName, options => + { + options.Duration = TimeSpan.FromDays(3); + }); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Equal(2, failures.Length); + VerifyRangeMessage(failures, 0, nameof(CollectTraceOptions.Duration), + ActionOptionsConstants.Duration_MinValue, ActionOptionsConstants.Duration_MaxValue); + VerifyEgressNotExistMessage(failures, 1, UnknownEgressName); + }); + } + [Fact] public Task CollectionRuleOptions_ExecuteAction_MinimumOptions() { @@ -1450,6 +1622,79 @@ await ValidateFailure( }); } + [Fact] + public async Task CollectionRuleOptions_CollectStacksAction_FeatureDisabled() + { + await ValidateFailure( + rootOptions => + { + const string fileEgress = nameof(fileEgress); + rootOptions.AddFileSystemEgress(fileEgress, "/tmp") + .CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectStacksAction(fileEgress); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyFeatureDisabled(failures, 0); + }, + servicesCallback => + { + // Set the experimental flags, but we did not set InProcessFeatures to be enabled + servicesCallback.AddSingleton((_) => new TestExperimentalFlags { IsCallStacksEnabled = true }); + }); + } + + [Fact] + public async Task CollectionRuleOptions_CollectStacksAction_FeatureDisabledByFlag() + { + await ValidateFailure( + rootOptions => + { + const string fileEgress = nameof(fileEgress); + rootOptions.AddFileSystemEgress(fileEgress, "/tmp") + .EnableInProcessFeatures() //Enable inproc features but don't set the flag + .CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddCollectStacksAction(fileEgress); + }, + ex => + { + string[] failures = ex.Failures.ToArray(); + Assert.Single(failures); + VerifyFeatureDisabled(failures, 0); + }, + servicesCallback => + { + servicesCallback.AddSingleton(); + }); + + } + + [Fact] + public async Task CollectionRuleOptions_CollectStacksAction_FeatureEnabled() + { + await ValidateSuccess( + rootOptions => + { + const string fileEgress = nameof(fileEgress); + rootOptions.AddFileSystemEgress(fileEgress, "/tmp") + .EnableInProcessFeatures() + .CreateCollectionRule(DefaultRuleName) + .SetCPUUsageTrigger(usageOptions => { usageOptions.GreaterThan = 100; }) + .AddCollectStacksAction(fileEgress); + }, + ruleOptions => + { + }, + servicesCallback => + { + servicesCallback.AddSingleton((_) => new TestExperimentalFlags { IsCallStacksEnabled = true }); + }); + } + public static IEnumerable GetIEventCounterShortcutsAndNames() { yield return new object[] { typeof(CPUUsageOptions), KnownCollectionRuleTriggers.CPUUsage }; @@ -1459,26 +1704,31 @@ public static IEnumerable GetIEventCounterShortcutsAndNames() private Task Validate( Action setup, - Action> validate) + Action> validate, + Action servicesCallback = null) { return TestHostHelper.CreateCollectionRulesHost( _outputHelper, setup, - host => validate(host.Services.GetRequiredService>())); + servicesCallback: servicesCallback, + hostCallback: host => validate(host.Services.GetRequiredService>())); } private Task ValidateSuccess( Action setup, - Action validate) + Action validate, + Action servicesCallback = null) { return Validate( setup, - monitor => validate(monitor.Get(DefaultRuleName))); + monitor => validate(monitor.Get(DefaultRuleName)), + servicesCallback); } private Task ValidateFailure( Action setup, - Action validate) + Action validate, + Action servicesCallback = null) { return Validate( setup, @@ -1487,7 +1737,8 @@ private Task ValidateFailure( OptionsValidationException ex = Assert.Throws(() => monitor.Get(DefaultRuleName)); _outputHelper.WriteLine("Exception: {0}", ex.Message); validate(ex); - }); + }, + servicesCallback); } private static void VerifyUnknownActionTypeMessage(string[] failures, int index, string actionType) @@ -1595,5 +1846,27 @@ private static void VerifyEgressNotExistMessage(string[] failures, int index, st Assert.Equal(message, failures[index]); } + + private static void VerifyFeatureDisabled(string[] failures, int index) + { + string expectedMessage = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_DisabledFeature, + nameof(Tools.Monitor.CollectionRules.Actions.CollectStacksAction)); + + Assert.Equal(expectedMessage, failures[index]); + } + + private static void VerifyMissingStoppingEventProviderMessage(string[] failures, int index, string fieldName, string providerName, string providerFieldName) + { + string message = string.Format( + CultureInfo.InvariantCulture, + Strings.ErrorMessage_MissingStoppingEventProvider, + fieldName, + providerName, + providerFieldName); + + Assert.Equal(message, failures[index]); + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTests.cs index 22c11630275..5fad31b754e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTests.cs @@ -8,18 +8,9 @@ using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Actions; using Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Triggers; -using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; -using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; -using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; -using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Triggers; -using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Triggers; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -48,7 +39,7 @@ public Task CollectionRulePipeline_StartupTriggerTest(TargetFrameworkMoniker app { CallbackActionService callbackService = new(_outputHelper); - return ExecuteScenario( + return CollectionRulePipelineTestsHelper.ExecuteScenario( appTfm, TestAppScenarios.AsyncWait.Name, TestRuleName, @@ -100,6 +91,7 @@ public Task CollectionRulePipeline_StartupTriggerTest(TargetFrameworkMoniker app await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }, + _outputHelper, services => { services.RegisterTestAction(callbackService); @@ -115,7 +107,7 @@ public Task CollectionRulePipeline_EventCounterTriggerTest(TargetFrameworkMonike { CallbackActionService callbackService = new(_outputHelper); - return ExecuteScenario( + return CollectionRulePipelineTestsHelper.ExecuteScenario( appTfm, TestAppScenarios.SpinWait.Name, TestRuleName, @@ -162,6 +154,7 @@ public Task CollectionRulePipeline_EventCounterTriggerTest(TargetFrameworkMonike await pipeline.StopAsync(cancellationSource.Token); }, + _outputHelper, services => { services.RegisterTestAction(callbackService); @@ -178,7 +171,7 @@ public Task CollectionRulePipeline_DurationLimitTest(TargetFrameworkMoniker appT ManualTriggerService triggerService = new(); CallbackActionService callbackService = new(_outputHelper); - return ExecuteScenario( + return CollectionRulePipelineTestsHelper.ExecuteScenario( appTfm, TestAppScenarios.AsyncWait.Name, TestRuleName, @@ -201,6 +194,7 @@ public Task CollectionRulePipeline_DurationLimitTest(TargetFrameworkMoniker appT // Action list should not have been executed. VerifyExecutionCount(callbackService, expectedCount: 0); }, + _outputHelper, services => { services.RegisterManualTrigger(triggerService); @@ -222,7 +216,7 @@ public Task CollectionRulePipeline_ActionCountLimitUnlimitedDurationTest(TargetF ManualTriggerService triggerService = new(); CallbackActionService callbackService = new(_outputHelper, clock); - return ExecuteScenario( + return CollectionRulePipelineTestsHelper.ExecuteScenario( appTfm, TestAppScenarios.AsyncWait.Name, TestRuleName, @@ -243,7 +237,7 @@ public Task CollectionRulePipeline_ActionCountLimitUnlimitedDurationTest(TargetF await startedTask.WithCancellation(cancellationSource.Token); - await ManualTriggerAsync( + await CollectionRulePipelineTestsHelper.ManualTriggerAsync( triggerService, callbackService, callbacks, @@ -262,6 +256,7 @@ await ManualTriggerAsync( // Action list should have been executed the expected number of times VerifyExecutionCount(callbackService, ExpectedActionExecutionCount); }, + _outputHelper, services => { services.AddSingleton(clock); @@ -286,7 +281,7 @@ public Task CollectionRulePipeline_ActionCountLimitSlidingDurationTest(TargetFra ManualTriggerService triggerService = new(); CallbackActionService callbackService = new(_outputHelper, clock); - return ExecuteScenario( + return CollectionRulePipelineTestsHelper.ExecuteScenario( appTfm, TestAppScenarios.AsyncWait.Name, TestRuleName, @@ -309,7 +304,7 @@ public Task CollectionRulePipeline_ActionCountLimitSlidingDurationTest(TargetFra await startedTask.WithCancellation(cancellationSource.Token); - await ManualTriggerAsync( + await CollectionRulePipelineTestsHelper.ManualTriggerAsync( triggerService, callbackService, callbacks, @@ -325,7 +320,7 @@ await ManualTriggerAsync( clock.Increment(2 * SlidingWindowDuration); - await ManualTriggerAsync( + await CollectionRulePipelineTestsHelper.ManualTriggerAsync( triggerService, callbackService, callbacks, @@ -346,6 +341,7 @@ await ManualTriggerAsync( await pipeline.StopAsync(cancellationSource.Token); }, + _outputHelper, services => { services.AddSingleton(clock); @@ -368,99 +364,6 @@ private void VerifyExecutionCount(CallbackActionService service, int expectedCou Assert.Equal(expectedCount, service.ExecutionTimestamps.Count); } - /// - /// Manually trigger for a number of iterations () and test - /// that the actions are invoked for the number of expected iterations () and - /// are throttled for the remaining number of iterations. - /// - private async Task ManualTriggerAsync( - ManualTriggerService triggerService, - CallbackActionService callbackService, - PipelineCallbacks callbacks, - int iterationCount, - int expectedCount, - MockSystemClock clock, - TimeSpan clockIncrementDuration, - bool completesOnLastExpectedIteration, - CancellationToken token) - { - if (iterationCount < expectedCount) - { - throw new InvalidOperationException("Number of iterations must be greater than or equal to number of expected iterations."); - } - - int iteration = 0; - Task actionStartedTask; - Task actionsThrottledTask = callbacks.StartWaitForActionsThrottled(); - - // Test that the actions are run for each iteration where the actions are expected to run. - while (iteration < expectedCount) - { - iteration++; - - TaskCompletionSource startedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - EventHandler startedHandler = (s, e) => startedSource.TrySetResult(null); - using var _ = token.Register(() => startedSource.TrySetCanceled(token)); - - actionStartedTask = await callbackService.StartWaitForCallbackAsync(token); - - triggerService.NotifyStarted += startedHandler; - - // Manually invoke the trigger. - triggerService.NotifyTriggerSubscribers(); - - // Wait until action has started. - await actionStartedTask.WithCancellation(token); - - // If the pipeline completes on the last expected iteration, the trigger will not be started again. - // Skip this check for the last expected iteration if the pipeline is expected to complete. - if (!completesOnLastExpectedIteration || iteration != expectedCount) - { - await startedSource.WithCancellation(token); - } - - triggerService.NotifyStarted -= startedHandler; - - // Advance the clock source. - clock.Increment(clockIncrementDuration); - } - - // Check that actions were not throttled. - Assert.False(actionsThrottledTask.IsCompleted); - - actionStartedTask = await callbackService.StartWaitForCallbackAsync(token); - - // Test that actions are throttled for remaining iterations. - while (iteration < iterationCount) - { - iteration++; - - TaskCompletionSource startedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); - EventHandler startedHandler = (s, e) => startedSource.TrySetResult(null); - using var _ = token.Register(() => startedSource.TrySetCanceled(token)); - - actionsThrottledTask = callbacks.StartWaitForActionsThrottled(); - - triggerService.NotifyStarted += startedHandler; - - // Manually invoke the trigger. - triggerService.NotifyTriggerSubscribers(); - - // Check throttling has occurred. - await actionsThrottledTask.WithCancellation(token); - - await startedSource.WithCancellation(token); - - triggerService.NotifyStarted -= startedHandler; - - // Advance the clock source. - clock.Increment(clockIncrementDuration); - } - - // Check that no actions have been executed. - Assert.False(actionStartedTask.IsCompleted); - } - private async Task ManualTriggerBurstAsync(ManualTriggerService service, int count = 10) { for (int i = 0; i < count; i++) @@ -472,163 +375,10 @@ private async Task ManualTriggerBurstAsync(ManualTriggerService service, int cou public static IEnumerable GetTfmsSupportingPortListener() { - yield return new object[] { TargetFrameworkMoniker.Net50 }; yield return new object[] { TargetFrameworkMoniker.Net60 }; #if INCLUDE_NEXT_DOTNET yield return new object[] { TargetFrameworkMoniker.Net70 }; #endif } - - private async Task ExecuteScenario( - TargetFrameworkMoniker tfm, - string scenarioName, - string collectionRuleName, - Action setup, - Func pipelineCallback, - Action servicesCallback = null) - { - EndpointInfoSourceCallback endpointInfoCallback = new(_outputHelper); - EndpointUtilities endpointUtilities = new(_outputHelper); - await using ServerSourceHolder sourceHolder = await endpointUtilities.StartServerAsync(endpointInfoCallback); - - AppRunner runner = new(_outputHelper, Assembly.GetExecutingAssembly(), tfm: tfm); - runner.ConnectionMode = DiagnosticPortConnectionMode.Connect; - runner.DiagnosticPortPath = sourceHolder.TransportName; - runner.ScenarioName = scenarioName; - - Task endpointInfoTask = endpointInfoCallback.WaitAddedEndpointInfoAsync(runner, CommonTestTimeouts.StartProcess); - - await runner.ExecuteAsync(async () => - { - IEndpointInfo endpointInfo = await endpointInfoTask; - - await TestHostHelper.CreateCollectionRulesHost( - _outputHelper, - setup, - async host => - { - ActionListExecutor actionListExecutor = - host.Services.GetRequiredService(); - ICollectionRuleTriggerOperations triggerOperations = - host.Services.GetRequiredService(); - IOptionsMonitor optionsMonitor = - host.Services.GetRequiredService>(); - ILogger logger = - host.Services.GetRequiredService>(); - ISystemClock clock = - host.Services.GetRequiredService(); - - PipelineCallbacks callbacks = new(); - - CollectionRuleContext context = new( - collectionRuleName, - optionsMonitor.Get(collectionRuleName), - endpointInfo, - logger, - clock, - callbacks.NotifyActionsThrottled); - - await using CollectionRulePipeline pipeline = new( - actionListExecutor, - triggerOperations, - context, - callbacks.NotifyPipelineStarted); - - await pipelineCallback(runner, pipeline, callbacks); - - Assert.Equal(1, callbacks.StartedCount); - }, - servicesCallback); - }); - } - - private class PipelineCallbacks - { - private readonly List _entries = new(); - - private int _startedCount; - - public Task StartWaitForPipelineStarted() - { - return RegisterCompletion(PipelineCallbackType.PipelineStarted); - } - - public Task StartWaitForActionsThrottled() - { - return RegisterCompletion(PipelineCallbackType.ActionsThrottled); - } - - public void NotifyActionsThrottled() - { - NotifyCompletions(PipelineCallbackType.ActionsThrottled); - } - - public void NotifyPipelineStarted() - { - _startedCount++; - NotifyCompletions(PipelineCallbackType.PipelineStarted); - } - - private Task RegisterCompletion(PipelineCallbackType callbackType) - { - CompletionEntry entry = new(callbackType); - lock (_entries) - { - _entries.Add(entry); - } - return entry.CompletionTask; - } - - private void NotifyCompletions(PipelineCallbackType callbackType) - { - List matchingEntries; - lock (_entries) - { - matchingEntries = new(_entries.Count); - for (int i = 0; i < _entries.Count; i++) - { - CompletionEntry entry = _entries[i]; - if (_entries[i].CallbackType == callbackType) - { - _entries.RemoveAt(i); - matchingEntries.Add(entry); - i--; - } - } - } - - foreach (CompletionEntry entry in matchingEntries) - { - entry.Complete(); - } - } - - public int StartedCount => _startedCount; - - private class CompletionEntry - { - private readonly TaskCompletionSource _source = new(TaskCreationOptions.RunContinuationsAsynchronously); - - public CompletionEntry(PipelineCallbackType callbackType) - { - CallbackType = callbackType; - } - - public void Complete() - { - _source.TrySetResult(null); - } - - public PipelineCallbackType CallbackType { get; } - - public Task CompletionTask => _source.Task; - } - - private enum PipelineCallbackType - { - PipelineStarted, - ActionsThrottled - } - } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTestsHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTestsHelper.cs new file mode 100644 index 00000000000..27a13d45602 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRulePipelineTestsHelper.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Authentication; +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; +using Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Actions; +using Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Triggers; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Triggers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + internal class CollectionRulePipelineTestsHelper + { + internal static async Task ExecuteScenario( + TargetFrameworkMoniker tfm, + string scenarioName, + string collectionRuleName, + Action setup, + Func pipelineCallback, + ITestOutputHelper outputHelper, + Action servicesCallback = null) + { + EndpointInfoSourceCallback endpointInfoCallback = new(outputHelper); + EndpointUtilities endpointUtilities = new(outputHelper); + await using ServerSourceHolder sourceHolder = await endpointUtilities.StartServerAsync(endpointInfoCallback); + + AppRunner runner = new(outputHelper, Assembly.GetExecutingAssembly(), tfm: tfm); + runner.ConnectionMode = DiagnosticPortConnectionMode.Connect; + runner.DiagnosticPortPath = sourceHolder.TransportName; + runner.ScenarioName = scenarioName; + + Task endpointInfoTask = endpointInfoCallback.WaitAddedEndpointInfoAsync(runner, CommonTestTimeouts.StartProcess); + + await runner.ExecuteAsync(async () => + { + IEndpointInfo endpointInfo = await endpointInfoTask; + + await TestHostHelper.CreateCollectionRulesHost( + outputHelper, + setup, + async host => + { + ActionListExecutor actionListExecutor = + host.Services.GetRequiredService(); + ICollectionRuleTriggerOperations triggerOperations = + host.Services.GetRequiredService(); + IOptionsMonitor optionsMonitor = + host.Services.GetRequiredService>(); + ILogger logger = + host.Services.GetRequiredService>(); + ISystemClock clock = + host.Services.GetRequiredService(); + + PipelineCallbacks callbacks = new(); + + CollectionRuleContext context = new( + collectionRuleName, + optionsMonitor.Get(collectionRuleName), + endpointInfo, + logger, + clock, + callbacks.NotifyActionsThrottled); + + await using CollectionRulePipeline pipeline = new( + actionListExecutor, + triggerOperations, + context, + callbacks.NotifyPipelineStarted); + + await pipelineCallback(runner, pipeline, callbacks); + + Assert.Equal(1, callbacks.StartedCount); + }, + servicesCallback); + }); + } + + /// + /// Manually trigger for a number of iterations () and test + /// that the actions are invoked for the number of expected iterations () and + /// are throttled for the remaining number of iterations. + /// + internal async static Task ManualTriggerAsync( + ManualTriggerService triggerService, + CallbackActionService callbackService, + PipelineCallbacks callbacks, + int iterationCount, + int expectedCount, + MockSystemClock clock, + TimeSpan clockIncrementDuration, + bool completesOnLastExpectedIteration, + CancellationToken token) + { + if (iterationCount < expectedCount) + { + throw new InvalidOperationException("Number of iterations must be greater than or equal to number of expected iterations."); + } + + int iteration = 0; + Task actionStartedTask; + Task actionsThrottledTask = callbacks.StartWaitForActionsThrottled(); + + // Test that the actions are run for each iteration where the actions are expected to run. + while (iteration < expectedCount) + { + iteration++; + + TaskCompletionSource startedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + EventHandler startedHandler = (s, e) => startedSource.TrySetResult(null); + using var _ = token.Register(() => startedSource.TrySetCanceled(token)); + + actionStartedTask = await callbackService.StartWaitForCallbackAsync(token); + + triggerService.NotifyStarted += startedHandler; + + // Manually invoke the trigger. + triggerService.NotifyTriggerSubscribers(); + + // Wait until action has started. + await actionStartedTask.WithCancellation(token); + + // If the pipeline completes on the last expected iteration, the trigger will not be started again. + // Skip this check for the last expected iteration if the pipeline is expected to complete. + if (!completesOnLastExpectedIteration || iteration != expectedCount) + { + await startedSource.WithCancellation(token); + } + + triggerService.NotifyStarted -= startedHandler; + + // Advance the clock source. + clock.Increment(clockIncrementDuration); + } + + // Check that actions were not throttled. + Assert.False(actionsThrottledTask.IsCompleted); + + actionStartedTask = await callbackService.StartWaitForCallbackAsync(token); + + // Test that actions are throttled for remaining iterations. + while (iteration < iterationCount) + { + iteration++; + + TaskCompletionSource startedSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + EventHandler startedHandler = (s, e) => startedSource.TrySetResult(null); + using var _ = token.Register(() => startedSource.TrySetCanceled(token)); + + actionsThrottledTask = callbacks.StartWaitForActionsThrottled(); + + triggerService.NotifyStarted += startedHandler; + + // Manually invoke the trigger. + triggerService.NotifyTriggerSubscribers(); + + // Check throttling has occurred. + await actionsThrottledTask.WithCancellation(token); + + await startedSource.WithCancellation(token); + + triggerService.NotifyStarted -= startedHandler; + + // Advance the clock source. + clock.Increment(clockIncrementDuration); + } + + // Check that no actions have been executed. + Assert.False(actionStartedTask.IsCompleted); + } + + internal class PipelineCallbacks + { + private readonly List _entries = new(); + + private int _startedCount; + + public Task StartWaitForPipelineStarted() + { + return RegisterCompletion(PipelineCallbackType.PipelineStarted); + } + + public Task StartWaitForActionsThrottled() + { + return RegisterCompletion(PipelineCallbackType.ActionsThrottled); + } + + public void NotifyActionsThrottled() + { + NotifyCompletions(PipelineCallbackType.ActionsThrottled); + } + + public void NotifyPipelineStarted() + { + _startedCount++; + NotifyCompletions(PipelineCallbackType.PipelineStarted); + } + + private Task RegisterCompletion(PipelineCallbackType callbackType) + { + CompletionEntry entry = new(callbackType); + lock (_entries) + { + _entries.Add(entry); + } + return entry.CompletionTask; + } + + private void NotifyCompletions(PipelineCallbackType callbackType) + { + List matchingEntries; + lock (_entries) + { + matchingEntries = new(_entries.Count); + for (int i = 0; i < _entries.Count; i++) + { + CompletionEntry entry = _entries[i]; + if (_entries[i].CallbackType == callbackType) + { + _entries.RemoveAt(i); + matchingEntries.Add(entry); + i--; + } + } + } + + foreach (CompletionEntry entry in matchingEntries) + { + entry.Complete(); + } + } + + public int StartedCount => _startedCount; + + private class CompletionEntry + { + private readonly TaskCompletionSource _source = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public CompletionEntry(PipelineCallbackType callbackType) + { + CallbackType = callbackType; + } + + public void Complete() + { + _source.TrySetResult(null); + } + + public PipelineCallbackType CallbackType { get; } + + public Task CompletionTask => _source.Task; + } + + private enum PipelineCallbackType + { + PipelineStarted, + ActionsThrottled + } + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/ActionsServiceCollectionExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/ActionsServiceCollectionExtensions.cs index 18ed1695d57..3927a7b137c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/ActionsServiceCollectionExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/ActionsServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.Monitoring.TestCommon.Options; using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Actions @@ -14,7 +15,16 @@ internal static class ActionsServiceCollectionExtensions public static IServiceCollection RegisterTestAction(this IServiceCollection services, CallbackActionService callback) { services.AddSingleton(callback); - services.RegisterCollectionRuleAction(CallbackAction.ActionName); + services.RegisterCollectionRuleAction(CallbackAction.ActionName); + + return services; + } + + public static IServiceCollection RegisterDelayedTestAction(this IServiceCollection services, CallbackActionService callback) + { + services.AddSingleton(callback); + services.RegisterCollectionRuleAction(DelayedCallbackAction.ActionName); + return services; } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/CallbackAction.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/CallbackAction.cs index 590b5797abd..e7b1e81ee80 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/CallbackAction.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/CallbackAction.cs @@ -6,6 +6,7 @@ using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; +using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; using System; using System.Collections.Generic; using System.Threading; @@ -14,7 +15,7 @@ namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests.CollectionRules.Actions { - internal sealed class CallbackActionFactory : ICollectionRuleActionFactory + internal sealed class CallbackActionFactory : ICollectionRuleActionFactory { private readonly CallbackActionService _service; @@ -23,7 +24,7 @@ public CallbackActionFactory(CallbackActionService service) _service = service; } - public ICollectionRuleAction Create(IEndpointInfo endpointInfo, object options) + public ICollectionRuleAction Create(IEndpointInfo endpointInfo, BaseRecordOptions options) { return new CallbackAction(_service); } @@ -40,6 +41,53 @@ public CallbackAction(CallbackActionService service) _service = service; } + public Task StartAsync(CollectionRuleMetadata collectionRuleMetadata, CancellationToken token) + { + return StartAsync(token); // We don't care about collectionRuleMetadata for testing (yet) + } + + public Task StartAsync(CancellationToken token) + { + return _service.NotifyListeners(token); + } + + public Task WaitForCompletionAsync(CancellationToken token) + { + return Task.FromResult(new CollectionRuleActionResult()); + } + } + + internal sealed class DelayedCallbackActionFactory : ICollectionRuleActionFactory + { + private readonly CallbackActionService _service; + + public DelayedCallbackActionFactory(CallbackActionService service) + { + _service = service; + } + + public ICollectionRuleAction Create(IEndpointInfo endpointInfo, BaseRecordOptions options) + { + return new DelayedCallbackAction(_service); + } + } + + internal sealed class DelayedCallbackAction : ICollectionRuleAction + { + public static readonly string ActionName = nameof(DelayedCallbackAction); + + private readonly CallbackActionService _service; + + public DelayedCallbackAction(CallbackActionService service) + { + _service = service; + } + + public Task StartAsync(CollectionRuleMetadata collectionRuleMetadata, CancellationToken token) + { + return StartAsync(token); // We don't care about collectionRuleMetadata for testing (yet) + } + public Task StartAsync(CancellationToken token) { return _service.NotifyListeners(token); @@ -47,13 +95,20 @@ public Task StartAsync(CancellationToken token) public Task WaitForCompletionAsync(CancellationToken token) { + var currentTime = _service.Clock.UtcNow; + while (_service.Clock.UtcNow == currentTime) + { + // waiting for clock to be ticked (simulated time) + token.ThrowIfCancellationRequested(); + } + return Task.FromResult(new CollectionRuleActionResult()); } } internal sealed class CallbackActionService { - private readonly ISystemClock _clock; + public ISystemClock Clock { get; } private readonly List _entries = new(); private readonly SemaphoreSlim _entriesSemaphore = new(1); private readonly List _executionTimestamps = new(); @@ -64,7 +119,7 @@ internal sealed class CallbackActionService public CallbackActionService(ITestOutputHelper outputHelper, ISystemClock clock = null) { _outputHelper = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); - _clock = clock ?? RealSystemClock.Instance; + Clock = clock ?? RealSystemClock.Instance; } public async Task NotifyListeners(CancellationToken token) @@ -74,9 +129,9 @@ public async Task NotifyListeners(CancellationToken token) { lock (_executionTimestamps) { - _executionTimestamps.Add(_clock.UtcNow.UtcDateTime); + _executionTimestamps.Add(Clock.UtcNow.UtcDateTime); } - + _outputHelper.WriteLine("[Callback] Completing {0} source(s).", _entries.Count); foreach (var entry in _entries) diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/PassThroughAction.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/PassThroughAction.cs index 5abe8a7943d..ccab972b3a2 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/PassThroughAction.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/CollectionRules/Actions/PassThroughAction.cs @@ -5,10 +5,7 @@ using Microsoft.Diagnostics.Monitoring.WebApi; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -29,7 +26,10 @@ public PassThroughAction(IEndpointInfo endpointInfo, PassThroughOptions settings { } - protected override Task ExecuteCoreAsync(TaskCompletionSource startCompletionSource, CancellationToken token) + protected override Task ExecuteCoreAsync( + TaskCompletionSource startCompletionSource, + CollectionRuleMetadata collectionRuleMetadata, + CancellationToken token) { startCompletionSource.TrySetResult(null); @@ -43,7 +43,7 @@ protected override Task ExecuteCoreAsync(TaskComplet } } - internal sealed class PassThroughOptions : ICloneable + internal sealed record class PassThroughOptions : BaseRecordOptions { [ActionOptionsDependencyProperty] public string Input1 { get; set; } @@ -53,7 +53,5 @@ internal sealed class PassThroughOptions : ICloneable [ActionOptionsDependencyProperty] public string Input3 { get; set; } - - public object Clone() => MemberwiseClone(); } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs index de248cce4e4..f1ab50ef65a 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ConfigurationTests.cs @@ -54,6 +54,11 @@ public sealed class ConfigurationTests { WebHostDefaults.ServerUrlsKey, nameof(ConfigurationLevel.UserSettings) } }; + private static readonly Dictionary UserProvidedFileSettingsContent = new(StringComparer.Ordinal) + { + { WebHostDefaults.ServerUrlsKey, nameof(ConfigurationLevel.UserProvidedFileSettings) } + }; + // This needs to be updated and kept in order for any future configuration sections private static readonly List OrderedConfigurationKeys = new() { @@ -65,6 +70,7 @@ public sealed class ConfigurationTests "CollectionRules", "CorsConfiguration", "DiagnosticPort", + "InProcessFeatures", "Metrics", "Storage", "DefaultProcess", @@ -79,6 +85,8 @@ public sealed class ConfigurationTests private const string SampleConfigurationsDirectory = "SampleConfigurations"; + private const string UserProvidedSettingsFileName = "UserSpecifiedFile.json"; // Note: if this name is updated, it must also be updated in the expected show sources configuration files + private readonly ITestOutputHelper _outputHelper; public ConfigurationTests(ITestOutputHelper outputHelper) @@ -101,11 +109,15 @@ public ConfigurationTests(ITestOutputHelper outputHelper) [InlineData(ConfigurationLevel.SharedSettings)] [InlineData(ConfigurationLevel.SharedKeyPerFile)] [InlineData(ConfigurationLevel.MonitorEnvironment)] + [InlineData(ConfigurationLevel.UserProvidedFileSettings)] public void ConfigurationOrderingTest(ConfigurationLevel level) { using TemporaryDirectory contentRootDirectory = new(_outputHelper); using TemporaryDirectory sharedConfigDir = new(_outputHelper); using TemporaryDirectory userConfigDir = new(_outputHelper); + using TemporaryDirectory userProvidedConfigDir = new(_outputHelper); + + string userProvidedConfigFullPath = Path.Combine(userProvidedConfigDir.FullName, UserProvidedSettingsFileName); // Set up the initial settings used to create the host builder. HostBuilderSettings settings = new() @@ -113,7 +125,8 @@ public void ConfigurationOrderingTest(ConfigurationLevel level) Authentication = HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false), ContentRootDirectory = contentRootDirectory.FullName, SharedConfigDirectory = sharedConfigDir.FullName, - UserConfigDirectory = userConfigDir.FullName + UserConfigDirectory = userConfigDir.FullName, + UserProvidedConfigFilePath = level >= ConfigurationLevel.UserProvidedFileSettings ? new FileInfo(userProvidedConfigFullPath) : null }; if (level >= ConfigurationLevel.HostBuilderSettingsUrl) { @@ -147,6 +160,12 @@ public void ConfigurationOrderingTest(ConfigurationLevel level) // is typically used when mounting secrets from a Docker volume. File.WriteAllText(Path.Combine(sharedConfigDir.FullName, WebHostDefaults.ServerUrlsKey), nameof(ConfigurationLevel.SharedKeyPerFile)); } + if (level >= ConfigurationLevel.UserProvidedFileSettings) + { + // This is the user-provided file in the directory specified on the command-line + string userSpecifiedFileSettingsContent = JsonSerializer.Serialize(UserProvidedFileSettingsContent); + File.WriteAllText(userProvidedConfigFullPath, userSpecifiedFileSettingsContent); + } // Create the initial host builder. IHostBuilder builder = HostBuilderHelper.CreateHostBuilder(settings); @@ -216,7 +235,7 @@ public void FullConfigurationTest(bool redact) string generatedConfig = WriteAndRetrieveConfiguration(rootConfiguration, redact); - Assert.Equal(CleanWhitespace(generatedConfig), CleanWhitespace(ConstructExpectedOutput(redact, ExpectedConfigurationsDirectory))); + Assert.Equal(CleanWhitespace(ConstructExpectedOutput(redact, ExpectedConfigurationsDirectory)), CleanWhitespace(generatedConfig)); } /// @@ -232,6 +251,9 @@ public void FullConfigurationWithSourcesTest(bool redact) using TemporaryDirectory contentRootDirectory = new(_outputHelper); using TemporaryDirectory sharedConfigDir = new(_outputHelper); using TemporaryDirectory userConfigDir = new(_outputHelper); + using TemporaryDirectory userProvidedConfigDir = new(_outputHelper); + + string userProvidedConfigFullPath = Path.Combine(userProvidedConfigDir.FullName, UserProvidedSettingsFileName); // Set up the initial settings used to create the host builder. HostBuilderSettings settings = new() @@ -239,13 +261,17 @@ public void FullConfigurationWithSourcesTest(bool redact) Authentication = HostBuilderHelper.CreateAuthConfiguration(noAuth: false, tempApiKey: false), ContentRootDirectory = contentRootDirectory.FullName, SharedConfigDirectory = sharedConfigDir.FullName, - UserConfigDirectory = userConfigDir.FullName + UserConfigDirectory = userConfigDir.FullName, + UserProvidedConfigFilePath = new FileInfo(userProvidedConfigFullPath) }; settings.Urls = new[] { "https://localhost:44444" }; // This corresponds to the value in SampleConfigurations/URLs.json + // This is the user-provided file in the directory specified on the command-line + File.WriteAllText(userProvidedConfigFullPath, ConstructSettingsJson("Egress.json")); + // This is the settings.json file in the user profile directory. - File.WriteAllText(Path.Combine(userConfigDir.FullName, "settings.json"), ConstructSettingsJson("Egress.json", "CollectionRules.json", "CollectionRuleDefaults.json", "Templates.json")); + File.WriteAllText(Path.Combine(userConfigDir.FullName, "settings.json"), ConstructSettingsJson("CollectionRules.json", "CollectionRuleDefaults.json", "Templates.json")); // This is the appsettings.json file that is normally next to the entrypoint assembly. // The location of the appsettings.json is determined by the content root in configuration. @@ -253,7 +279,7 @@ public void FullConfigurationWithSourcesTest(bool redact) // This is the settings.json file in the shared configuration directory that is visible // to all users on the machine e.g. /etc/dotnet-monitor on Unix systems. - File.WriteAllText(Path.Combine(sharedConfigDir.FullName, "settings.json"), ConstructSettingsJson("Logging.json", "Metrics.json")); + File.WriteAllText(Path.Combine(sharedConfigDir.FullName, "settings.json"), ConstructSettingsJson("Logging.json", "Metrics.json", "InProcessFeatures.json")); // Create the initial host builder. IHostBuilder builder = HostBuilderHelper.CreateHostBuilder(settings); @@ -271,7 +297,7 @@ public void FullConfigurationWithSourcesTest(bool redact) string generatedConfig = WriteAndRetrieveConfiguration(rootConfiguration, redact, showSources: true); - Assert.Equal(CleanWhitespace(generatedConfig), CleanWhitespace(ConstructExpectedOutput(redact, ExpectedShowSourcesConfigurationsDirectory))); + Assert.Equal(CleanWhitespace(ConstructExpectedOutput(redact, ExpectedShowSourcesConfigurationsDirectory)), CleanWhitespace(generatedConfig)); } /// @@ -410,7 +436,8 @@ private Dictionary GetConfigurationFileNames(bool redact) { "CollectionRules", "CollectionRules.json" }, { "CollectionRuleDefaults", "CollectionRuleDefaults.json" }, { "Templates", "Templates.json" }, - { "Authentication", redact ? "AuthenticationRedacted.json" : "AuthenticationFull.json" } + { "Authentication", redact ? "AuthenticationRedacted.json" : "AuthenticationFull.json" }, + { "InProcessFeatures", "InProcessFeatures.json" } }; } @@ -434,7 +461,8 @@ public enum ConfigurationLevel UserSettings, SharedSettings, SharedKeyPerFile, - MonitorEnvironment + MonitorEnvironment, + UserProvidedFileSettings } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/DiagnosticPortConfigurations/Connect.txt b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/DiagnosticPortConfigurations/Connect.txt index c8045d7a184..0d3afec4253 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/DiagnosticPortConfigurations/Connect.txt +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/DiagnosticPortConfigurations/Connect.txt @@ -1,4 +1,3 @@ "DiagnosticPort": { - "ConnectionMode": "Connect", - "EndpointName": null -} \ No newline at end of file + "ConnectionMode": "Connect" +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Egress/AzureBlob/AzureBlobEgressProviderTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Egress/AzureBlob/AzureBlobEgressProviderTests.cs new file mode 100644 index 00000000000..32e543a96d8 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Egress/AzureBlob/AzureBlobEgressProviderTests.cs @@ -0,0 +1,456 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Queues; +using Azure.Storage.Queues.Models; +using Azure.Storage.Sas; +using Microsoft.Diagnostics.Monitoring.TestCommon; +using Microsoft.Diagnostics.Monitoring.TestCommon.Fixtures; +using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor.Egress; +using Microsoft.Diagnostics.Tools.Monitor.Egress.AzureBlob; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests.Egress.AzureBlob +{ + /* + [TargetFrameworkMonikerTrait(TargetFrameworkMonikerExtensions.CurrentTargetFrameworkMoniker)] + public class AzureBlobEgressProviderTests : IClassFixture, IDisposable + { + public enum UploadAction + { + ProvideUploadStream, + WriteToProviderStream + } + + private readonly ITestOutputHelper _outputHelper; + private readonly AzuriteFixture _azuriteFixture; + private readonly TemporaryDirectory _tempDirectory; + + private readonly string _testUploadFile; + private readonly string _testUploadFileHash; + + public AzureBlobEgressProviderTests(ITestOutputHelper outputHelper, AzuriteFixture azuriteFixture) + { + _outputHelper = outputHelper; + _azuriteFixture = azuriteFixture; + _tempDirectory = new TemporaryDirectory(outputHelper); + + _testUploadFile = Path.Combine(_tempDirectory.FullName, Path.GetRandomFileName()); + File.WriteAllText(_testUploadFile, "Sample Contents\n123"); + _testUploadFileHash = GetFileSHA256(_testUploadFile); + } + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_UploadsCorrectData(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(create: false); + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings(containerClient); + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act + string identifier = await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None); + + // Assert + List blobs = await GetAllBlobsAsync(containerClient); + BlobItem resultingBlob = Assert.Single(blobs); + + string downloadedFile = await DownloadBlobAsync(containerClient, resultingBlob.Name, CancellationToken.None); + Assert.Equal(_testUploadFileHash, GetFileSHA256(downloadedFile)); + } + + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_Supports_QueueMessages(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(create: false); + QueueClient queueClient = await ConstructQueueContainerClientAsync(create: false); + + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings(containerClient, queueClient); + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act + string identifier = await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None); + + // Assert + List blobs = await GetAllBlobsAsync(containerClient); + List messages = await GetAllMessagesAsync(queueClient); + + ValidateQueue(blobs, messages, expectedCount: 1); + } + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_Supports_RestrictiveSasToken(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(); + + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings( + containerClient, + sasToken: ConstructBlobContainerSasToken(containerClient)); + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act + string identifier = await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None); + + // Assert + List blobs = await GetAllBlobsAsync(containerClient); + + BlobItem resultingBlob = Assert.Single(blobs); + Assert.Equal($"{providerOptions.BlobPrefix}/{artifactSettings.Name}", resultingBlob.Name); + } + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_Supports_RestrictiveQueueSasToken(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(); + QueueClient queueClient = await ConstructQueueContainerClientAsync(create: true); + + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings( + containerClient, + queueClient, + queueSasToken: ConstructQueueSasToken(queueClient)); + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act + string identifier = await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None); + + // Assert + List blobs = await GetAllBlobsAsync(containerClient); + List messages = await GetAllMessagesAsync(queueClient); + + ValidateQueue(blobs, messages, expectedCount: 1); + } + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_Supports_OnlyRestrictiveSasTokens(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(create: true); + QueueClient queueClient = await ConstructQueueContainerClientAsync(create: true); + + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings( + containerClient, + queueClient, + sasToken: ConstructBlobContainerSasToken(containerClient), + queueSasToken: ConstructQueueSasToken(queueClient)); + + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act + string identifier = await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None); + + // Assert + List blobs = await GetAllBlobsAsync(containerClient); + List messages = await GetAllMessagesAsync(queueClient); + + ValidateQueue(blobs, messages, expectedCount: 1); + } + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_ThrowsWhen_ContainerDoesNotExistAndUsingRestrictiveSasToken(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(create: false); + + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings( + containerClient, + sasToken: ConstructBlobContainerSasToken(containerClient)); + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act & Assert + await Assert.ThrowsAsync(async () => await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None)); + } + + [ConditionalTheory(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)] + [InlineData(UploadAction.ProvideUploadStream)] + [InlineData(UploadAction.WriteToProviderStream)] + public async Task AzureBlobEgress_DoesNotThrowWhen_QueueDoesNotExistAndUsingRestrictiveQueueSasToken(UploadAction uploadAction) + { + _azuriteFixture.SkipTestIfNotAvailable(); + + // Arrange + TestOutputLoggerProvider loggerProvider = new(_outputHelper); + AzureBlobEgressProvider egressProvider = new(loggerProvider.CreateLogger()); + + BlobContainerClient containerClient = await ConstructBlobContainerClientAsync(); + QueueClient queueClient = await ConstructQueueContainerClientAsync(create: false); + + AzureBlobEgressProviderOptions providerOptions = ConstructEgressProviderSettings( + containerClient, + queueClient, + queueSasToken: ConstructQueueSasToken(queueClient)); + EgressArtifactSettings artifactSettings = ConstructArtifactSettings(); + + // Act + string identifier = await EgressAsync(uploadAction, egressProvider, providerOptions, artifactSettings, CancellationToken.None); + + // Assert + List blobs = await GetAllBlobsAsync(containerClient); + List messages = await GetAllMessagesAsync(queueClient); + + Assert.Single(blobs); + Assert.Empty(messages); + } + + private Task ProvideUploadStreamAsync(CancellationToken token) + { + return Task.FromResult(new FileStream(_testUploadFile, FileMode.Open, FileAccess.Read)); + } + + private async Task WriteToEgressStreamAsync(Stream stream, CancellationToken token) + { + await using FileStream fs = new(_testUploadFile, FileMode.Open, FileAccess.Read); + await fs.CopyToAsync(stream, token); + } + + private async Task EgressAsync(UploadAction uploadAction, AzureBlobEgressProvider egressProvider, AzureBlobEgressProviderOptions options, EgressArtifactSettings artifactSettings, CancellationToken token) + { + return uploadAction switch + { + UploadAction.ProvideUploadStream => await egressProvider.EgressAsync(options, ProvideUploadStreamAsync, artifactSettings, token), + UploadAction.WriteToProviderStream => await egressProvider.EgressAsync(options, WriteToEgressStreamAsync, artifactSettings, token), + _ => throw new ArgumentException(nameof(uploadAction)), + }; + } + + private async Task ConstructBlobContainerClientAsync(string containerName = null, bool create = true) + { + BlobServiceClient serviceClient = new(_azuriteFixture.Account.ConnectionString); + + containerName ??= Guid.NewGuid().ToString("D"); + BlobContainerClient containerClient = serviceClient.GetBlobContainerClient(containerName); + + if (create) + { + await containerClient.CreateIfNotExistsAsync(); + } + + return containerClient; + } + + private async Task ConstructQueueContainerClientAsync(string queueName = null, bool create = true) + { + QueueServiceClient serviceClient = new(_azuriteFixture.Account.ConnectionString); + + queueName ??= Guid.NewGuid().ToString("D"); + QueueClient queueClient = serviceClient.GetQueueClient(queueName); + + if (create) + { + await queueClient.CreateIfNotExistsAsync(); + } + + return queueClient; + } + + private EgressArtifactSettings ConstructArtifactSettings(int numberOfMetadataEntries = 2) + { + EgressArtifactSettings settings = new() + { + ContentType = ContentTypes.ApplicationOctetStream, + Name = Guid.NewGuid().ToString("D") + }; + + for (int i = 0; i < numberOfMetadataEntries; i++) + { + settings.Metadata.Add($"key_{i}", Guid.NewGuid().ToString("D")); + } + + return settings; + } + + private AzureBlobEgressProviderOptions ConstructEgressProviderSettings(BlobContainerClient containerClient, QueueClient queueClient = null, string sasToken = null, string queueSasToken = null) + { + AzureBlobEgressProviderOptions options = new() + { + AccountUri = new Uri(_azuriteFixture.Account.BlobEndpoint), + ContainerName = containerClient.Name, + BlobPrefix = Guid.NewGuid().ToString("D"), + QueueAccountUri = (queueClient == null) ? null : new Uri(_azuriteFixture.Account.QueueEndpoint), + QueueName = queueClient?.Name, + QueueSharedAccessSignature = queueSasToken + }; + + if (sasToken == null) + { + options.AccountKey = _azuriteFixture.Account.Key; + } + else + { + options.SharedAccessSignature = sasToken; + } + + return options; + } + + private async Task> GetAllBlobsAsync(BlobContainerClient containerClient) + { + List blobs = new(); + + try + { + var resultSegment = containerClient.GetBlobsAsync(BlobTraits.All).AsPages(default); + await foreach (Page blobPage in resultSegment) + { + foreach (BlobItem blob in blobPage.Values) + { + blobs.Add(blob); + } + } + } + catch (XmlException) + { + // Can be thrown when there are no blobs + } + + return blobs; + } + + private async Task> GetAllMessagesAsync(QueueClient queueClient) + { + try + { + const int MaxReceiveMessages = 32; + Response messages = await queueClient.ReceiveMessagesAsync(MaxReceiveMessages); + return messages.Value.ToList(); + } + catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.NotFound) + { + + } + + return new List(); + } + + private string DecodeQueueMessageBody(BinaryData body) + { + // Our queue messages are UTF-8 encoded text that is then Base64 encoded and then stored as a byte array. + return Encoding.UTF8.GetString(Convert.FromBase64String(body.ToString())); + } + + private async Task DownloadBlobAsync(BlobContainerClient containerClient, string blobName, CancellationToken token) + { + BlobClient blobClient = containerClient.GetBlobClient(blobName); + + string downloadPath = Path.Combine(_tempDirectory.FullName, Path.GetRandomFileName()); + + await using FileStream fs = File.OpenWrite(downloadPath); + await blobClient.DownloadToAsync(fs, token); + + return downloadPath; + } + + private string GetFileSHA256(string filePath) + { + using SHA256 sha = SHA256.Create(); + using FileStream fileStream = File.OpenRead(filePath); + return BitConverter.ToString(sha.ComputeHash(fileStream)); + } + + private void ValidateQueue(List blobs, List messages, int expectedCount) + { + Assert.Equal(expectedCount, messages.Count); + Assert.Equal(expectedCount, blobs.Count); + + HashSet blobNames = new(blobs.Select((b) => b.Name)); + foreach (QueueMessage message in messages) + { + Assert.Contains(DecodeQueueMessageBody(message.Body), blobNames); + } + } + + private string ConstructBlobContainerSasToken(BlobContainerClient containerClient) + { + // Requires: + // - Add for UploadAction.ProvideUploadStream + // - Write for UploadAction.WriteToProviderStream + BlobSasBuilder sasBuilder = new( + BlobContainerSasPermissions.Add | BlobContainerSasPermissions.Write, + DateTimeOffset.MaxValue) + { + StartsOn = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(5)) + }; + Uri sasUri = containerClient.GenerateSasUri(sasBuilder); + + return sasUri.Query; + } + + private string ConstructQueueSasToken(QueueClient queueClient) + { + QueueSasBuilder sasBuilder = new(QueueSasPermissions.Add, DateTimeOffset.MaxValue) + { + StartsOn = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(5)) + }; + Uri sasUri = queueClient.GenerateSasUri(sasBuilder); + + return sasUri.Query; + } + + public void Dispose() + { + _tempDirectory.Dispose(); + } + }*/ + +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EnvironmentVariableActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EnvironmentVariableActionTests.cs index 0d652a325db..298fd608522 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EnvironmentVariableActionTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/EnvironmentVariableActionTests.cs @@ -11,7 +11,6 @@ using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Exceptions; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; using Microsoft.Extensions.DependencyInjection; -using System.Threading; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -162,10 +161,10 @@ await TestHostHelper.CreateCollectionRulesHost( outputHelper: _outputHelper, setup: (Tools.Monitor.RootOptions rootOptions) => { - rootOptions.CreateCollectionRule(DefaultRuleName) - .SetStartupTrigger() - .AddSetEnvironmentVariableAction(DefaultVarName, DefaultVarValue) - .AddGetEnvironmentVariableAction(DefaultVarName); + rootOptions.CreateCollectionRule(DefaultRuleName) + .SetStartupTrigger() + .AddSetEnvironmentVariableAction(DefaultVarName, DefaultVarValue) + .AddGetEnvironmentVariableAction(DefaultVarName); }, hostCallback: async (Extensions.Hosting.IHost host) => { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExecuteActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExecuteActionTests.cs index 17e056758d7..a539d6f0afe 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExecuteActionTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExecuteActionTests.cs @@ -33,7 +33,7 @@ public async Task ExecuteAction_ZeroExitCode() await ValidateAction( options => { - options.Path = DotNetHost.HostExePath; + options.Path = DotNetHost.GetPath(); options.Arguments = ExecuteActionTestHelper.GenerateArgumentsString(new string[] { ActionTestsConstants.ZeroExitCode }); }, async (action, token) => @@ -52,7 +52,7 @@ public async Task ExecuteAction_NonzeroExitCode() await ValidateAction( options => { - options.Path = DotNetHost.HostExePath; + options.Path = DotNetHost.GetPath(); options.Arguments = ExecuteActionTestHelper.GenerateArgumentsString(new string[] { ActionTestsConstants.NonzeroExitCode }); }, async (action, token) => @@ -77,7 +77,7 @@ public async Task ExecuteAction_TokenCancellation() await ValidateAction( options => { - options.Path = DotNetHost.HostExePath; + options.Path = DotNetHost.GetPath(); options.Arguments = ExecuteActionTestHelper.GenerateArgumentsString(new string[] { ActionTestsConstants.Sleep, sleepMsArg }); }, async (action, token) => @@ -105,7 +105,7 @@ public async Task ExecuteAction_TextFileOutput() await ValidateAction( options => { - options.Path = DotNetHost.HostExePath; + options.Path = DotNetHost.GetPath(); options.Arguments = ExecuteActionTestHelper.GenerateArgumentsString(new string[] { ActionTestsConstants.TextFileOutput, textFileOutputPath, testMessage }); }, async (action, token) => @@ -146,7 +146,7 @@ public async Task ExecuteAction_IgnoreExitCode() await ValidateAction( options => { - options.Path = DotNetHost.HostExePath; + options.Path = DotNetHost.GetPath(); options.Arguments = ExecuteActionTestHelper.GenerateArgumentsString(new string[] { ActionTestsConstants.NonzeroExitCode }); options.IgnoreExitCode = true; }, diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/EgressRedacted.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/EgressRedacted.json index 294b94d8b97..c7b5e321372 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/EgressRedacted.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/EgressRedacted.json @@ -14,7 +14,10 @@ "AccountKey": ":NOT PRESENT:", "SharedAccessSignatureName": ":NOT PRESENT:", "AccountKeyName": "MonitorBlobAccountKey", - "ManagedIdentityClientId": ":NOT PRESENT:" + "ManagedIdentityClientId": ":NOT PRESENT:", + "QueueSharedAccessSignature": ":NOT PRESENT:", + "QueueSharedAccessSignatureName": ":NOT PRESENT:", + "Metadata": ":NOT PRESENT:" } }, "FileSystem": { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/InProcessFeatures.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/InProcessFeatures.json new file mode 100644 index 00000000000..cbcac88e641 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedConfigurations/InProcessFeatures.json @@ -0,0 +1,3 @@ +{ + "Enabled": "True" +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DefaultProcess.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DefaultProcess.json index 631627ccb2c..ba3c591df82 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DefaultProcess.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DefaultProcess.json @@ -1,8 +1,8 @@ { "Filters": [ { - "Key": "ProcessId" /*Microsoft.Extensions.Configuration.ChainedConfigurationProvider*/, - "Value": "12345" /*Microsoft.Extensions.Configuration.ChainedConfigurationProvider*/ + "Key": "ProcessId" /*MemoryConfigurationProvider*/, + "Value": "12345" /*MemoryConfigurationProvider*/ } ] } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DiagnosticPort.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DiagnosticPort.json index 6b8d25a54ad..9655c2aa63f 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DiagnosticPort.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/DiagnosticPort.json @@ -1,4 +1,4 @@ { - "ConnectionMode": "Listen" /*Microsoft.Extensions.Configuration.ChainedConfigurationProvider*/, - "EndpointName": "\\\\.\\pipe\\dotnet-monitor-pipe" /*Microsoft.Extensions.Configuration.ChainedConfigurationProvider*/ + "ConnectionMode": "Listen" /*MemoryConfigurationProvider*/, + "EndpointName": "\\\\.\\pipe\\dotnet-monitor-pipe" /*MemoryConfigurationProvider*/ } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json index dc4c1337989..771418e4df4 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressFull.json @@ -1,18 +1,18 @@ { "AzureBlobStorage": { "monitorBlob": { - "accountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "accountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "blobPrefix": "artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "containerName": "dotnet-monitor" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "accountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "accountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "blobPrefix": "artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "containerName": "dotnet-monitor" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ } }, "FileSystem": { "artifacts": { - "directoryPath": "/artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "directoryPath": "/artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ } }, "Properties": { - "MonitorBlobAccountKey": "accountKey" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "MonitorBlobAccountKey": "accountKey" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json index 44dd1093cda..ba6d15adcb7 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/EgressRedacted.json @@ -1,25 +1,28 @@ { "Properties": { - "MonitorBlobAccountKey": ":REDACTED:" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ + "MonitorBlobAccountKey": ":REDACTED:" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/ }, "AzureBlobStorage": { "monitorBlob": { - "AccountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "BlobPrefix": "artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "ContainerName": "dotnet-monitor" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, + "AccountUri": "https://exampleaccount.blob.core.windows.net" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "BlobPrefix": "artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "ContainerName": "dotnet-monitor" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, "CopyBufferSize": ":NOT PRESENT:", "QueueName": ":NOT PRESENT:", "QueueAccountUri": ":NOT PRESENT:", "SharedAccessSignature": ":NOT PRESENT:", "AccountKey": ":NOT PRESENT:", "SharedAccessSignatureName": ":NOT PRESENT:", - "AccountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, - "ManagedIdentityClientId": ":NOT PRESENT:" + "AccountKeyName": "MonitorBlobAccountKey" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, + "ManagedIdentityClientId": ":NOT PRESENT:", + "QueueSharedAccessSignature": ":NOT PRESENT:", + "QueueSharedAccessSignatureName": ":NOT PRESENT:", + "Metadata": ":NOT PRESENT:" } }, "FileSystem": { "artifacts": { - "DirectoryPath": " /artifacts" /*JsonConfigurationProvider for 'settings.json' (Optional)*/, + "DirectoryPath": " /artifacts" /*JsonConfigurationProvider for 'UserSpecifiedFile.json' (Optional)*/, "IntermediateDirectoryPath": ":NOT PRESENT:", "CopyBufferSize": ":NOT PRESENT:" } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/InProcessFeatures.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/InProcessFeatures.json new file mode 100644 index 00000000000..1d71045d172 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/InProcessFeatures.json @@ -0,0 +1,3 @@ +{ + "Enabled": "True" /*JsonConfigurationProvider for 'settings.json' (Optional)*/ +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/URLs.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/URLs.json index d788cd2c20f..5590f355b5e 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/URLs.json +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/ExpectedShowSourcesConfigurations/URLs.json @@ -1 +1 @@ -"https://localhost:44444"/*Microsoft.Extensions.Configuration.ChainedConfigurationProvider*/ +"https://localhost:44444"/*CommandLineConfigurationProvider*/ diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/HostBuilderExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/HostBuilderExtensions.cs index ad1ba9aa31c..d1e680fff04 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/HostBuilderExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/HostBuilderExtensions.cs @@ -70,7 +70,7 @@ public static IHostBuilder ReplaceMonitorEnvironment(this IHostBuilder builder, { return builder.ConfigureAppConfiguration(builder => { - ReplaceEnvironment(builder.Sources, HostBuilderHelper.ConfigPrefix, values); + ReplaceEnvironment(builder.Sources, ToolIdentifiers.StandardPrefix, values); }); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LimitsTestsConstants.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LimitsTestsConstants.cs index 9a376bb998c..f89beb62673 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LimitsTestsConstants.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LimitsTestsConstants.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; - namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests { internal class LimitsTestsConstants diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LoadProfilerActionTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LoadProfilerActionTests.cs index 103231531cd..66165421d18 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LoadProfilerActionTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/LoadProfilerActionTests.cs @@ -6,6 +6,7 @@ using Microsoft.Diagnostics.Monitoring.TestCommon.Options; using Microsoft.Diagnostics.Monitoring.TestCommon.Runners; using Microsoft.Diagnostics.Monitoring.WebApi; +using Microsoft.Diagnostics.Tools.Monitor; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Actions; using Microsoft.Diagnostics.Tools.Monitor.CollectionRules.Options.Actions; @@ -25,11 +26,6 @@ public sealed class LoadProfilerActionTests { private const string DefaultRuleName = "ProfilerTestRule"; - // This environment variable name is embedded into the profiler and set at profiler initialization. - // The value is determined BEFORE native build by the generation of the product version into the - // _productversion.h header file. - private const string ProductVersionEnvVarName = "DotnetMonitorProfiler_ProductVersion"; - private readonly ITestOutputHelper _outputHelper; private readonly EndpointUtilities _endpointUtilities; @@ -46,21 +42,16 @@ public LoadProfilerActionTests(ITestOutputHelper outputHelper) [MemberData(nameof(ActionTestsHelper.GetTfmArchitectureProfilerPath), MemberType = typeof(ActionTestsHelper))] public async Task LoadProfilerAsStartupProfilerTest(TargetFrameworkMoniker tfm, Architecture architecture, string profilerPath) { - if (Architecture.X86 == architecture) - { - _outputHelper.WriteLine("Skipping x86 architecture since x86 host is not used at this time."); - return; - } - string profilerFileName = Path.GetFileName(profilerPath); await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => { rootOptions.CreateCollectionRule(DefaultRuleName) + .AddSetEnvironmentVariableAction(ProfilerIdentifiers.EnvironmentVariables.RuntimeInstanceId, ConfigurationTokenParser.RuntimeIdReference) .AddLoadProfilerAction(options => { options.Path = profilerPath; - options.Clsid = NativeLibraryHelper.MonitorProfilerClsid; + options.Clsid = ProfilerIdentifiers.Clsid.Guid; }) .SetStartupTrigger(); }, async host => @@ -69,15 +60,14 @@ await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => await using ServerSourceHolder sourceHolder = await _endpointUtilities.StartServerAsync(callback); AppRunner runner = _endpointUtilities.CreateAppRunner(sourceHolder.TransportName, tfm); + runner.Architecture = architecture; runner.ScenarioName = TestAppScenarios.AsyncWait.Name; await runner.ExecuteAsync(async () => { // At this point, the profiler has already been initialized and managed code is already running. // Use any of the initialization state of the profiler to validate that it is loaded. - string productVersion = await runner.GetEnvironmentVariable(ProductVersionEnvVarName, CommonTestTimeouts.EnvVarsTimeout); - Assert.False(string.IsNullOrEmpty(productVersion), "Expected product version to not be null or empty."); - _outputHelper.WriteLine("{0} = {1}", ProductVersionEnvVarName, productVersion); + await ProfilerHelper.VerifyProductVersionEnvironmentVariableAsync(runner, _outputHelper); await runner.SendCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue); }); @@ -100,8 +90,13 @@ public LoadProfilerCallback(ITestOutputHelper outputHelper, IHost host) public override async Task OnBeforeResumeAsync(IEndpointInfo endpointInfo, CancellationToken token) { + SetEnvironmentVariableOptions envOptions = ActionTestsHelper.GetActionOptions(_host, DefaultRuleName, actionIndex: 0); + Assert.True(_host.Services.GetService().TryCreateFactory(KnownCollectionRuleActions.SetEnvironmentVariable, out ICollectionRuleActionFactoryProxy setEnvFactory)); + ICollectionRuleAction setEnvAction = setEnvFactory.Create(endpointInfo, envOptions); + await ActionTestsHelper.ExecuteAndDisposeAsync(setEnvAction, CommonTestTimeouts.EnvVarsTimeout); + // Load the profiler into the target process - LoadProfilerOptions options = ActionTestsHelper.GetActionOptions(_host, DefaultRuleName); + LoadProfilerOptions options = ActionTestsHelper.GetActionOptions(_host, DefaultRuleName, actionIndex: 1); ICollectionRuleActionFactoryProxy factory; Assert.True(_host.Services.GetService().TryCreateFactory(KnownCollectionRuleActions.LoadProfiler, out factory)); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj index 49e8cf8349e..d24a1a1d003 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests.csproj @@ -18,6 +18,9 @@ Always + + Always + Always @@ -30,6 +33,9 @@ Always + + Always + Always @@ -108,6 +114,9 @@ Always + + Always + Always diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs index c5c8b73c25f..6574caa9bb7 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/Options/CollectionRuleOptionsExtensions.cs @@ -128,6 +128,34 @@ public static CollectionRuleOptions AddCollectTraceAction(this CollectionRuleOpt }); } + public static CollectionRuleOptions AddCollectLiveMetricsAction(this CollectionRuleOptions options, string egress = null, Action callback = null) + { + return options.AddAction( + KnownCollectionRuleActions.CollectLiveMetrics, + actionOptions => + { + CollectLiveMetricsOptions collectLiveMetricsOptions = new(); + collectLiveMetricsOptions.Egress = egress; + + callback?.Invoke(collectLiveMetricsOptions); + + actionOptions.Settings = collectLiveMetricsOptions; + }); + } + + public static CollectionRuleOptions AddCollectStacksAction(this CollectionRuleOptions options, string egress, CallStackFormat? format = null) + { + return options.AddAction( + KnownCollectionRuleActions.CollectStacks, + actionOptions => + { + CollectStacksOptions collectStacksOptions = new(); + collectStacksOptions.Egress = egress; + collectStacksOptions.Format = format; + actionOptions.Settings = collectStacksOptions; + }); + } + public static CollectionRuleOptions AddExecuteAction(this CollectionRuleOptions options, string path, string arguments = null, bool? waitForCompletion = null) { return options.AddAction( @@ -145,28 +173,28 @@ public static CollectionRuleOptions AddExecuteAction(this CollectionRuleOptions public static CollectionRuleOptions AddExecuteActionAppAction(this CollectionRuleOptions options, params string[] args) { - options.AddExecuteAction(DotNetHost.HostExePath, ExecuteActionTestHelper.GenerateArgumentsString(args)); + options.AddExecuteAction(DotNetHost.GetPath(), ExecuteActionTestHelper.GenerateArgumentsString(args)); return options; } public static CollectionRuleOptions AddExecuteActionAppAction(this CollectionRuleOptions options, bool waitForCompletion, params string[] args) { - options.AddExecuteAction(DotNetHost.HostExePath, ExecuteActionTestHelper.GenerateArgumentsString(args), waitForCompletion); + options.AddExecuteAction(DotNetHost.GetPath(), ExecuteActionTestHelper.GenerateArgumentsString(args), waitForCompletion); return options; } public static CollectionRuleOptions AddLoadProfilerAction(this CollectionRuleOptions options, Action configureOptions) { - return options.AddAction( - KnownCollectionRuleActions.LoadProfiler, - callback: actionOptions => - { - LoadProfilerOptions loadProfilerOptions = new(); - configureOptions?.Invoke(loadProfilerOptions); - actionOptions.Settings = loadProfilerOptions; - }); + return options.AddAction( + KnownCollectionRuleActions.LoadProfiler, + callback: actionOptions => + { + LoadProfilerOptions loadProfilerOptions = new(); + configureOptions?.Invoke(loadProfilerOptions); + actionOptions.Settings = loadProfilerOptions; + }); } public static CollectionRuleOptions AddSetEnvironmentVariableAction(this CollectionRuleOptions options, string name, string value = null) @@ -198,7 +226,7 @@ public static CollectionRuleOptions AddGetEnvironmentVariableAction(this Collect }); } - public static CollectionRuleOptions SetActionLimits(this CollectionRuleOptions options, int? count = null, TimeSpan? slidingWindowDuration = null) + public static CollectionRuleOptions SetActionLimits(this CollectionRuleOptions options, int? count = null, TimeSpan? slidingWindowDuration = null, TimeSpan? ruleDuration = null) { if (null == options.Limits) { @@ -207,6 +235,7 @@ public static CollectionRuleOptions SetActionLimits(this CollectionRuleOptions o options.Limits.ActionCount = count; options.Limits.ActionCountSlidingWindowDuration = slidingWindowDuration; + options.Limits.RuleDuration = ruleDuration; return options; } @@ -217,7 +246,7 @@ public static CollectionRuleOptions SetDurationLimit(this CollectionRuleOptions { options.Limits = new CollectionRuleLimitsOptions(); } - + options.Limits.RuleDuration = duration; return options; @@ -468,7 +497,7 @@ public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOp return collectTraceOptions; } - public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress) + public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOptions ruleOptions, int actionIndex, IEnumerable providers, string expectedEgress, TraceEventFilter expectedStoppingEvent = null) { CollectTraceOptions collectTraceOptions = ruleOptions.VerifyAction( actionIndex, KnownCollectionRuleActions.CollectTrace); @@ -476,6 +505,7 @@ public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOp Assert.Equal(expectedEgress, collectTraceOptions.Egress); Assert.NotNull(collectTraceOptions.Providers); Assert.Equal(providers.Count(), collectTraceOptions.Providers.Count); + Assert.Equal(expectedStoppingEvent, collectTraceOptions.StoppingEvent); int index = 0; foreach (EventPipeProvider expectedProvider in providers) @@ -505,6 +535,16 @@ public static CollectTraceOptions VerifyCollectTraceAction(this CollectionRuleOp return collectTraceOptions; } + public static CollectLiveMetricsOptions VerifyCollectLiveMetricsAction(this CollectionRuleOptions ruleOptions, int actionIndex, string expectedEgress) + { + CollectLiveMetricsOptions collectLiveMetricsOptions = ruleOptions.VerifyAction( + actionIndex, KnownCollectionRuleActions.CollectLiveMetrics); + + Assert.Equal(expectedEgress, collectLiveMetricsOptions.Egress); + + return collectLiveMetricsOptions; + } + public static ExecuteOptions VerifyExecuteAction(this CollectionRuleOptions ruleOptions, int actionIndex, string expectedPath, string expectedArguments = null) { ExecuteOptions executeOptions = ruleOptions.VerifyAction( diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/SampleConfigurations/InProcessFeatures.json b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/SampleConfigurations/InProcessFeatures.json new file mode 100644 index 00000000000..25d588f9605 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/SampleConfigurations/InProcessFeatures.json @@ -0,0 +1,5 @@ +{ + "InProcessFeatures": { + "Enabled": "True" + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TemplatesTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TemplatesTests.cs index 13d57adab47..abd18cff867 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TemplatesTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TemplatesTests.cs @@ -43,7 +43,7 @@ public async void TemplatesTranslationSuccessTest() { using TemporaryDirectory userConfigDir = new(_outputHelper); - await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => {}, host => + await TestHostHelper.CreateCollectionRulesHost(_outputHelper, rootOptions => { }, host => { IOptionsMonitor optionsMonitor = host.Services.GetRequiredService>(); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestExperimentalFlags.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestExperimentalFlags.cs new file mode 100644 index 00000000000..5a1f941fc13 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestExperimentalFlags.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + internal sealed class TestExperimentalFlags : Microsoft.Diagnostics.Monitoring.WebApi.IExperimentalFlags + { + public bool IsCallStacksEnabled { get; set; } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs index 003449decd5..1f8ef348e3d 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestHostHelper.cs @@ -80,14 +80,12 @@ public static IHost CreateHost( builder.AddInMemoryCollection(configurationValues); - builder.ConfigureStorageDefaults(); - if (null != overrideSource) { overrideSource.ForEach(source => builder.Sources.Add(source)); } }) - .ConfigureLogging( loggingBuilder => + .ConfigureLogging(loggingBuilder => { loggingCallback?.Invoke(loggingBuilder); @@ -108,6 +106,8 @@ public static IHost CreateHost( services.AddSingleton(); services.ConfigureStorage(context.Configuration); + services.ConfigureInProcessFeatures(context.Configuration); + services.AddSingleton(); servicesCallback?.Invoke(services); }) .Build(); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestLogger.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestLogger.cs index 81b334deffb..942196e4fae 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestLogger.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestLogger.cs @@ -5,9 +5,6 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests { diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestOutputLogger.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestOutputLogger.cs index 998d495af29..f1832273b76 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestOutputLogger.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestOutputLogger.cs @@ -25,13 +25,25 @@ public ILogger CreateLogger(string categoryName) return _loggers.GetOrAdd(categoryName, (name, helper) => new TestOutputLogger(helper, name), _outputHelper); } + public ILogger CreateLogger() + { + return (ILogger)_loggers.GetOrAdd(nameof(T), (name, helper) => new TestOutputLogger(helper), _outputHelper); + } + public void Dispose() { _loggers.Clear(); } } - internal sealed class TestOutputLogger : ILogger + internal sealed class TestOutputLogger : TestOutputLogger, ILogger + { + public TestOutputLogger(ITestOutputHelper outputHelper) : base(outputHelper, nameof(T)) + { + } + } + + internal class TestOutputLogger : ILogger { private readonly string _categoryName; private readonly ITestOutputHelper _outputHelper; diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestTimeouts.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestTimeouts.cs new file mode 100644 index 00000000000..47ad5a47e28 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.Tool.UnitTests/TestTimeouts.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.Monitoring.Tool.UnitTests +{ + internal static class TestTimeouts + { + /// + /// Timeout for an egress unit test (Must be const int to be used as an attribute). + /// + public const int EgressUnitTestTimeoutMs = 30 * 1000; // 30 seconds + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/LoggingExtensions.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/LoggingExtensions.cs index 0582bb72617..736c01c83c2 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/LoggingExtensions.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/LoggingExtensions.cs @@ -10,8 +10,8 @@ namespace Microsoft.Diagnostics.Monitoring.UnitTestApp { internal static class LoggingExtensions { - private static readonly Action _scenarioState = - LoggerMessage.Define( + private static readonly Action _scenarioState = + LoggerMessage.Define( eventId: TestAppLogEventIds.ScenarioState.EventId(), logLevel: LogLevel.Information, formatString: "State: {state}"); @@ -28,7 +28,7 @@ internal static class LoggingExtensions logLevel: LogLevel.Information, formatString: "Environment Variable: {name} = {value}"); - public static void ScenarioState(this ILogger logger, TestAppScenarios.SenarioState state) + public static void ScenarioState(this ILogger logger, TestAppScenarios.ScenarioState state) { _scenarioState(logger, state, null); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs index fbba6e71a3c..5c2ed74efc8 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Program.cs @@ -4,7 +4,6 @@ using Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios; using System.CommandLine; -using System.CommandLine.Builder; using System.CommandLine.Parsing; using System.Threading.Tasks; @@ -19,7 +18,9 @@ public static Task Main(string[] args) AsyncWaitScenario.Command(), LoggerScenario.Command(), SpinWaitScenario.Command(), - EnvironmentVariablesScenario.Command() + EnvironmentVariablesScenario.Command(), + StacksScenario.Command(), + TraceEventsScenario.Command() }) .UseDefaults() .Build() diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/ScenarioHelpers.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/ScenarioHelpers.cs index 4d8f8914426..846e17d9a2c 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/ScenarioHelpers.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/ScenarioHelpers.cs @@ -4,10 +4,13 @@ using Microsoft.Diagnostics.Monitoring.TestCommon; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using System; -using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -22,23 +25,27 @@ public static async Task RunScenarioAsync(Func> func, Ca using ServiceProvider hostServices = new ServiceCollection() .AddLogging(builder => { - builder.AddFilter(typeof(Program).FullName, LogLevel.Debug) - .AddJsonConsole(options => - { - options.UseUtcTimestamp = true; - }); + builder.AddFilter(typeof(Program).FullName, LogLevel.Debug); + // Console logger infra is not writing out all lines during process lifetime + // which causes the coordination between the unit test and the test app to fail. + // Temporarily replace with custom JSON console logger. + //builder.AddJsonConsole(options => + //{ + // options.UseUtcTimestamp = true; + //}); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); }).BuildServiceProvider(); // All test host communication should be sent through this logger. ILogger logger = hostServices.GetRequiredService() .CreateLogger(); - logger.ScenarioState(TestAppScenarios.SenarioState.Ready); + logger.ScenarioState(TestAppScenarios.ScenarioState.Ready); // Wait for test host before executing scenario await WaitForCommandAsync(TestAppScenarios.Commands.StartScenario, logger); - logger.ScenarioState(TestAppScenarios.SenarioState.Executing); + logger.ScenarioState(TestAppScenarios.ScenarioState.Executing); int result = -1; try @@ -50,7 +57,7 @@ public static async Task RunScenarioAsync(Func> func, Ca Console.Error.WriteLine($"Exception: {ex}"); } - logger.ScenarioState(TestAppScenarios.SenarioState.Finished); + logger.ScenarioState(TestAppScenarios.ScenarioState.Finished); // Wait for test host before ending scenario await WaitForCommandAsync(TestAppScenarios.Commands.EndScenario, logger); @@ -65,7 +72,7 @@ public static async Task WaitForCommandAsync(string expectedCommand, ILogger log public static async Task WaitForCommandAsync(string[] expectedCommands, ILogger logger) { - logger.ScenarioState(TestAppScenarios.SenarioState.Waiting); + logger.ScenarioState(TestAppScenarios.ScenarioState.Waiting); bool receivedExpected = false; string line, commandReceived = null; @@ -93,5 +100,87 @@ public static async Task WaitForCommandAsync(string[] expectedCommands, return commandReceived; } + + private class JsonConsoleLoggerProvider : ILoggerProvider + { + private readonly ConcurrentDictionary _loggers = new(); + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, name => new JsonConsoleLogger(name)); + } + + public void Dispose() + { + } + + private class JsonConsoleLogger : ILogger + { + private string _categoryName; + + public JsonConsoleLogger(string categoryName) + { + _categoryName = categoryName; + } + + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + using Utf8JsonWriter writer = new(Console.OpenStandardOutput()); + + writer.WriteStartObject(); + + writer.WriteNumber("EventId", eventId.Id); + writer.WriteString("Level", logLevel.ToString("G")); + writer.WriteString("Category", _categoryName); + writer.WriteString("Message", formatter(state, exception)); + + if (null != state) + { + writer.WriteStartObject("State"); + writer.WriteString("Message", state.ToString()); + + if ((object)state is IReadOnlyCollection> readOnlyCollection) + { + foreach (KeyValuePair item in readOnlyCollection) + { + if (item.Value is string stringValue) + { + writer.WriteString(item.Key, stringValue); + } + else if (item.Value is Enum enumValue) + { + writer.WriteString(item.Key, enumValue.ToString("G")); + } + else if (item.Value is bool booleanValue) + { + writer.WriteBoolean(item.Key, booleanValue); + } + else + { + writer.WriteString(item.Key, "[UNHANDLED]"); + } + } + } + writer.WriteEndObject(); + } + + writer.WriteEndObject(); + + writer.Flush(); + + Console.WriteLine(); + } + } + } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/AsyncWaitScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/AsyncWaitScenario.cs index 40fce64ff38..85c497307ff 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/AsyncWaitScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/AsyncWaitScenario.cs @@ -3,10 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.TestCommon; -using System; using System.CommandLine; using System.CommandLine.Invocation; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios @@ -19,18 +17,18 @@ internal class AsyncWaitScenario public static Command Command() { Command command = new(TestAppScenarios.AsyncWait.Name); - command.SetHandler((Func>)ExecuteAsync); + command.SetHandler(ExecuteAsync); return command; } - public static Task ExecuteAsync(CancellationToken token) + public static async Task ExecuteAsync(InvocationContext context) { - return ScenarioHelpers.RunScenarioAsync(async logger => + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { await ScenarioHelpers.WaitForCommandAsync(TestAppScenarios.AsyncWait.Commands.Continue, logger); return 0; - }, token); + }, context.GetCancellationToken()); } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/EnvironmentVariablesScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/EnvironmentVariablesScenario.cs index ce9e0c36d8b..986133b6d47 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/EnvironmentVariablesScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/EnvironmentVariablesScenario.cs @@ -4,10 +4,8 @@ using Microsoft.Diagnostics.Monitoring.TestCommon; using System; -using System.Collections; using System.CommandLine; using System.CommandLine.Invocation; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios @@ -20,18 +18,18 @@ internal class EnvironmentVariablesScenario public static Command Command() { Command command = new(TestAppScenarios.EnvironmentVariables.Name); - command.SetHandler((Func>)ExecuteAsync); + command.SetHandler(ExecuteAsync); return command; } - public static Task ExecuteAsync(CancellationToken token) + public static async Task ExecuteAsync(InvocationContext context) { string[] acceptableCommands = new string[] { TestAppScenarios.EnvironmentVariables.Commands.IncVar, TestAppScenarios.EnvironmentVariables.Commands.ShutdownScenario, }; - return ScenarioHelpers.RunScenarioAsync(async logger => + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { while (true) { @@ -52,7 +50,7 @@ public static Task ExecuteAsync(CancellationToken token) return 0; } } - }, token); + }, context.GetCancellationToken()); } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/LoggerScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/LoggerScenario.cs index b8f7e7a79a2..f0ff2972dc9 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/LoggerScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/LoggerScenario.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; -using System.Threading; using System.Threading.Tasks; namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios @@ -20,13 +19,13 @@ internal class LoggerScenario public static Command Command() { Command command = new(TestAppScenarios.Logger.Name); - command.SetHandler((Func>)ExecuteAsync); + command.SetHandler(ExecuteAsync); return command; } - public static Task ExecuteAsync(CancellationToken token) + public static async Task ExecuteAsync(InvocationContext context) { - return ScenarioHelpers.RunScenarioAsync(async logger => + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { using ServiceProvider services = new ServiceCollection() .AddLogging(builder => @@ -67,7 +66,7 @@ public static Task ExecuteAsync(CancellationToken token) LogCriticalMessage(cat3Logger); return 0; - }, token); + }, context.GetCancellationToken()); } private static void LogTraceMessage(ILogger logger) @@ -128,7 +127,7 @@ public CustomLogState(string message, string[] keys, object[] values) _message = message; _keys = keys ?? throw new ArgumentNullException(nameof(keys)); _values = values ?? throw new ArgumentNullException(nameof(values)); - + if (_keys.Length != _values.Length) { throw new ArgumentException($"{nameof(keys)} and {nameof(values)} must have the same length."); diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/SpinWaitScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/SpinWaitScenario.cs index bc754ac4ce6..acdc71fa7b4 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/SpinWaitScenario.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/SpinWaitScenario.cs @@ -3,10 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.Monitoring.TestCommon; -using System; using System.CommandLine; using System.CommandLine.Invocation; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -20,13 +18,13 @@ internal class SpinWaitScenario public static Command Command() { Command command = new(TestAppScenarios.SpinWait.Name); - command.SetHandler((Func>)ExecuteAsync); + command.SetHandler(ExecuteAsync); return command; } - public static Task ExecuteAsync(CancellationToken token) + public static async Task ExecuteAsync(InvocationContext context) { - return ScenarioHelpers.RunScenarioAsync(async logger => + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => { await ScenarioHelpers.WaitForCommandAsync(TestAppScenarios.SpinWait.Commands.StartSpin, logger); @@ -38,7 +36,7 @@ public static Task ExecuteAsync(CancellationToken token) } return 0; - }, token); + }, context.GetCancellationToken()); } } } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksScenario.cs new file mode 100644 index 00000000000..2170dc627b5 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksScenario.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios +{ + internal class StacksScenario + { + [DllImport(ProfilerIdentifiers.LibraryRootFileName, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] + public static extern int TestHook([MarshalAs(UnmanagedType.FunctionPtr)] Action callback); + + static StacksScenario() + { + NativeLibrary.SetDllImportResolver(typeof(StacksScenario).Assembly, ResolveDllImport); + } + + public static IntPtr ResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + //DllImport for Windows automatically loads in-memory modules (such as the profiler). This is not the case for Linux/MacOS. + //If we fail resolving the DllImport, we have to load the profiler ourselves. + + string profilerName = ProfilerHelper.GetPath(RuntimeInformation.ProcessArchitecture); + if (NativeLibrary.TryLoad(profilerName, out IntPtr handle)) + { + return handle; + } + + return IntPtr.Zero; + } + + public static Command Command() + { + Command command = new(TestAppScenarios.Stacks.Name); + + command.SetHandler(ExecuteAsync); + return command; + } + + public static async Task ExecuteAsync(InvocationContext context) + { + using StacksWorker worker = new StacksWorker(); + + //Background thread will create an expected callstack and pause. + Thread thread = new Thread(Entrypoint); + thread.Start(worker); + + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => + { + await ScenarioHelpers.WaitForCommandAsync(TestAppScenarios.Stacks.Commands.Continue, logger); + + //Allow the background thread to resume work. + worker.Signal(); + + return 0; + }, context.GetCancellationToken()); + } + + public static void Entrypoint(object worker) + { + var stacksWorker = (StacksWorker)worker; + stacksWorker.Work(); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs new file mode 100644 index 00000000000..b2059fca71c --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/StacksWorker.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Tracing; +using System.Threading; + +namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios +{ + internal class StacksWorker : IDisposable + { + private EventWaitHandle _eventWaitHandle = new ManualResetEvent(false); + + public class StacksWorkerNested + { + private WaitHandle _handle; + + public void DoWork(U test, WaitHandle handle) + { + _handle = handle; + StacksScenario.TestHook(Callback); + } + + public void Callback() + { + using EventSource eventSource = new EventSource("StackScenario"); + using EventCounter eventCounter = new EventCounter("Ready", eventSource); + eventCounter.WriteMetric(1.0); + _handle.WaitOne(); + } + } + + public void Work() + { + StacksWorkerNested nested = new StacksWorkerNested(); + + nested.DoWork(5, _eventWaitHandle); + } + + public void Signal() + { + _eventWaitHandle.Set(); + } + + public void Dispose() + { + _eventWaitHandle.Dispose(); + + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs new file mode 100644 index 00000000000..48d249c863a --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.UnitTestApp/Scenarios/TraceEventsScenario.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Monitoring.TestCommon; +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Diagnostics.Tracing; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostics.Monitoring.UnitTestApp.Scenarios +{ + /// + /// Continously emits trace events and a unique one on request. + /// Only stops once an exit request is received. + /// + internal class TraceEventsScenario + { + [EventSource(Name = "TestScenario")] + class TestScenarioEventSource : EventSource + { + public static TestScenarioEventSource Log { get; } = new TestScenarioEventSource(); + + [Event(1)] + public void RandomNumberGenerated(int number) => WriteEvent(1, number); + + [Event(2, Opcode = EventOpcode.Reply)] + public void UniqueEvent(string message) => WriteEvent(2, message); + } + + public static Command Command() + { + Command command = new(TestAppScenarios.TraceEvents.Name); + command.SetHandler(ExecuteAsync); + return command; + } + + public static async Task ExecuteAsync(InvocationContext context) + { + string[] acceptableCommands = new string[] + { + TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent, + TestAppScenarios.TraceEvents.Commands.ShutdownScenario + }; + + context.ExitCode = await ScenarioHelpers.RunScenarioAsync(async logger => + { + using ManualResetEventSlim stopGeneratingEvents = new(initialState: false); + + Task eventEmitterTask = Task.Run(async () => + { + Random random = new(); + while (!stopGeneratingEvents.IsSet) + { + TestScenarioEventSource.Log.RandomNumberGenerated(random.Next()); + await Task.Delay(TimeSpan.FromMilliseconds(100), context.GetCancellationToken()); + } + }, context.GetCancellationToken()); + + while (true) + { + switch (await ScenarioHelpers.WaitForCommandAsync(acceptableCommands, logger)) + { + case TestAppScenarios.TraceEvents.Commands.EmitUniqueEvent: + TestScenarioEventSource.Log.UniqueEvent(TestAppScenarios.TraceEvents.UniqueEventMessage); + break; + case TestAppScenarios.TraceEvents.Commands.ShutdownScenario: + stopGeneratingEvents.Set(); + eventEmitterTask.Wait(context.GetCancellationToken()); + return 0; + } + } + }, context.GetCancellationToken()); + } + } +} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/DefaultProcessConfigurationTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/DefaultProcessConfigurationTests.cs index c7e31055716..2195bc585bd 100644 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/DefaultProcessConfigurationTests.cs +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/DefaultProcessConfigurationTests.cs @@ -2,17 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Diagnostics.Monitoring.TestCommon; -using Microsoft.Extensions.DependencyInjection; using System; using System.Globalization; using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Cryptography; -using System.Threading.Tasks; using Xunit; namespace Microsoft.Diagnostics.Monitoring.WebApi.UnitTests @@ -165,7 +157,7 @@ private static DiagProcessFilterEntry CreateFilterEntry(ProcessFilterDescriptor private static DiagProcessFilter CreateOptions(params ProcessFilterDescriptor[] filters) { var filterOptions = new ProcessFilterOptions(); - foreach(var processFilter in filters) + foreach (var processFilter in filters) { filterOptions.Filters.Add(processFilter); } diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/MetricsExportTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/MetricsExportTests.cs deleted file mode 100644 index f54d1c215ee..00000000000 --- a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/MetricsExportTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.Diagnostics.Monitoring.EventPipe; -using Microsoft.Diagnostics.Monitoring.WebApi; -using Microsoft.Diagnostics.Monitoring.TestCommon; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Security.Cryptography; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.Diagnostics.Monitoring.WebApi.UnitTests -{ - public class MetricsExportTests - { - [Fact] - public void TestPrometheusNormalization() - { - string metric = Normalize("Test-Provider", "Test-Metric"); - Assert.Equal("testprovider_Test_Metric_bytes", metric); - - metric = Normalize("!@#", "#@#"); - //__ - //provider becomes '_' - //metric becomes '___' - //unit defaults to bytes - Assert.Equal("______bytes", metric); - - metric = Normalize("Asp-Net-Provider", "Requests!Received", "0$customs"); - Assert.Equal("aspnetprovider_Requests_Received___customs", metric); - - metric = Normalize("a", "b", unit: null); - Assert.Equal("a_b", metric); - - metric = Normalize("UnicodeάήΰLetter", "Unicode\u0befDigit", unit: null); - Assert.Equal("unicodeletter_Unicode_Digit", metric); - } - - private static string Normalize(string provider, string name, string unit = "b") - { - return PrometheusDataModel.Normalize(provider, name, unit, 0.0, out _); - } - } -} diff --git a/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/PrometheusDataModelTests.cs b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/PrometheusDataModelTests.cs new file mode 100644 index 00000000000..ffc10382833 --- /dev/null +++ b/src/Tests/Microsoft.Diagnostics.Monitoring.WebApi.UnitTests/PrometheusDataModelTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; + +namespace Microsoft.Diagnostics.Monitoring.WebApi.UnitTests +{ + public class PrometheusDataModelTests + { + [Theory()] + [InlineData("System.Runtime", "cpu-usage", "%", "systemruntime_cpu_usage_ratio")] + [InlineData("System.Runtime", "working-set", "MB", "systemruntime_working_set_bytes")] + [InlineData("System.Runtime", "gc-heap-size", "MB", "systemruntime_gc_heap_size_bytes")] + [InlineData("System.Runtime", "gen-0-gc-count", "count", "systemruntime_gen_0_gc_count")] + [InlineData("System.Runtime", "gen-1-gc-count", "count", "systemruntime_gen_1_gc_count")] + [InlineData("System.Runtime", "gen-2-gc-count", "count", "systemruntime_gen_2_gc_count")] + [InlineData("System.Runtime", "threadpool-thread-count", "", "systemruntime_threadpool_thread_count")] + [InlineData("System.Runtime", "monitor-lock-contention-count", "count", "systemruntime_monitor_lock_contention_count")] + [InlineData("System.Runtime", "threadpool-queue-length", "", "systemruntime_threadpool_queue_length")] + [InlineData("System.Runtime", "threadpool-completed-items-count", "count", "systemruntime_threadpool_completed_items_count")] + [InlineData("System.Runtime", "alloc-rate", "B", "systemruntime_alloc_rate_bytes")] + [InlineData("System.Runtime", "active-timer-count", "", "systemruntime_active_timer_count")] + [InlineData("System.Runtime", "gc-fragmentation", "%", "systemruntime_gc_fragmentation_ratio")] + [InlineData("System.Runtime", "gc-committed", "MB", "systemruntime_gc_committed_bytes")] + [InlineData("System.Runtime", "exception-count", "count", "systemruntime_exception_count")] + [InlineData("System.Runtime", "time-in-gc", "%", "systemruntime_time_in_gc_ratio")] + [InlineData("System.Runtime", "gen-0-size", "B", "systemruntime_gen_0_size_bytes")] + [InlineData("System.Runtime", "gen-1-size", "B", "systemruntime_gen_1_size_bytes")] + [InlineData("System.Runtime", "gen-2-size", "B", "systemruntime_gen_2_size_bytes")] + [InlineData("System.Runtime", "loh-size", "B", "systemruntime_loh_size_bytes")] + [InlineData("System.Runtime", "poh-size", "B", "systemruntime_poh_size_bytes")] + [InlineData("System.Runtime", "assembly-count", "", "systemruntime_assembly_count")] + [InlineData("System.Runtime", "il-bytes-jitted", "B", "systemruntime_il_bytes_jitted_bytes")] + [InlineData("System.Runtime", "methods-jitted-count", "", "systemruntime_methods_jitted_count")] + [InlineData("System.Runtime", "time-in-jit", "ms", "systemruntime_time_in_jit_ms")] + [InlineData("Microsoft.AspNetCore.Hosting", "requests-per-second", "count", "microsoftaspnetcorehosting_requests_per_second")] + [InlineData("Microsoft.AspNetCore.Hosting", "total-requests", "", "microsoftaspnetcorehosting_total_requests")] + [InlineData("Microsoft.AspNetCore.Hosting", "current-requests", "", "microsoftaspnetcorehosting_current_requests")] + [InlineData("Microsoft.AspNetCore.Hosting", "failed-requests", "", "microsoftaspnetcorehosting_failed_requests")] + [InlineData("Test-Provider", "Test-Metric", "b", "testprovider_Test_Metric_bytes")] + [InlineData("!@#", "#@#", "b", "______bytes")] + [InlineData("Asp-Net-Provider", "Requests!Received", "0$customs", "aspnetprovider_Requests_Received___customs")] + [InlineData("a", "b", null, "a_b")] + [InlineData("UnicodeάήΰLetter", "Unicode\u0befDigit", null, "unicodeletter_Unicode_Digit")] + public void TestGetPrometheusNormalizedName(string metricProvider, string metricName, string metricUnit, string expectedName) + { + var normalizedMetricName = PrometheusDataModel.GetPrometheusNormalizedName(metricProvider, metricName, metricUnit); + Assert.Equal(expectedName, normalizedMetricName); + } + + [Theory()] + [InlineData("", 225, "225")] + [InlineData("MB", 9, "9000000")] + [InlineData("mb", 112, "112000000")] + [InlineData("B", 48, "48")] + [InlineData("B", 178936, "178936")] + [InlineData("b", 461142, "461142")] + [InlineData("ms", 0, "0")] + [InlineData("", 4, "4")] + [InlineData("count", 10, "10")] + [InlineData("%", 2, "2")] + [InlineData("%", 0.691783039570667, "0.691783039570667")] + [InlineData("%", 0, "0")] + public void TestGetPrometheusNormalizedValue(string metricUnit, double metricValue, string expectedValue) + { + var normalizedValue = PrometheusDataModel.GetPrometheusNormalizedValue(metricUnit, metricValue); + Assert.Equal(normalizedValue, expectedValue); + } + } +} diff --git a/src/Tools/Common/CommandExtensions.cs b/src/Tools/Common/CommandExtensions.cs deleted file mode 100644 index c5989bcb83f..00000000000 --- a/src/Tools/Common/CommandExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.CommandLine; -using System.CommandLine.Invocation; - -namespace Microsoft.Tools.Common -{ - public static class CommandExtenions - { - /// - /// Allows the command handler to be included in the collection initializer. - /// - public static void Add(this Command command, ICommandHandler handler) - { - command.Handler = handler; - } - - public static void Add(this Command command, IEnumerable