Skip to content

Conversation

@gregkalapos
Copy link
Contributor

@gregkalapos gregkalapos commented May 12, 2020

Solves #677

Language specific stack traces elastic/kibana#49467 was added in 7.10 in Kibana.

This PR utilizes that feature.

Prior to this PR

Pre 7.10, Kibana always showed stack traces in the following format: [Filename] in [MethodName] at line [LineNo].

For .NET this was very problematic, because that would have meant we never show stack traces in callstacks, furthermore, sometimes only the assembly name would be the only filename we could get, so lots of frames would be something like System.Private.CoreLib.dll in SomeMethod().

As a workaround we (...well I) had the idea to use the FileName to store the real class name. This sounds very dirty (and it is), but I strongly think it's still better to show proper call stacks and include the ClassName always.

Solution

As stated in #677 APM Server introduced a new field to store class names and in 7.10 Kibana uses language specific call stacks where for .NET it also shows the class name. Details: elastic/kibana#49467

What we do now

If the agent connects to APM Server version 7.10 or newer, we send both FileName and ClassName and Kibana will show a stacktrace with format: [ClassName] in [MethodName] in [FileName] at line [LineNo]. 🎉

But we need to maintain backwards compatibility, so for pre 7.10 versions we still use the hack described above. Once 7.9 is out of support we can remove this code.

So this PR does basically 2 things:

  • Introduces a way to query the APM Server version.
  • Based on the the server version it either properly fills fields on stack traces (7.10 or newer), or it uses the hack described above (pre-7.10).

@gregkalapos gregkalapos self-assigned this May 12, 2020
@ghost
Copy link

ghost commented May 13, 2020

❕ Build Aborted

Either there was a build timeout or someone aborted the build.'}

the below badges are clickable and redirect to their specific view in the CI or DOCS
Pipeline View Test View Changes Artifacts

Expand to view the summary

Build stats

  • Build Cause: [Branch indexing]

  • Start Time: 2020-11-12T02:20:19.762+0000

  • Duration: 80 min 9 sec

Test stats 🧪

Test Results
Failed 0
Passed 1466
Skipped 0
Total 1466

Log output

Expand to view the last 100 lines of log output

