Example #1
0
        /// <summary>
        /// Collects the Request body in the transaction
        /// </summary>
        /// <param name="transaction">Transaction object</param>
        /// <param name="isForError">Is request body being captured for error (otherwise it's for transaction)</param>
        /// <param name="httpRequest">Current http request</param>
        /// <param name="logger">Logger object</param>
        /// <param name="configSnapshot">The config snapshot of the current transaction. This is for reading the sanitization settings.</param>
        internal static void CollectRequestBody(this Transaction transaction, bool isForError, HttpRequest httpRequest, IApmLogger logger,
                                                IConfigSnapshot configSnapshot
                                                )
        {
            if (!transaction.IsSampled)
            {
                return;
            }

            if (httpRequest == null)
            {
                return;
            }

            string body = null;

            // Is request body already captured?
            // We check transaction.IsContextCreated to avoid creating empty Context (that accessing transaction.Context directly would have done).
            if (transaction.IsContextCreated &&
                transaction.Context.Request.Body != null &&
                !ReferenceEquals(transaction.Context.Request.Body, Apm.Consts.Redacted))
            {
                return;
            }

            if (transaction.IsCaptureRequestBodyEnabled(isForError) && IsCaptureRequestBodyEnabledForContentType(transaction, httpRequest, logger))
            {
                body = httpRequest.ExtractRequestBody(logger, configSnapshot);
            }

            // According to the documentation - the default value of 'body' is '[Redacted]'
            transaction.Context.Request.Body = body ?? Apm.Consts.Redacted;
        }
Example #2
0
        internal BackendCommComponentBase(bool isEnabled, IApmLogger logger, string dbgDerivedClassName, Service service
                                          , IConfigSnapshot config, HttpMessageHandler httpMessageHandler = null
                                          )
        {
            _dbgName   = $"{ThisClassName} ({dbgDerivedClassName})";
            _logger    = logger?.Scoped(_dbgName);
            _isEnabled = isEnabled;

            if (!_isEnabled)
            {
                _logger.Debug()?.Log("Disabled - exiting without initializing any members used by work loop");
                return;
            }

            CtsInstance = new CancellationTokenSource();

            _disposableHelper = new DisposableHelper();

            _loopStarted   = new ManualResetEventSlim();
            _loopCompleted = new ManualResetEventSlim();

            HttpClientInstance = BackendCommUtils.BuildHttpClient(logger, config, service, _dbgName, httpMessageHandler);

            _singleThreadTaskScheduler = new SingleThreadTaskScheduler($"ElasticApm{dbgDerivedClassName}", logger);
        }
Example #3
0
            internal SutEnv(IConfigSnapshot startConfig = null)
            {
                Agent = new ApmAgent(new TestAgentComponents(new NoopLogger(), startConfig, MockPayloadSender));

                _taskForSampleApp = Program.CreateWebHostBuilder(null)
                                    .ConfigureServices(services =>
                {
                    Startup.ConfigureServicesExceptMvc(services);

                    services.AddMvc()
                    .AddApplicationPart(Assembly.Load(new AssemblyName(nameof(SampleAspNetCoreApp))));
                }
                                                       )
                                    .Configure(app =>
                {
                    app.UseElasticApm(Agent, new TestLogger());
                    Startup.ConfigureAllExceptAgent(app);
                })
                                    .UseUrls(UrlForTestApp)
                                    .Build()
                                    .RunAsync(_cancellationTokenSource.Token);

                HttpClient = new HttpClient {
                    BaseAddress = new Uri(UrlForTestApp)
                };
            }
Example #4
0
        private SutEnv StartSutEnv(IConfigSnapshot startConfig = null)
        {
            if (_sutEnv != null)
            {
                return(_sutEnv);
            }

            _sutEnv = new SutEnv(startConfig);
            return(_sutEnv);
        }
Example #5
0
        public PayloadSenderV2(IApmLogger logger, IConfigSnapshot config, Service service, Api.System system,
                               HttpMessageHandler httpMessageHandler = null, string dbgName = null
                               )
            : base(/* isEnabled: */ true, logger, ThisClassName, service, config, httpMessageHandler)
        {
            _logger = logger?.Scoped(ThisClassName + (dbgName == null ? "" : $" (dbgName: `{dbgName}')"));
            _payloadItemSerializer = new PayloadItemSerializer(config);

            _intakeV2EventsAbsoluteUrl = BackendCommUtils.ApmServerEndpoints.BuildIntakeV2EventsAbsoluteUrl(config.ServerUrls.First());

            System = system;

            _metadata = new Metadata {
                Service = service, System = System
            };
            foreach (var globalLabelKeyValue in config.GlobalLabels)
            {
                _metadata.Labels.Add(globalLabelKeyValue.Key, globalLabelKeyValue.Value);
            }

            if (config.MaxQueueEventCount < config.MaxBatchEventCount)
            {
                _logger?.Error()
                ?.Log(
                    "MaxQueueEventCount is less than MaxBatchEventCount - using MaxBatchEventCount as MaxQueueEventCount."
                    + " MaxQueueEventCount: {MaxQueueEventCount}."
                    + " MaxBatchEventCount: {MaxBatchEventCount}.",
                    config.MaxQueueEventCount, config.MaxBatchEventCount);

                _maxQueueEventCount = config.MaxBatchEventCount;
            }
            else
            {
                _maxQueueEventCount = config.MaxQueueEventCount;
            }

            _flushInterval = config.FlushInterval;

            _logger?.Debug()
            ?.Log(
                "Using the following configuration options:"
                + " Events intake API absolute URL: {EventsIntakeAbsoluteUrl}"
                + ", FlushInterval: {FlushInterval}"
                + ", MaxBatchEventCount: {MaxBatchEventCount}"
                + ", MaxQueueEventCount: {MaxQueueEventCount}"
                , _intakeV2EventsAbsoluteUrl, _flushInterval.ToHms(), config.MaxBatchEventCount, _maxQueueEventCount);

            _eventQueue = new BatchBlock <object>(config.MaxBatchEventCount);
            TransactionFilters.Add(new TransactionIgnoreUrlsFilter(config).Filter);
            StartWorkLoop();
        }
        internal static HttpClient BuildHttpClient(IApmLogger loggerArg, IConfigSnapshot config, Service service, string dbgCallerDesc
                                                   , HttpMessageHandler httpMessageHandler = null
                                                   )
        {
            var logger = loggerArg.Scoped(ThisClassName);

            var serverUrlBase = config.ServerUrl;

            ConfigServicePoint(serverUrlBase, loggerArg);

            logger.Debug()
            ?.Log("Building HTTP client with BaseAddress: {ApmServerUrl} for {dbgCallerDesc}..."
                  , serverUrlBase, dbgCallerDesc);
            var httpClient =
                new HttpClient(httpMessageHandler ?? CreateHttpClientHandler(config, loggerArg))
            {
                BaseAddress = serverUrlBase
            };

            httpClient.DefaultRequestHeaders.UserAgent.Add(
                new ProductInfoHeaderValue($"elasticapm-{Consts.AgentName}", AdaptUserAgentValue(service.Agent.Version)));
            httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("System.Net.Http",
                                                                                      AdaptUserAgentValue(typeof(HttpClient).Assembly.GetCustomAttribute <AssemblyFileVersionAttribute>().Version)));

            if (service.Runtime != null)
            {
                httpClient.DefaultRequestHeaders.UserAgent.Add(
                    new ProductInfoHeaderValue(AdaptUserAgentValue(service.Runtime.Name), AdaptUserAgentValue(service.Runtime.Version)));
            }

            if (config.ApiKey != null)
            {
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("ApiKey", config.ApiKey);
            }
            else if (config.SecretToken != null)
            {
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", config.SecretToken);
            }

            return(httpClient);

            // Replace invalid characters by underscore. All invalid characters can be found at
            // https://github.com/dotnet/corefx/blob/e64cac6dcacf996f98f0b3f75fb7ad0c12f588f7/src/System.Net.Http/src/System/Net/Http/HttpRuleParser.cs#L41
            string AdaptUserAgentValue(string value)
            {
                return(Regex.Replace(value, "[ /()<>@,:;={}?\\[\\]\"\\\\]", "_"));
            }
        }
Example #7
0
 public TestAgentComponents(
     IApmLogger logger            = null,
     IConfigSnapshot config       = null,
     IPayloadSender payloadSender = null,
     ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer = null,
     ICentralConfigFetcher centralConfigFetcher = null
     ) : base(
         logger ?? new NoopLogger(),
         config ?? new MockConfigSnapshot(logger ?? new NoopLogger()),
         payloadSender ?? new MockPayloadSender(),
         new FakeMetricsCollector(),
         currentExecutionSegmentsContainer,
         centralConfigFetcher ?? new NoopCentralConfigFetcher()
         )
 {
 }
        public PayloadSenderV2(IApmLogger logger, IConfigSnapshot config, Service service, Api.System system,
                               HttpMessageHandler httpMessageHandler = null, string dbgName = null
                               )
            : base(/* isEnabled: */ true, logger, ThisClassName, service, config, httpMessageHandler)
        {
            _logger = logger?.Scoped(ThisClassName + (dbgName == null ? "" : $" (dbgName: `{dbgName}')"));

            System    = system;
            _metadata = new Metadata {
                Service = service, System = System
            };

            if (config.MaxQueueEventCount < config.MaxBatchEventCount)
            {
                _logger?.Error()
                ?.Log(
                    "MaxQueueEventCount is less than MaxBatchEventCount - using MaxBatchEventCount as MaxQueueEventCount."
                    + " MaxQueueEventCount: {MaxQueueEventCount}."
                    + " MaxBatchEventCount: {MaxBatchEventCount}.",
                    config.MaxQueueEventCount, config.MaxBatchEventCount);

                _maxQueueEventCount = config.MaxBatchEventCount;
            }
            else
            {
                _maxQueueEventCount = config.MaxQueueEventCount;
            }

            _flushInterval = config.FlushInterval;

            _logger?.Debug()
            ?.Log(
                "Using the following configuration options:"
                + " FlushInterval: {FlushInterval}"
                + ", MaxBatchEventCount: {MaxBatchEventCount}"
                + ", MaxQueueEventCount: {MaxQueueEventCount}"
                , _flushInterval.ToHms(), config.MaxBatchEventCount, _maxQueueEventCount);

            _eventQueue = new BatchBlock <object>(config.MaxBatchEventCount);

            StartWorkLoop();
        }
Example #9
0
        /// <summary>
        /// We need this private ctor to avoid calling configStore.CurrentSnapshot twice (and thus possibly using different
        /// snapshots)
        /// when passing isEnabled: initialConfigSnapshot.CentralConfig and config: initialConfigSnapshot to base
        /// </summary>
        private CentralConfigFetcher(IApmLogger logger, IConfigStore configStore, IConfigSnapshot initialConfigSnapshot, Service service
                                     , HttpMessageHandler httpMessageHandler, IAgentTimer agentTimer, string dbgName
                                     )
            : base(/* isEnabled: */ initialConfigSnapshot.CentralConfig, logger, ThisClassName, service, initialConfigSnapshot, httpMessageHandler)
        {
            _logger = logger?.Scoped(ThisClassName + (dbgName == null ? "" : $" (dbgName: `{dbgName}')"));

            _initialSnapshot = initialConfigSnapshot;

            var isCentralConfigOptEqDefault = _initialSnapshot.CentralConfig == ConfigConsts.DefaultValues.CentralConfig;
            var centralConfigStatus         = _initialSnapshot.CentralConfig ? "enabled" : "disabled";

            if (!isCentralConfigOptEqDefault)
            {
                centralConfigStatus = centralConfigStatus.ToUpper();
            }
            _logger.IfLevel(isCentralConfigOptEqDefault ? LogLevel.Debug : LogLevel.Information)
            ?.Log("Central configuration feature is {CentralConfigStatus} because CentralConfig option's value is {CentralConfigOptionValue}"
                  + " (default value is {CentralConfigOptionDefaultValue})"
                  , centralConfigStatus, _initialSnapshot.CentralConfig, ConfigConsts.DefaultValues.CentralConfig);

            if (!_initialSnapshot.CentralConfig)
            {
                return;
            }

            _configStore = configStore;

            _agentTimer = agentTimer ?? new AgentTimer();

            _getConfigAbsoluteUrl = BackendCommUtils.ApmServerEndpoints.BuildGetConfigAbsoluteUrl(initialConfigSnapshot.ServerUrls.First(), service);
            _logger.Debug()
            ?.Log("Combined absolute URL for APM Server get central configuration endpoint: `{Url}'. Service: {Service}."
                  , _getConfigAbsoluteUrl, service);

            StartWorkLoop();
        }
        /// <summary>
        /// Extracts the request body using measure to prevent the 'read once' problem (cannot read after the body ha been already
        /// read).
        /// </summary>
        /// <param name="request"></param>
        /// <param name="logger"></param>
        /// <returns></returns>
        public static string ExtractRequestBody(this HttpRequest request, IApmLogger logger, IConfigSnapshot configSnapshot)
        {
            string body = null;

            try
            {
                if (request.HasFormContentType)
                {
                    var form = request.Form;

                    var itemProcessed = 0;
                    if (form != null && form.Count > 0)
                    {
                        var sb = new StringBuilder();

                        foreach (var item in form)
                        {
                            sb.Append(item.Key);
                            sb.Append("=");

                            if (WildcardMatcher.IsAnyMatch(configSnapshot.SanitizeFieldNames, item.Key))
                            {
                                sb.Append(Elastic.Apm.Consts.Redacted);
                            }
                            else
                            {
                                sb.Append(item.Value);
                            }

                            itemProcessed++;
                            if (itemProcessed != form.Count)
                            {
                                sb.Append("&");
                            }
                        }

                        body = sb.ToString();
                    }
                }
                else
                {
                    request.EnableBuffering();
                    request.Body.Position = 0;

                    using (var reader = new StreamReader(request.Body,
                                                         Encoding.UTF8,
                                                         false,
                                                         1024 * 2,
                                                         true))
                        body = reader.ReadToEnd();

                    // Truncate the body to the first 2kb if it's longer
                    if (body.Length > Consts.RequestBodyMaxLength)
                    {
                        body = body.Substring(0, Consts.RequestBodyMaxLength);
                    }
                    request.Body.Position = 0;
                }
            }
            catch (IOException ioException)
            {
                logger.Error()?.LogException(ioException, "IO Error reading request body");
            }
            catch (Exception e)
            {
                logger.Error()?.LogException(e, "Error reading request body");
            }
            return(body);
        }
Example #11
0
 private static Dictionary <string, string> GetHeaders(IHeaderDictionary headers, IConfigSnapshot configSnapshot) =>
 configSnapshot.CaptureHeaders && headers != null
                         ? headers.ToDictionary(header => header.Key,
                                                header => WildcardMatcher.IsAnyMatch(configSnapshot.SanitizeFieldNames, header.Key)
                                                ?Apm.Consts.Redacted
                                                : header.Value.ToString())
                         : null;
Example #12
0
 public TransactionIgnoreUrlsFilter(IConfigSnapshot configSnapshot) => _configSnapshot = configSnapshot;
Example #13
0
 internal WrappingConfigSnapshot(IConfigSnapshot wrapped, ConfigDelta configDelta, string dbgDescription)
 {
     _wrapped       = wrapped;
     _configDelta   = configDelta;
     DbgDescription = dbgDescription;
 }
Example #14
0
        /// <summary>
        /// Creates a new transaction
        /// </summary>
        /// <param name="logger">The logger which logs debug information during the transaction  creation process</param>
        /// <param name="name">The name of the transaction</param>
        /// <param name="type">The type of the transaction</param>
        /// <param name="sampler">The sampler implementation which makes the sampling decision</param>
        /// <param name="distributedTracingData">Distributed tracing data, in case this transaction is part of a distributed trace</param>
        /// <param name="sender">The IPayloadSender implementation which will record this transaction</param>
        /// <param name="configSnapshot">The current configuration snapshot which contains the up-do-date config setting values</param>
        /// <param name="currentExecutionSegmentsContainer" />
        /// The ExecutionSegmentsContainer which makes sure this transaction flows
        /// <param name="apmServerInfo">Component to fetch info about APM Server (e.g. APM Server version)</param>
        /// <param name="ignoreActivity">
        /// If set the transaction will ignore Activity.Current and it's trace id,
        /// otherwise the agent will try to keep ids in-sync across async work-flows
        /// </param>
        internal Transaction(
            IApmLogger logger,
            string name,
            string type,
            Sampler sampler,
            DistributedTracingData distributedTracingData,
            IPayloadSender sender,
            IConfigSnapshot configSnapshot,
            ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer,
            IApmServerInfo apmServerInfo,
            bool ignoreActivity = false
            )
        {
            ConfigSnapshot = configSnapshot;
            Timestamp      = TimeUtils.TimestampNow();

            _logger        = logger?.Scoped(nameof(Transaction));
            _apmServerInfo = apmServerInfo;
            _sender        = sender;
            _currentExecutionSegmentsContainer = currentExecutionSegmentsContainer;

            Name          = name;
            HasCustomName = false;
            Type          = type;

            // For each new transaction, start an Activity if we're not ignoring them.
            // If Activity.Current is not null, the started activity will be a child activity,
            // so the traceid and tracestate of the parent will flow to it.
            if (!ignoreActivity)
            {
                _activity = StartActivity();
            }

            var isSamplingFromDistributedTracingData = false;

            if (distributedTracingData == null)
            {
                // We consider a newly created transaction **without** explicitly passed distributed tracing data
                // to be a root transaction.
                // Ignore the created activity ActivityTraceFlags because it starts out without setting the IsSampled flag,
                // so relying on that would mean a transaction is never sampled.
                if (_activity != null)
                {
                    // If an activity was created, reuse its id
                    Id      = _activity.SpanId.ToHexString();
                    TraceId = _activity.TraceId.ToHexString();

                    var idBytesFromActivity = new Span <byte>(new byte[16]);
                    _activity.TraceId.CopyTo(idBytesFromActivity);

                    // Read right most bits. From W3C TraceContext: "it is important for trace-id to carry "uniqueness" and "randomness"
                    // in the right part of the trace-id..."
                    idBytesFromActivity = idBytesFromActivity.Slice(8);

                    _traceState = new TraceState();

                    // If activity has a tracestate, populate the transaction tracestate with it.
                    if (!string.IsNullOrEmpty(_activity.TraceStateString))
                    {
                        _traceState.AddTextHeader(_activity.TraceStateString);
                    }

                    IsSampled = sampler.DecideIfToSample(idBytesFromActivity.ToArray());

                    // In the unlikely event that tracestate populated from activity contains an es vendor key, the tracestate
                    // is mutated to set the sample rate defined by the sampler, because we consider a transaction without
                    // explicitly passed distributedTracingData to be a **root** transaction. The end result
                    // is that activity tracestate will be propagated, along with the sample rate defined by this transaction.
                    if (IsSampled)
                    {
                        SampleRate = sampler.Rate;
                        _traceState.SetSampleRate(sampler.Rate);
                    }
                    else
                    {
                        SampleRate = 0;
                        _traceState.SetSampleRate(0);
                    }

                    // sync the activity tracestate with the tracestate of the transaction
                    _activity.TraceStateString = _traceState.ToTextHeader();
                }
                else
                {
                    // If no activity is created, create new random ids
                    var idBytes = new byte[8];
                    Id        = RandomGenerator.GenerateRandomBytesAsString(idBytes);
                    IsSampled = sampler.DecideIfToSample(idBytes);

                    idBytes = new byte[16];
                    TraceId = RandomGenerator.GenerateRandomBytesAsString(idBytes);

                    if (IsSampled)
                    {
                        _traceState = new TraceState(sampler.Rate);
                        SampleRate  = sampler.Rate;
                    }
                    else
                    {
                        _traceState = new TraceState(0);
                        SampleRate  = 0;
                    }
                }

                // ParentId could be also set here, but currently in the UI each trace must start with a transaction where the ParentId is null,
                // so to avoid https://github.com/elastic/apm-agent-dotnet/issues/883 we don't set it yet.
            }
            else
            {
                if (_activity != null)
                {
                    Id = _activity.SpanId.ToHexString();

                    // try to set the parent id and tracestate on the created activity, based on passed distributed tracing data.
                    // This is so that the distributed tracing data will flow to any child activities
                    try
                    {
                        _activity.SetParentId(
                            ActivityTraceId.CreateFromString(distributedTracingData.TraceId.AsSpan()),
                            ActivitySpanId.CreateFromString(distributedTracingData.ParentId.AsSpan()),
                            distributedTracingData.FlagRecorded ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None);

                        if (distributedTracingData.HasTraceState)
                        {
                            _activity.TraceStateString = distributedTracingData.TraceState.ToTextHeader();
                        }
                    }
                    catch (Exception e)
                    {
                        _logger.Error()?.LogException(e, "Error setting trace context on created activity");
                    }
                }
                else
                {
                    var idBytes = new byte[8];
                    Id = RandomGenerator.GenerateRandomBytesAsString(idBytes);
                }

                TraceId   = distributedTracingData.TraceId;
                ParentId  = distributedTracingData.ParentId;
                IsSampled = distributedTracingData.FlagRecorded;
                isSamplingFromDistributedTracingData = true;
                _traceState = distributedTracingData.TraceState;

                // If there is no tracestate or no valid "es" vendor entry with an "s" (sample rate) attribute, then the agent must
                // omit sample rate from non-root transactions and their spans.
                // See https://github.com/elastic/apm/blob/master/specs/agents/tracing-sampling.md#propagation
                if (_traceState?.SampleRate is null)
                {
                    SampleRate = null;
                }
                else
                {
                    SampleRate = _traceState.SampleRate.Value;
                }
            }

            // Also mark the sampling decision on the Activity
            if (IsSampled && _activity != null)
            {
                _activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded;
            }

            SpanCount = new SpanCount();
            _currentExecutionSegmentsContainer.CurrentTransaction = this;

            if (isSamplingFromDistributedTracingData)
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) and SampleRate ({SampleRate}) is based on incoming distributed tracing data ({DistributedTracingData})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, SampleRate, distributedTracingData, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }
            else
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on the given sampler ({Sampler})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, sampler, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }
        }
Example #15
0
        internal Transaction(
            IApmLogger logger,
            string name,
            string type,
            Sampler sampler,
            DistributedTracingData distributedTracingData,
            IPayloadSender sender,
            IConfigSnapshot configSnapshot,
            ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer
            )
        {
            ConfigSnapshot = configSnapshot;
            Timestamp      = TimeUtils.TimestampNow();
            var idBytes = new byte[8];

            Id      = RandomGenerator.GenerateRandomBytesAsString(idBytes);
            _logger = logger?.Scoped($"{nameof(Transaction)}.{Id}");

            _sender = sender;
            _currentExecutionSegmentsContainer = currentExecutionSegmentsContainer;

            Name          = name;
            HasCustomName = false;
            Type          = type;

            StartActivity();

            var isSamplingFromDistributedTracingData = false;

            if (distributedTracingData == null)
            {
                // Here we ignore Activity.Current.ActivityTraceFlags because it starts out without setting the IsSampled flag, so relying on that would mean a transaction is never sampled.
                IsSampled = sampler.DecideIfToSample(idBytes);

                if (Activity.Current != null && Activity.Current.IdFormat == ActivityIdFormat.W3C)
                {
                    TraceId  = Activity.Current.TraceId.ToString();
                    ParentId = Activity.Current.ParentId;

                    // Also mark the sampling decision on the Activity
                    if (IsSampled)
                    {
                        Activity.Current.ActivityTraceFlags |= ActivityTraceFlags.Recorded;
                    }
                }
                else
                {
                    TraceId = _activity.TraceId.ToString();
                }
            }
            else
            {
                TraceId   = distributedTracingData.TraceId;
                ParentId  = distributedTracingData.ParentId;
                IsSampled = distributedTracingData.FlagRecorded;
                isSamplingFromDistributedTracingData = true;
                _traceState = distributedTracingData.TraceState;
            }

            SpanCount = new SpanCount();

            _currentExecutionSegmentsContainer.CurrentTransaction = this;

            if (isSamplingFromDistributedTracingData)
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on incoming distributed tracing data ({DistributedTracingData})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, distributedTracingData, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }
            else
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on the given sampler ({Sampler})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, sampler, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }

            void StartActivity()
            {
                _activity = new Activity("ElasticApm.Transaction");
                _activity.SetIdFormat(ActivityIdFormat.W3C);
                _activity.Start();
            }
        }
        private static HttpClientHandler CreateHttpClientHandler(IConfigSnapshot config, IApmLogger logger)
        {
            Func <HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> serverCertificateCustomValidationCallback = null;

            if (!config.VerifyServerCert)
            {
                serverCertificateCustomValidationCallback = (_, _, _, policyError) =>
                {
                    if (policyError == SslPolicyErrors.None)
                    {
                        return(true);
                    }

                    logger.Trace()?.Log("Certificate validation failed. Policy error {PolicyError}", policyError);
                    return(true);
                };
            }
            else if (!string.IsNullOrEmpty(config.ServerCert))
            {
                try
                {
                    var serverCertificate = new X509Certificate2(config.ServerCert);
                    var publicKey         = serverCertificate.GetPublicKeyString();

                    serverCertificateCustomValidationCallback = (_, certificate, _, policyError) =>
                    {
                        if (policyError == SslPolicyErrors.None)
                        {
                            return(true);
                        }

                        if (certificate is null)
                        {
                            logger.Trace()?.Log("Certificate validation failed. No certificate to validate");
                            return(false);
                        }

                        var publicKeyToValidate = certificate.GetPublicKeyString();
                        if (string.Equals(publicKey, publicKeyToValidate, StringComparison.Ordinal))
                        {
                            return(true);
                        }

                        logger.Trace()
                        ?.Log(
                            "Certificate validation failed. Public key {PublicKey} does not match {ServerCert} public key",
                            publicKeyToValidate,
                            nameof(config.ServerCert));

                        return(false);
                    };
                }
                catch (Exception e)
                {
                    logger.Error()?.LogException(
                        e,
                        "Could not configure {ConfigServerCert} at path {Path} for certificate pinning",
                        nameof(config.ServerCert),
                        config.ServerCert);
                }
            }
            else
            {
                // set a default callback to log the policy error
                serverCertificateCustomValidationCallback = (_, _, _, policyError) =>
                {
                    if (policyError == SslPolicyErrors.None)
                    {
                        return(true);
                    }

                    logger.Trace()?.Log("Certificate validation failed. Policy error {PolicyError}", policyError);
                    return(false);
                };
            }

            return(new HttpClientHandler {
                ServerCertificateCustomValidationCallback = serverCertificateCustomValidationCallback
            });
        }
Example #17
0
        /// <summary>
        /// Extracts the request body, up to a specified maximum length.
        /// The request body that is read is buffered.
        /// </summary>
        /// <param name="request">The request</param>
        /// <param name="logger">The logger</param>
        /// <param name="configSnapshot">The configuration snapshot</param>
        /// <returns></returns>
        public static string ExtractRequestBody(this HttpRequest request, IApmLogger logger, IConfigSnapshot configSnapshot)
        {
            string body = null;
            var    longerThanMaxLength = false;

            try
            {
                if (request.HasFormContentType)
                {
                    var form = request.Form;

                    var itemProcessed = 0;
                    if (form != null && form.Count > 0)
                    {
                        var sb = new StringBuilder();
                        foreach (var item in form)
                        {
                            sb.Append(item.Key);
                            sb.Append("=");

                            if (WildcardMatcher.IsAnyMatch(configSnapshot.SanitizeFieldNames, item.Key))
                            {
                                sb.Append(Elastic.Apm.Consts.Redacted);
                            }
                            else
                            {
                                sb.Append(item.Value);
                            }

                            itemProcessed++;
                            if (itemProcessed != form.Count)
                            {
                                sb.Append("&");
                            }

                            // perf: check length once per iteration and truncate at the end, rather than each append
                            if (sb.Length > RequestBodyMaxLength)
                            {
                                longerThanMaxLength = true;
                                break;
                            }
                        }

                        body = sb.ToString(0, Math.Min(sb.Length, RequestBodyMaxLength));
                    }
                }
                else
                {
                    // allow synchronous reading of the request stream, which is false by default from 3.0 onwards.
                    // Reading must be synchronous as it can happen within a synchronous diagnostic listener method
                    var bodyControlFeature = request.HttpContext.Features.Get <IHttpBodyControlFeature>();
                    if (bodyControlFeature != null)
                    {
                        bodyControlFeature.AllowSynchronousIO = true;
                    }

                    request.EnableBuffering();
                    var requestBody = request.Body;
                    requestBody.Position = 0;
                    var arrayPool = ArrayPool <char> .Shared;
                    var capacity  = 512;
                    var buffer    = arrayPool.Rent(capacity);
                    var totalRead = 0;
                    int read;

                    // requestBody.Length is 0 on initial buffering - length relates to how much has been read and buffered.
                    // Read to just beyond request body max length so that we can determine if truncation will occur
                    try
                    {
                        // TODO: can we assume utf-8 encoding?
                        using var reader = new StreamReader(requestBody, Encoding.UTF8, false, buffer.Length, true);
                        while ((read = reader.Read(buffer, 0, capacity)) != 0)
                        {
                            totalRead += read;
                            if (totalRead > RequestBodyMaxLength)
                            {
                                longerThanMaxLength = true;
                                break;
                            }
                        }
                    }
                    finally
                    {
                        arrayPool.Return(buffer);
                    }

                    requestBody.Position = 0;
                    capacity             = Math.Min(totalRead, RequestBodyMaxLength);
                    buffer = arrayPool.Rent(capacity);

                    try
                    {
                        using var reader = new StreamReader(requestBody, Encoding.UTF8, false, RequestBodyMaxLength, true);
                        read             = reader.ReadBlock(buffer, 0, capacity);
                        body             = new string(buffer, 0, read);
                    }
                    finally
                    {
                        arrayPool.Return(buffer);
                    }

                    requestBody.Position = 0;
                }
            }
            catch (IOException ioException)
            {
                logger.Error()?.LogException(ioException, "IO Error reading request body");
            }
            catch (Exception e)
            {
                logger.Error()?.LogException(e, "Error reading request body");
            }

            if (longerThanMaxLength)
            {
                logger.Debug()?.Log("truncated body to max length {MaxLength}", RequestBodyMaxLength);
            }

            return(body);
        }
Example #18
0
 private static Dictionary <string, string> GetHeaders(IHeaderDictionary headers, IConfigSnapshot configSnapshot) =>
 configSnapshot.CaptureHeaders && headers != null
                         ? headers.ToDictionary(header => header.Key, header => header.Value.ToString())
                         : null;
Example #19
0
 internal ConfigStore(IConfigSnapshot initialSnapshot, IApmLogger logger)
 {
     _logger          = logger.Scoped(ThisClassName);
     _currentSnapshot = initialSnapshot;
 }
Example #20
0
        public PayloadSenderV2(
            IApmLogger logger,
            IConfigSnapshot config,
            Service service,
            Api.System system,
            IApmServerInfo apmServerInfo,
            HttpMessageHandler httpMessageHandler = null,
            string dbgName = null,
            bool isEnabled = true,
            IEnvironmentVariables environmentVariables = null
            )
            : base(isEnabled, logger, ThisClassName, service, config, httpMessageHandler)
        {
            if (!isEnabled)
            {
                return;
            }

            _logger = logger?.Scoped(ThisClassName + (dbgName == null ? "" : $" (dbgName: `{dbgName}')"));
            _payloadItemSerializer = new PayloadItemSerializer();
            _configSnapshot        = config;

            _intakeV2EventsAbsoluteUrl = BackendCommUtils.ApmServerEndpoints.BuildIntakeV2EventsAbsoluteUrl(config.ServerUrl);

            System = system;

            _cloudMetadataProviderCollection = new CloudMetadataProviderCollection(config.CloudProvider, _logger, environmentVariables);
            _apmServerInfo = apmServerInfo;
            _metadata      = new Metadata {
                Service = service, System = System
            };
            foreach (var globalLabelKeyValue in config.GlobalLabels)
            {
                _metadata.Labels.Add(globalLabelKeyValue.Key, globalLabelKeyValue.Value);
            }

            if (config.MaxQueueEventCount < config.MaxBatchEventCount)
            {
                _logger?.Error()
                ?.Log(
                    "MaxQueueEventCount is less than MaxBatchEventCount - using MaxBatchEventCount as MaxQueueEventCount."
                    + " MaxQueueEventCount: {MaxQueueEventCount}."
                    + " MaxBatchEventCount: {MaxBatchEventCount}.",
                    config.MaxQueueEventCount, config.MaxBatchEventCount);

                _maxQueueEventCount = config.MaxBatchEventCount;
            }
            else
            {
                _maxQueueEventCount = config.MaxQueueEventCount;
            }

            _flushInterval = config.FlushInterval;

            _logger?.Debug()
            ?.Log(
                "Using the following configuration options:"
                + " Events intake API absolute URL: {EventsIntakeAbsoluteUrl}"
                + ", FlushInterval: {FlushInterval}"
                + ", MaxBatchEventCount: {MaxBatchEventCount}"
                + ", MaxQueueEventCount: {MaxQueueEventCount}"
                , _intakeV2EventsAbsoluteUrl, _flushInterval.ToHms(), config.MaxBatchEventCount, _maxQueueEventCount);

            _eventQueue = new BatchBlock <object>(config.MaxBatchEventCount);

            SetUpFilters(TransactionFilters, SpanFilters, ErrorFilters, apmServerInfo, logger);
            StartWorkLoop();
        }
Example #21
0
        internal Transaction(
            IApmLogger logger,
            string name,
            string type,
            Sampler sampler,
            DistributedTracingData distributedTracingData,
            IPayloadSender sender,
            IConfigSnapshot configSnapshot,
            ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer
            )
        {
            ConfigSnapshot = configSnapshot;
            Timestamp      = TimeUtils.TimestampNow();
            var idBytes = new byte[8];

            Id      = RandomGenerator.GenerateRandomBytesAsString(idBytes);
            _logger = logger?.Scoped($"{nameof(Transaction)}.{Id}");

            _sender = sender;
            _currentExecutionSegmentsContainer = currentExecutionSegmentsContainer;

            Name          = name;
            HasCustomName = false;
            Type          = type;

            var isSamplingFromDistributedTracingData = false;

            if (distributedTracingData == null)
            {
                var traceIdBytes = new byte[16];
                TraceId   = RandomGenerator.GenerateRandomBytesAsString(traceIdBytes);
                IsSampled = sampler.DecideIfToSample(idBytes);
            }
            else
            {
                TraceId   = distributedTracingData.TraceId;
                ParentId  = distributedTracingData.ParentId;
                IsSampled = distributedTracingData.FlagRecorded;
                isSamplingFromDistributedTracingData = true;
                _traceState = distributedTracingData.TraceState;
            }

            SpanCount = new SpanCount();

            _currentExecutionSegmentsContainer.CurrentTransaction = this;

            if (isSamplingFromDistributedTracingData)
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on incoming distributed tracing data ({DistributedTracingData})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, distributedTracingData, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }
            else
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on the given sampler ({Sampler})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, sampler, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }
        }
Example #22
0
 internal WrappingConfigSnapshot(IConfigSnapshot wrapped, CentralConfigReader centralConfig, string dbgDescription)
 {
     _wrapped       = wrapped;
     _centralConfig = centralConfig;
     DbgDescription = dbgDescription;
 }
Example #23
0
        /// <summary>
        /// Creates a new transaction
        /// </summary>
        /// <param name="logger">The logger which logs debug information during the transaction  creation process</param>
        /// <param name="name">The name of the transaction</param>
        /// <param name="type">The type of the transaction</param>
        /// <param name="sampler">The sampler implementation which makes the sampling decision</param>
        /// <param name="distributedTracingData">Distributed tracing data, in case this transaction is part of a distributed trace</param>
        /// <param name="sender">The IPayloadSender implementation which will record this transaction</param>
        /// <param name="configSnapshot">The current configuration snapshot which contains the up-do-date config setting values</param>
        /// <param name="currentExecutionSegmentsContainer" />
        /// The ExecutionSegmentsContainer which makes sure this transaction flows
        /// <param name="ignoreActivity">
        /// If set the transaction will ignore Activity.Current and it's trace id,
        /// otherwise the agent will try to keep ids in-sync across async work-flows
        /// </param>
        internal Transaction(
            IApmLogger logger,
            string name,
            string type,
            Sampler sampler,
            DistributedTracingData distributedTracingData,
            IPayloadSender sender,
            IConfigSnapshot configSnapshot,
            ICurrentExecutionSegmentsContainer currentExecutionSegmentsContainer,
            bool ignoreActivity = false
            )
        {
            ConfigSnapshot = configSnapshot;
            Timestamp      = TimeUtils.TimestampNow();

            _logger = logger?.Scoped($"{nameof(Transaction)}");

            _sender = sender;
            _currentExecutionSegmentsContainer = currentExecutionSegmentsContainer;

            Name          = name;
            HasCustomName = false;
            Type          = type;

            // For each transaction start, we fire an Activity
            // If Activity.Current is null, then we create one with this and set its traceid, which will flow to all child activities and we also reuse it in Elastic APM, so it'll be the same on all Activities and in Elastic
            // If Activity.Current is not null, we pick up its traceid and apply it in Elastic APM
            if (!ignoreActivity)
            {
                StartActivity();
            }

            var isSamplingFromDistributedTracingData = false;

            if (distributedTracingData == null)
            {
                // Here we ignore Activity.Current.ActivityTraceFlags because it starts out without setting the IsSampled flag, so relying on that would mean a transaction is never sampled.
                // To be sure activity creation was successful let's check on it
                if (_activity != null)
                {
                    // In case activity creation was successful, let's reuse the ids
                    Id      = _activity.SpanId.ToHexString();
                    TraceId = _activity.TraceId.ToHexString();

                    var idBytesFromActivity = new Span <byte>(new byte[16]);
                    _activity.TraceId.CopyTo(idBytesFromActivity);
                    // Read right most bits. From W3C TraceContext: "it is important for trace-id to carry "uniqueness" and "randomness" in the right part of the trace-id..."
                    idBytesFromActivity = idBytesFromActivity.Slice(8);
                    IsSampled           = sampler.DecideIfToSample(idBytesFromActivity.ToArray());
                }
                else
                {
                    // In case from some reason the activity creation was not successful, let's create new random ids
                    var idBytes = new byte[8];
                    Id        = RandomGenerator.GenerateRandomBytesAsString(idBytes);
                    IsSampled = sampler.DecideIfToSample(idBytes);

                    idBytes = new byte[16];
                    TraceId = RandomGenerator.GenerateRandomBytesAsString(idBytes);
                }

                // PrentId could be also set here, but currently in the UI each trace must start with a transaction where the ParentId is null,
                // so to avoid https://github.com/elastic/apm-agent-dotnet/issues/883 we don't set it yet.
            }
            else
            {
                if (_activity != null)
                {
                    Id = _activity.SpanId.ToHexString();
                }
                else
                {
                    var idBytes = new byte[8];
                    Id = RandomGenerator.GenerateRandomBytesAsString(idBytes);
                }
                TraceId   = distributedTracingData.TraceId;
                ParentId  = distributedTracingData.ParentId;
                IsSampled = distributedTracingData.FlagRecorded;
                isSamplingFromDistributedTracingData = true;
                _traceState = distributedTracingData.TraceState;
            }

            // Also mark the sampling decision on the Activity
            if (IsSampled && _activity != null && !ignoreActivity)
            {
                _activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded;
            }

            SpanCount = new SpanCount();
            _currentExecutionSegmentsContainer.CurrentTransaction = this;

            if (isSamplingFromDistributedTracingData)
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on incoming distributed tracing data ({DistributedTracingData})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, distributedTracingData, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }
            else
            {
                _logger.Trace()
                ?.Log("New Transaction instance created: {Transaction}. " +
                      "IsSampled ({IsSampled}) is based on the given sampler ({Sampler})." +
                      " Start time: {Time} (as timestamp: {Timestamp})",
                      this, IsSampled, sampler, TimeUtils.FormatTimestampForLog(Timestamp), Timestamp);
            }

            void StartActivity()
            {
                _activity = new Activity(ApmTransactionActivityName);
                _activity.SetIdFormat(ActivityIdFormat.W3C);
                _activity.Start();
            }
        }
Example #24
0
        internal static ITransaction StartTransactionAsync(HttpContext context, IApmLogger logger, Tracer tracer, IConfigSnapshot configSnapshot)
        {
            try
            {
                if (WildcardMatcher.IsAnyMatch(configSnapshot.TransactionIgnoreUrls, context.Request.Path))
                {
                    logger.Debug()?.Log("Request ignored based on TransactionIgnoreUrls, url: {urlPath}", context.Request.Path);
                    return(null);
                }

                ITransaction transaction;
                var          transactionName = $"{context.Request.Method} {context.Request.Path}";

                if (context.Request.Headers.ContainsKey(TraceContext.TraceParentHeaderNamePrefixed) ||
                    context.Request.Headers.ContainsKey(TraceContext.TraceParentHeaderName))
                {
                    var headerValue = context.Request.Headers.ContainsKey(TraceContext.TraceParentHeaderName)
                                                ? context.Request.Headers[TraceContext.TraceParentHeaderName].ToString()
                                                : context.Request.Headers[TraceContext.TraceParentHeaderNamePrefixed].ToString();

                    var tracingData = context.Request.Headers.ContainsKey(TraceContext.TraceStateHeaderName)
                                                ? TraceContext.TryExtractTracingData(headerValue, context.Request.Headers[TraceContext.TraceStateHeaderName].ToString())
                                                : TraceContext.TryExtractTracingData(headerValue);

                    if (tracingData != null)
                    {
                        logger.Debug()
                        ?.Log(
                            "Incoming request with {TraceParentHeaderName} header. DistributedTracingData: {DistributedTracingData}. Continuing trace.",
                            TraceContext.TraceParentHeaderNamePrefixed, tracingData);

                        transaction = tracer.StartTransaction(transactionName, ApiConstants.TypeRequest, tracingData);
                    }
                    else
                    {
                        logger.Debug()
                        ?.Log(
                            "Incoming request with invalid {TraceParentHeaderName} header (received value: {TraceParentHeaderValue}). Starting trace with new trace id.",
                            TraceContext.TraceParentHeaderNamePrefixed, headerValue);

                        transaction = tracer.StartTransaction(transactionName, ApiConstants.TypeRequest);
                    }
                }
                else
                {
                    logger.Debug()?.Log("Incoming request. Starting Trace.");
                    transaction = tracer.StartTransaction(transactionName, ApiConstants.TypeRequest);
                }

                return(transaction);
            }
            catch (Exception ex)
            {
                logger?.Error()?.LogException(ex, "Exception thrown while trying to start transaction");
                return(null);
            }
        }
Example #25
0
        internal static async Task FillApmServerInfo(IApmServerInfo apmServerInfo, IApmLogger logger, IConfigSnapshot configSnapshot,
                                                     HttpClient httpClient
                                                     )
        {
            try
            {
                using var requestMessage = new HttpRequestMessage(HttpMethod.Get, configSnapshot.ServerUrl);
                requestMessage.Headers.Add("Metadata", "true");

                var responseMessage = await httpClient.SendAsync(requestMessage).ConfigureAwait(false);

                if (responseMessage.IsSuccessStatusCode)
                {
                    using var stream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false);

                    using var streamReader = new StreamReader(stream, Encoding.UTF8);
                    using var jsonReader   = new JsonTextReader(streamReader);

                    var serializer = new JsonSerializer();
                    var metadata   = serializer.Deserialize <JObject>(jsonReader);
                    var version    = metadata["version"];
                    var strVersion = version?.Value <string>();
                    if (strVersion != null)
                    {
                        try
                        {
                            apmServerInfo.Version = new ElasticVersion(strVersion);
                        }
                        catch (Exception e)
                        {
                            logger.Warning()?.LogException(e, "Failed parsing APM Server version - version string: {VersionString}", strVersion);
                        }
                    }
                }
                else
                {
                    logger.Warning()?.Log("Failed reading APM server info, response from server: {ResponseCode}", responseMessage.StatusCode);
                }
            }
            catch (Exception e)
            {
                logger.Warning()?.LogException(e, "Failed reading APM server info");
            }
        }