Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added MockTracer and MockTracer.Tests projects #54

Merged
merged 8 commits into from
Feb 21, 2018
326 changes: 326 additions & 0 deletions src/OpenTracing/Mock/MockSpan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;

namespace OpenTracing.Mock
{
/// <summary>
/// MockSpans are created via <see cref="MockTracer.BuildSpan"/>, but they are also returned via calls to
/// <see cref="MockTracer.FinishedSpans"/>. They provide accessors to all Span state.
/// </summary>
/// <seealso cref="MockTracer.FinishedSpans"/>
public sealed class MockSpan : ISpan
{
/// <summary>
/// Used to monotonically update ids
/// </summary>
private static long _nextIdCounter = 0;

/// <summary>
/// A simple-as-possible (consecutive for repeatability) id generator.
/// </summary>
private static long NextId()
{
return Interlocked.Increment(ref _nextIdCounter);
}

private readonly object _lock = new object();

private readonly MockTracer _mockTracer;
private MockSpanContext _context;
private DateTimeOffset _finishTimestamp;
private bool _finished;
private readonly Dictionary<string, object> _tags;
private readonly List<Reference> _references;
private readonly List<LogEntry> _logEntries = new List<LogEntry>();
private readonly List<Exception> _errors = new List<Exception>();

/// <summary>
/// The spanId of the Span's first <see cref="References.ChildOf"/> reference, or the first reference of any type,
/// or 0 if no reference exists.
/// </summary>
/// <seealso cref="MockSpanContext.SpanId"/>
/// <seealso cref="MockSpan.References"/>
public long ParentId { get; }

public DateTimeOffset StartTimestamp { get; }

/// <summary>
/// The finish time of the Span; only valid after a call to <see cref="Finish()"/>.
/// </summary>
public DateTimeOffset FinishTimestamp
{
get
{
if (_finishTimestamp == DateTimeOffset.MinValue)
throw new InvalidOperationException("Must call Finish() before FinishTimestamp");

return _finishTimestamp;
}
}

public string OperationName { get; private set; }

/// <summary>
/// A copy of all tags set on this span.
/// </summary>
public Dictionary<string, object> Tags => new Dictionary<string, object>(_tags);

/// <summary>
/// A copy of all log entries added to this span.
/// </summary>
public List<LogEntry> LogEntries => new List<LogEntry>(_logEntries);

/// <summary>
/// A copy of exceptions thrown by this class (e.g. adding a tag after span is finished).
/// </summary>
public List<Exception> GeneratedErrors => new List<Exception>(_errors);

public List<Reference> References => new List<Reference>(_references);

public MockSpanContext Context
{
// C# doesn't have "return type covariance" so we use the trick with the explicit interface implementation
// and this separate property.
get
{
lock (_lock)
{
return _context;
}
}
}

ISpanContext ISpan.Context => Context;

public MockSpan(
MockTracer tracer,
string operationName,
DateTimeOffset startTimestamp,
Dictionary<string, object> initialTags,
List<Reference> references)
{
_mockTracer = tracer;
OperationName = operationName;
StartTimestamp = startTimestamp;

_tags = initialTags == null
? new Dictionary<string, object>()
: new Dictionary<string, object>(initialTags);

_references = references == null
? new List<Reference>()
: references.ToList();

var parentContext = FindPreferredParentRef(_references);

if (parentContext == null)
{
// we are a root span
_context = new MockSpanContext(NextId(), NextId(), new Dictionary<string, string>());
ParentId = 0;
}
else
{
// we are a child span
_context = new MockSpanContext(parentContext.TraceId, NextId(), MergeBaggages(_references));
ParentId = parentContext.SpanId;
}
}

public ISpan SetOperationName(string operationName)
{
CheckForFinished("Setting operationName [{0}] on already finished span", operationName);
OperationName = operationName;
return this;
}

public ISpan SetTag(string key, bool value)
{
return SetObjectTag(key, value);
}

public ISpan SetTag(string key, double value)
{
return SetObjectTag(key, value);
}

public ISpan SetTag(string key, int value)
{
return SetObjectTag(key, value);
}

public ISpan SetTag(string key, string value)
{
return SetObjectTag(key, value);
}

private ISpan SetObjectTag(string key, object value)
{
lock (_lock)
{
CheckForFinished("Setting tag [{0}:{1}] on already finished span", key, value);
_tags[key] = value;
return this;
}
}

public ISpan Log(IDictionary<string, object> fields)
{
return Log(DateTimeOffset.UtcNow, fields);
}

public ISpan Log(DateTimeOffset timestamp, IDictionary<string, object> fields)
{
lock (_lock)
{
CheckForFinished("Adding logs {0} at {1} to already finished span.", fields, timestamp);
_logEntries.Add(new LogEntry(timestamp, fields));
return this;
}
}

public ISpan Log(string @event)
{
return Log(DateTimeOffset.UtcNow, @event);
}

public ISpan Log(DateTimeOffset timestamp, string @event)
{
return Log(timestamp, new Dictionary<string, object> { { "event", @event } });
}

public ISpan SetBaggageItem(string key, string value)
{
lock (_lock)
Copy link
Contributor

Choose a reason for hiding this comment

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

:( locks hurt

System.Collections.Concurrent avoid locks, and definitely minimize them (e.g. why is the string format inside the lock?)
This is fine if this is genuinely a MockTracer, only used for testing, but it almost seems like it's an InMemoryTracer. If we are not going to have a separate InMemoryTracer, then I'd suggest we ensure that this tracer is also optimized for production code for folks who want to instrument but do not want to send their data off to another system quite yet.

Actually, I revise the above to question explicitly -- for a developer instrumenting but not yet set up with a trace sink location, what options do we plan to expose for them to be able to, for instance, just hook up all trace data to go to standard out (ideally we expose an event so they can send to whatever out they wish).

Copy link
Member

Choose a reason for hiding this comment

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

This tracer is meant to be used for (unit) testing and definitely not for production. Performance therefore isn't that much of a concern.

Using Concurrent would be nice but it's not enough (as e.g. _baggage is a regular variable that is replaced) so we need this locking logic anyway. Right now it's at least consistent and more similar to what Java has.

Actually, I revise the above to question explicitly -- for a developer instrumenting but not yet set up with a trace sink location, what options do we plan to expose for them to be able to, for instance, just hook up all trace data to go to standard out (ideally we expose an event so they can send to whatever out they wish).

I guess this would be a good fit for a opentracing-contrib project. Something like this shouldn't be part of the core API library as it probably needs a different release cycle etc.

{
CheckForFinished("Adding baggage [{0}:{1}] to already finished span.", key, value);
_context = _context.WithBaggageItem(key, value);
return this;
}
}

public string GetBaggageItem(string key)
{
lock (_lock)
{
return _context.GetBaggageItem(key);
}
}

public void Finish()
{
Finish(DateTimeOffset.UtcNow);
}

public void Finish(DateTimeOffset finishTimestamp)
{
lock (_lock)
{
CheckForFinished("Tried to finish already finished span");
_finishTimestamp = finishTimestamp;
_mockTracer.AppendFinishedSpan(this);
_finished = true;
}
}

private static MockSpanContext FindPreferredParentRef(IList<Reference> references)
{
if (!references.Any())
return null;

// return the context of the parent, if applicable
foreach (var reference in references)
{
if (OpenTracing.References.ChildOf.Equals(reference.ReferenceType))
return reference.Context;
}

// otherwise, return the context of the first reference
return references.First().Context;
}

private static Dictionary<string, string> MergeBaggages(IList<Reference> references)
{
var baggage = new Dictionary<string, string>();
foreach (var reference in references)
{
if (reference.Context.GetBaggageItems() != null)
{
foreach (var bagItem in reference.Context.GetBaggageItems())
{
baggage[bagItem.Key] = bagItem.Value;
}
}
}

return baggage;
}

private void CheckForFinished(string format, params object[] args)
{
if (_finished)
{
var ex = new InvalidOperationException(string.Format(format, args));
_errors.Add(ex);
throw ex;
}
}

public override string ToString()
{
return $"TraceId: {_context.TraceId}, SpanId: {_context.SpanId}, ParentId: {ParentId}, OperationName: {OperationName}";
}

public sealed class LogEntry
{
public DateTimeOffset Timestamp { get; }

public IReadOnlyDictionary<string, object> Fields { get; }

public LogEntry(DateTimeOffset timestamp, IDictionary<string, object> fields)
{
Timestamp = timestamp;
Fields = new ReadOnlyDictionary<string, object>(fields);
}
}

public sealed class Reference : IEquatable<Reference>
{
public MockSpanContext Context { get; }

/// <summary>
/// See <see cref="OpenTracing.References"/>.
/// </summary>
public string ReferenceType { get; }

public Reference(MockSpanContext context, string referenceType)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
ReferenceType = referenceType ?? throw new ArgumentNullException(nameof(referenceType));
}

public override bool Equals(object obj)
{
return Equals(obj as Reference);
}

public bool Equals(Reference other)
{
return other != null &&
EqualityComparer<MockSpanContext>.Default.Equals(Context, other.Context) &&
Copy link
Contributor

Choose a reason for hiding this comment

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

EqualityComparer<MockSpanContext>.Default wow, VS generates weird Equality. They should take a hint from R# :-)

[not asking for a change here]

ReferenceType == other.ReferenceType;
}

public override int GetHashCode()
{
var hashCode = 2083322454;
hashCode = hashCode * -1521134295 + EqualityComparer<MockSpanContext>.Default.GetHashCode(Context);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(ReferenceType);
return hashCode;
}
}
}
}
Loading