[2020-11-12T03:40:14.377Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/Config/ConfigSnapshotFromReader.cs. 
[2020-11-12T03:40:14.435Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/Config/ConfigSnapshotFromReader.cs. 
[2020-11-12T03:40:14.435Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs. 
[2020-11-12T03:40:14.496Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs. 
[2020-11-12T03:40:14.496Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs. 
[2020-11-12T03:40:14.556Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/Config/EnvironmentConfigurationReader.cs. 
[2020-11-12T03:40:14.556Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/HelpersTests/IntExtensionsTests.cs. 
[2020-11-12T03:40:14.613Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/HelpersTests/IntExtensionsTests.cs. 
[2020-11-12T03:40:14.613Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigReader.cs. 
[2020-11-12T03:40:14.670Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/BackendComm/CentralConfig/CentralConfigReader.cs. 
[2020-11-12T03:40:14.670Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/Api/Container.cs. 
[2020-11-12T03:40:14.726Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm/Api/Container.cs. 
[2020-11-12T03:40:14.726Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/ApiTests/ConvenientApiSpanTests.cs. 
[2020-11-12T03:40:14.796Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/ApiTests/ConvenientApiSpanTests.cs. 
[2020-11-12T03:40:14.796Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/InstrumentationFlagTests.cs. 
[2020-11-12T03:40:14.864Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/InstrumentationFlagTests.cs. 
[2020-11-12T03:40:14.864Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/HelpersTests/TimeUtilsTests.cs. 
[2020-11-12T03:40:14.923Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/HelpersTests/TimeUtilsTests.cs. 
[2020-11-12T03:40:14.923Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm.Extensions.Hosting/Config/MicrosoftExtensionsConfig.cs. 
[2020-11-12T03:40:14.981Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm.Extensions.Hosting/Config/MicrosoftExtensionsConfig.cs. 
[2020-11-12T03:40:14.981Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/HelpersTests/PropertyFetcherTests.cs. 
[2020-11-12T03:40:15.038Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/HelpersTests/PropertyFetcherTests.cs. 
[2020-11-12T03:40:15.039Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/TestHelpers/FlushingTextWriterToLoggerAdaptor.cs. 
[2020-11-12T03:40:15.099Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/TestHelpers/FlushingTextWriterToLoggerAdaptor.cs. 
[2020-11-12T03:40:15.099Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/TestHelpers/IntExtensions.cs. 
[2020-11-12T03:40:15.155Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/test/Elastic.Apm.Tests/TestHelpers/IntExtensions.cs. 
[2020-11-12T03:40:15.156Z] Starting copy source file /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm.AspNetCore/Consts.cs. 
[2020-11-12T03:40:15.212Z] Copied /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet/src/Elastic.Apm.AspNetCore/Consts.cs. 
[2020-11-12T03:40:21.718Z] [INFO] getVaultSecret: Getting secrets
[2020-11-12T03:40:21.848Z] Masking supported pattern matches of $VAULT_ADDR or $VAULT_ROLE_ID or $VAULT_SECRET_ID
[2020-11-12T03:40:22.206Z] Running in /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848/apm-agent-dotnet
[2020-11-12T03:40:22.223Z] [INFO] Codecov: Getting branch ref...
[2020-11-12T03:40:22.329Z] Masking supported pattern matches of $GITHUB_TOKEN
[2020-11-12T03:40:22.831Z] [INFO] Codecov: Sending data...
[2020-11-12T03:40:23.216Z] + curl -sSLo codecov.sh https://codecov.io/bash
[2020-11-12T03:40:23.537Z] + bash codecov.sh
[2020-11-12T03:40:23.537Z] 
[2020-11-12T03:40:23.537Z]   _____          _
[2020-11-12T03:40:23.537Z]  / ____|        | |
[2020-11-12T03:40:23.537Z] | |     ___   __| | ___  ___ _____   __
[2020-11-12T03:40:23.537Z] | |    / _ \ / _` |/ _ \/ __/ _ \ \ / /
[2020-11-12T03:40:23.537Z] | |___| (_) | (_| |  __/ (_| (_) \ V /
[2020-11-12T03:40:23.537Z]  \_____\___/ \__,_|\___|\___\___/ \_/
[2020-11-12T03:40:23.537Z]                               Bash-20201106-81372f2
[2020-11-12T03:40:23.537Z] 
[2020-11-12T03:40:23.537Z] 
[2020-11-12T03:40:23.537Z] ==> Jenkins CI detected.
[2020-11-12T03:40:23.537Z]     project root: .
[2020-11-12T03:40:23.537Z] --> token set from env
[2020-11-12T03:40:23.537Z]     Yaml found at: codecov.yml
[2020-11-12T03:40:23.537Z] ==> Running gcov in . (disable via -X gcov)
[2020-11-12T03:40:23.796Z] ==> Python coveragepy not found
[2020-11-12T03:40:23.796Z] ==> Searching for coverage reports in:
[2020-11-12T03:40:23.796Z]     + .
[2020-11-12T03:40:24.364Z]     -> Found 12 reports
[2020-11-12T03:40:24.364Z] ==> Detecting git/mercurial file structure
[2020-11-12T03:40:24.364Z] ==> Reading reports
[2020-11-12T03:40:24.364Z]     + ./.nuget/packages/microsoft.codecoverage/16.5.0/microsoft.codecoverage.16.5.0.nupkg.sha512 bytes=88
[2020-11-12T03:40:24.364Z]     + ./.nuget/packages/microsoft.codecoverage/16.5.0/microsoft.codecoverage.nuspec bytes=972
[2020-11-12T03:40:24.364Z]     + ./.nuget/packages/microsoft.codecoverage/16.5.0/microsoft.codecoverage.16.5.0.nupkg bytes=3098154
[2020-11-12T03:40:24.365Z]     + ./.nuget/packages/microsoft.codecoverage/16.5.0/build/netstandard1.0/CodeCoverage/codecoveragemessages.dll bytes=42784
[2020-11-12T03:40:24.365Z]     + ./target/b21724a1-0ee1-4449-bd36-fe6496572e9e/Elastic.Apm.EntityFrameworkCore.Tests.csproj-coverage.cobertura.xml bytes=3307567
[2020-11-12T03:40:24.365Z]     + ./target/3df34f94-14ea-494e-8f70-9a1e05e62210/Elastic.Apm.EntityFrameworkCore.Tests.csproj-coverage.cobertura.xml bytes=3313748
[2020-11-12T03:40:24.365Z]     + ./target/d7114e39-3e27-448a-b642-052836491fc6/Elastic.Apm.EntityFrameworkCore.Tests.csproj-coverage.cobertura.xml bytes=3313748
[2020-11-12T03:40:24.365Z]     + ./target/c667ee59-fb55-409a-b526-0730a689975b/Elastic.Apm.Feature.Tests.csproj-coverage.cobertura.xml bytes=3244467
[2020-11-12T03:40:24.365Z]     + ./target/ee611b15-1768-4c08-8c4c-3eab448af079/Elastic.Apm.AspNetCore.Tests.csproj-coverage.cobertura.xml bytes=3780450
[2020-11-12T03:40:24.366Z]     + ./target/00a57444-766b-41c9-a628-4aac1a1fa875/Elastic.Apm.EntityFrameworkCore.Tests.csproj-coverage.cobertura.xml bytes=3306316
[2020-11-12T03:40:24.366Z]     + ./.local/share/NuGet/v3-cache/1ca707a4d90792ce8e42453d4e350886a0fdaa4d$ps:_api.nuget.org_v3_index.json/list_microsoft.codecoverage.dat bytes=1421
[2020-11-12T03:40:24.366Z]     + ./.local/share/NuGet/v3-cache/1ca707a4d90792ce8e42453d4e350886a0fdaa4d$ps:_api.nuget.org_v3_index.json/nupkg_microsoft.codecoverage.16.5.0.dat bytes=3098154
[2020-11-12T03:40:24.366Z] ==> Appending adjustments
[2020-11-12T03:40:24.366Z]     https://docs.codecov.io/docs/fixing-reports
[2020-11-12T03:40:24.934Z]     + Found adjustments
[2020-11-12T03:40:24.935Z] ==> Gzipping contents
[2020-11-12T03:40:25.872Z] ==> Uploading reports
[2020-11-12T03:40:25.872Z]     url: https://codecov.io
[2020-11-12T03:40:25.872Z]     query: branch=gregkalapos%2FClassNameOnStackTrace&commit=cd0ce52a7bd024173fba0569f54ec2901fb406d3&build=17&build_url=https%3A%2F%2Fapm-ci.elastic.co%2Fjob%2Fapm-agent-dotnet%2Fjob%2Fapm-agent-dotnet-mbp%2Fjob%2FPR-848%2F17%2F&name=&tag=&slug=elastic%2Fapm-agent-dotnet&service=jenkins&flags=&pr=848&job=&cmd_args=
[2020-11-12T03:40:25.872Z] ->  Pinging Codecov
[2020-11-12T03:40:25.872Z] https://codecov.io/upload/v4?package=bash-20201106-81372f2&token=secret&branch=gregkalapos%2FClassNameOnStackTrace&commit=cd0ce52a7bd024173fba0569f54ec2901fb406d3&build=17&build_url=https%3A%2F%2Fapm-ci.elastic.co%2Fjob%2Fapm-agent-dotnet%2Fjob%2Fapm-agent-dotnet-mbp%2Fjob%2FPR-848%2F17%2F&name=&tag=&slug=elastic%2Fapm-agent-dotnet&service=jenkins&flags=&pr=848&job=&cmd_args=
[2020-11-12T03:40:26.440Z] ->  Uploading to
[2020-11-12T03:40:26.440Z] https://storage.googleapis.com/codecov/v4/raw/2020-11-12/DB094A53A8E3E06DBBF05BB7869242D5/cd0ce52a7bd024173fba0569f54ec2901fb406d3/4b619474-3068-47a1-9379-d2aa0235350c.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=GOOG1EQX6OZVJGHKK3633AAFGLBUCOOATRACRQRQF6HMSMLYUP6EAD6XSWAAY%2F20201112%2FUS%2Fs3%2Faws4_request&X-Amz-Date=20201112T034026Z&X-Amz-Expires=10&X-Amz-SignedHeaders=host&X-Amz-Signature=770c8479f6309ebfa2329baf7daacfe5295d9929c347af13fc12f3a64c00d5d8
[2020-11-12T03:40:26.440Z]   % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
[2020-11-12T03:40:26.440Z]                                  Dload  Upload   Total   Spent    Left  Speed
[2020-11-12T03:40:27.008Z] 
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 6926k    0     0  100 6926k      0  12.0M --:--:-- --:--:-- --:--:-- 12.0M
100 6926k    0     0  100 6926k      0  12.0M --:--:-- --:--:-- --:--:-- 12.0M
[2020-11-12T03:40:27.008Z]     -> View reports at https://codecov.io/github/elastic/apm-agent-dotnet/commit/cd0ce52a7bd024173fba0569f54ec2901fb406d3
[2020-11-12T03:40:27.100Z] Archiving artifacts
[2020-11-12T03:40:27.185Z] Failed in branch Linux
[2020-11-12T03:40:27.308Z] Stage "Release to feedz.io" skipped due to earlier failure(s)
[2020-11-12T03:40:27.356Z] Stage "Release" skipped due to earlier failure(s)
[2020-11-12T03:40:27.371Z] Stage "Release" skipped due to earlier failure(s)
[2020-11-12T03:40:27.412Z] Stage "Release" skipped due to earlier failure(s)
[2020-11-12T03:40:27.473Z] Stage "AfterRelease" skipped due to earlier failure(s)
[2020-11-12T03:40:27.490Z] Stage "AfterRelease" skipped due to earlier failure(s)
[2020-11-12T03:40:27.711Z] Running on Jenkins in /var/lib/jenkins/workspace/tnet_apm-agent-dotnet-mbp_PR-848
[2020-11-12T03:40:27.755Z] [INFO] getVaultSecret: Getting secrets
[2020-11-12T03:40:27.880Z] Masking supported pattern matches of $VAULT_ADDR or $VAULT_ROLE_ID or $VAULT_SECRET_ID
[2020-11-12T03:40:28.582Z] + chmod 755 generate-build-data.sh
[2020-11-12T03:40:28.582Z] + ./generate-build-data.sh https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-848/ https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-848/runs/17 ABORTED 4808560
[2020-11-12T03:40:29.133Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-848/runs/17/steps/?limit=10000 -o steps-info.json
[2020-11-12T03:40:29.383Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-848/runs/17/tests/?status=FAILED -o tests-errors.json
[2020-11-12T03:40:29.634Z] INFO: curl https://apm-ci.elastic.co/blue/rest/organizations/jenkins/pipelines/apm-agent-dotnet/apm-agent-dotnet-mbp/PR-848/runs/17/log/ -o pipeline-log.txt

@gregkalapos
Copy link
Contributor Author

We discussed this and we'll go with the 3. option:

we query the APM Server version and based on that for pre 7.9 we put the classname into filename (as we do now) and for 7.9+ we put the filename into filename and the classname into classname.

@smith
Copy link

smith commented Aug 25, 2020

We are planning on implementing elastic/kibana#49467 for 7.10, where we will keep the existing output if we're missing a classname, and use the classname if we have it.

@codecov-io
Copy link

codecov-io commented Nov 10, 2020

Codecov Report

Merging #848 (ad7db94) into master (b609a01) will increase coverage by 3.61%.
The diff coverage is 71.42%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #848      +/-   ##
==========================================
+ Coverage   76.24%   79.85%   +3.61%     
==========================================
  Files         144      146       +2     
  Lines        6866     7085     +219     
==========================================
+ Hits         5235     5658     +423     
+ Misses       1631     1427     -204     
Impacted Files Coverage Δ
...lasticsearch/ElasticsearchDiagnosticsSubscriber.cs 0.00% <0.00%> (ø)
src/Elastic.Apm.SqlClient/SqlEventListener.cs 0.00% <0.00%> (ø)
.../BackendComm/CentralConfig/CentralConfigFetcher.cs 55.97% <0.00%> (-0.72%) ⬇️
...dComm/CentralConfig/CentralConfigResponseParser.cs 96.34% <ø> (ø)
...steners/HttpDiagnosticListenerFullFrameworkImpl.cs 0.00% <0.00%> (ø)
...m/BackendComm/CentralConfig/CentralConfigReader.cs 73.17% <33.33%> (-3.15%) ⬇️
src/Elastic.Apm/Model/NoopTransaction.cs 38.09% <38.09%> (ø)
src/Elastic.Apm/ServerInfo/ServerInfo.cs 41.02% <41.02%> (ø)
src/Elastic.Apm/Model/NoopSpan.cs 48.27% <48.27%> (ø)
...c/Elastic.Apm.AspNetCore/ApmMiddlewareExtension.cs 48.14% <50.00%> (+25.92%) ⬆️
... and 55 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c6475ab...ad7db94. Read the comment docs.

Copy link
Contributor

@russcam russcam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left some comments. Introducing a type that encapsulates fetching and transforming stack traces may be useful

/// <summary>
/// Encapsulates information about the APM Server that receives data from the agent.
/// </summary>
internal interface IServerInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth naming this IApmServerInfo to indicate that it's information specific to APM server, as opposed to any other server e.g. server on which agent is running

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - implemented.

/// <see cref="System.Version.Minor" />
/// <see cref="System.Version.Build" />
/// </summary>
public Version Version { get; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this ever need to take into account prerelease versions? If so, we might want to use a semantic version representation that accommodates prerelease suffixes. for example

// A simple version implementation based on 
// https://github.com/maxhauser/semver/blob/master/src/Semver/SemVersion.cs
// MIT License
// Copyright (c) 2013 Max Hauser 
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.


using System;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Apm.Agent
{
    
    /// <summary>
    /// An Elastic product version
    /// </summary>
    public sealed class ElasticVersion : IEquatable<ElasticVersion>, IComparable<ElasticVersion>, IComparable
    {
        private static readonly Regex VersionRegex = new Regex(
            @"^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?(\-(?<pre>[0-9A-Za-z]+))?$", 
            RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
        
        public ElasticVersion(string version)
        {
            var match = VersionRegex.Match(version);
            if (!match.Success)
                throw new ArgumentException($"Invalid version '{version}'", nameof(version));

            var major = int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture);

            var minorMatch = match.Groups["minor"];
            var minor = minorMatch.Success
                ? int.Parse(minorMatch.Value, CultureInfo.InvariantCulture)
                : 0;

            var patchMatch = match.Groups["patch"];
            var patch = patchMatch.Success 
                ? int.Parse(patchMatch.Value, CultureInfo.InvariantCulture)
                : 0;

            var prerelease = match.Groups["pre"].Value;
            
            Major = major;
            Minor = minor;
            Patch = patch;
            Prerelease = prerelease;
        }

        public ElasticVersion(int major, int minor, int patch, string prerelease)
        {
            Major = major;
            Minor = minor;
            Patch = patch;
            Prerelease = prerelease;
        }

        public static bool TryParse(string version, out ElasticVersion elasticVersion)
        {
            try
            {
                elasticVersion = new ElasticVersion(version);
                return true;
            }
            catch (Exception)
            {
                elasticVersion = null;
                return false;
            }
        }

        public static bool Equals(ElasticVersion versionA, ElasticVersion versionB) => 
            ReferenceEquals(versionA, null) 
                ? ReferenceEquals(versionB, null) 
                : versionA.Equals(versionB);

        public static int Compare(ElasticVersion versionA, ElasticVersion versionB) =>
            ReferenceEquals(versionA, null)
                ? ReferenceEquals(versionB, null) ? 0 : -1
                : versionA.CompareTo(versionB);

        public ElasticVersion Change(int? major = null, int? minor = null, int? patch = null, string prerelease = null) =>
            new ElasticVersion(
                major ?? Major,
                minor ?? Minor,
                patch ?? Patch,
                prerelease ?? Prerelease);

        public int Major { get; }

        public int Minor { get; }

        public int Patch { get; }

        public string Prerelease { get; }

        public override string ToString()
        {
            var version = "" + Major + "." + Minor + "." + Patch;
            if (!string.IsNullOrEmpty(Prerelease))
                version += "-" + Prerelease;
            
            return version;
        }

        public int CompareTo(object obj) => CompareTo((ElasticVersion)obj);

        public int CompareTo(ElasticVersion other)
        {
            if (ReferenceEquals(other, null))
                return 1;

            var r = CompareByPrecedence(other);
            return r;
        }

        public bool PrecedenceMatches(ElasticVersion other) => CompareByPrecedence(other) == 0;

        public int CompareByPrecedence(ElasticVersion other)
        {
            if (ReferenceEquals(other, null))
                return 1;

            var r = Major.CompareTo(other.Major);
            if (r != 0) return r;

            r = Minor.CompareTo(other.Minor);
            if (r != 0) return r;

            r = Patch.CompareTo(other.Patch);
            if (r != 0) return r;

            r = CompareComponent(Prerelease, other.Prerelease, true);
            return r;
        }

        private static int CompareComponent(string a, string b, bool lower = false)
        {
            var aEmpty = string.IsNullOrEmpty(a);
            var bEmpty = string.IsNullOrEmpty(b);
            if (aEmpty && bEmpty)
                return 0;

            if (aEmpty)
                return lower ? 1 : -1;
            if (bEmpty)
                return lower ? -1 : 1;

            var aComps = a.Split('.');
            var bComps = b.Split('.');

            var minLen = Math.Min(aComps.Length, bComps.Length);
            for (int i = 0; i < minLen; i++)
            {
                var ac = aComps[i];
                var bc = bComps[i];
                int anum, bnum;
                var isanum = int.TryParse(ac, out anum);
                var isbnum = int.TryParse(bc, out bnum);
                int r;
                if (isanum && isbnum)
                {
                    r = anum.CompareTo(bnum);
                    if (r != 0) return anum.CompareTo(bnum);
                }
                else
                {
                    if (isanum)
                        return -1;
                    if (isbnum)
                        return 1;
                    r = string.CompareOrdinal(ac, bc);
                    if (r != 0)
                        return r;
                }
            }

            return aComps.Length.CompareTo(bComps.Length);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(obj, null))
                return false;

            if (ReferenceEquals(this, obj))
                return true;

            var other = (ElasticVersion)obj;

            return Equals(other);
        }

        public bool Equals(ElasticVersion other)
        {
            if (other == null)
                return false;

            return Major == other.Major &&
                Minor == other.Minor &&
                Patch == other.Patch &&
                string.Equals(Prerelease, other.Prerelease, StringComparison.Ordinal);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                int result = Major.GetHashCode();
                result = result * 31 + Minor.GetHashCode();
                result = result * 31 + Patch.GetHashCode();
                result = result * 31 + Prerelease.GetHashCode();
                return result;
            }
        }

        public static bool operator ==(ElasticVersion left, ElasticVersion right) => Equals(left, right);

        public static bool operator !=(ElasticVersion left, ElasticVersion right) => !Equals(left, right);

        public static bool operator >(ElasticVersion left, ElasticVersion right) => Compare(left, right) > 0;

        public static bool operator >=(ElasticVersion left, ElasticVersion right) => left == right || left > right;

        public static bool operator <(ElasticVersion left, ElasticVersion right) => Compare(left, right) < 0;

        public static bool operator <=(ElasticVersion left, ElasticVersion right) => left == right || left < right;
    }
}

Copy link
Contributor Author

@gregkalapos gregkalapos Nov 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point - especially because the APM Server sends semantic versions, so we should parse it accordingly.

To the question:

Will this ever need to take into account prerelease versions?

I don't think it will, but that's just a guess, maybe it will - but for the parsing we need to take this into consideration.

So I took this class and added it to the PR.

public bool ServerVersionQueried => _initialized;
public Version Version { get; private set; }

public async Task GetServerInfoAsync()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the fetching of server info (behaviour) be separate from the data?

Copy link
Contributor Author

@gregkalapos gregkalapos Nov 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I separated the fetching logic - one related problem I found is that if an API keys or Secret token is set, then it should be also used for the Server Info API, otherwise the APM server won't respond.

});
};

if (serverInfo.ServerVersionQueried && serverInfo.Version != null && serverInfo.Version < V710)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on having an IStackTrace[Generator|Transformer] with two different implementations to perform the relevant changes to CapturedStackFrame?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the changes in this PR I would not do it - long term this if block will go away. We can completely remove this part once 7.9 is out of support.

But in general I agree there is a potential to better structure the stack trace capturing logic. Happy to make follow ups, but for solving the current problem from this PR I think this is fine.

Copy link
Contributor

@russcam russcam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@russcam
Copy link
Contributor

russcam commented Nov 11, 2020

jenkins run the tests please

@russcam
Copy link
Contributor

russcam commented Nov 12, 2020

tests look like they're timing out on this. Can we bump the timeout, @gregkalapos?

@gregkalapos gregkalapos merged commit e352281 into elastic:master Nov 12, 2020
@gregkalapos gregkalapos deleted the ClassNameOnStackTrace branch November 12, 2020 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants