├── README.md ├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── .gitignore ├── AppmetrS2S ├── packages.config ├── Persister │ ├── IBatchPersister.cs │ ├── MemoryBatchPersister.cs │ ├── Batch.cs │ └── FileBatchPersister.cs ├── Actions │ ├── Level.cs │ ├── Event.cs │ ├── AppMetrAction.cs │ └── Payment.cs ├── Properties │ └── AssemblyInfo.cs ├── AppMetrTimer.cs ├── HttpRequestService.cs ├── AppmetrS2S.csproj ├── AppMetr.cs └── Utils.cs └── AppmetrS2S.sln /README.md: -------------------------------------------------------------------------------- 1 | # appmetr-s2s-csharp 2 | Server-to-server appmetr event analytics realization 3 | -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/volkovku/appmetr-s2s-csharp/master/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## VS 2 | 3 | */obj/* 4 | */bin/* 5 | 6 | *.suo 7 | 8 | *.sln.DotSettings.user 9 | *.csproj.user 10 | 11 | deploy.sh 12 | 13 | packages -------------------------------------------------------------------------------- /AppmetrS2S/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AppmetrS2S/Persister/IBatchPersister.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Persister 2 | { 3 | using System.Collections.Generic; 4 | using Actions; 5 | 6 | public interface IBatchPersister 7 | { 8 | /// 9 | /// Get the oldest batch from storage, but dont remove it. 10 | /// 11 | Batch GetNext(); 12 | 13 | /// 14 | /// Persist list of events as Batch. 15 | /// 16 | /// actionList list of events. 17 | void Persist(List actionList); 18 | 19 | /// 20 | /// Remove oldest batch from storage. 21 | /// 22 | void Remove(); 23 | } 24 | } -------------------------------------------------------------------------------- /AppmetrS2S/Actions/Level.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Actions 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Runtime.Serialization; 7 | 8 | #endregion 9 | 10 | [DataContract] 11 | public class Level : AppMetrAction 12 | { 13 | private const String ACTION = "trackLevel"; 14 | 15 | [DataMember(Name = "level")] 16 | private int _level; 17 | 18 | protected Level() 19 | { 20 | } 21 | 22 | public Level(int level) : base(ACTION) 23 | { 24 | _level = level; 25 | } 26 | 27 | public override int CalcApproximateSize() 28 | { 29 | return base.CalcApproximateSize() + 4; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /AppmetrS2S/Actions/Event.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Actions 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Runtime.Serialization; 7 | 8 | #endregion 9 | 10 | [DataContract] 11 | public class Event : AppMetrAction 12 | { 13 | private const String ACTION = "trackEvent"; 14 | 15 | [DataMember(Name = "event")] 16 | private String _event; 17 | 18 | protected Event() 19 | { 20 | } 21 | 22 | public Event(string eventName) : base(ACTION) 23 | { 24 | _event = eventName; 25 | } 26 | 27 | public String GetEvent() 28 | { 29 | return _event; 30 | } 31 | 32 | public override int CalcApproximateSize() 33 | { 34 | return base.CalcApproximateSize() + GetStringLength(_event); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /AppmetrS2S/Persister/MemoryBatchPersister.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Persister 2 | { 3 | #region using directives 4 | 5 | using System.Collections.Generic; 6 | using Actions; 7 | 8 | #endregion 9 | 10 | public class MemoryBatchPersister : IBatchPersister 11 | { 12 | private readonly Queue _batchQueue = new Queue(); 13 | private int _batchId = 0; 14 | 15 | public Batch GetNext() 16 | { 17 | lock (_batchQueue) 18 | { 19 | return _batchQueue.Count == 0 ? null : _batchQueue.Peek(); 20 | } 21 | } 22 | 23 | public void Persist(List actionList) 24 | { 25 | lock (_batchQueue) 26 | { 27 | _batchQueue.Enqueue(new Batch(_batchId++, actionList)); 28 | } 29 | } 30 | 31 | public void Remove() 32 | { 33 | lock (_batchQueue) 34 | { 35 | _batchQueue.Dequeue(); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /AppmetrS2S.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppmetrS2S", "AppmetrS2S\AppmetrS2S.csproj", "{4DB6FBE6-3852-4F8C-8327-8C5A835AAD5D}" 5 | EndProject 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{5A0D9EF6-5D7A-4886-9667-CF495ECE3E36}" 7 | ProjectSection(SolutionItems) = preProject 8 | .nuget\NuGet.Config = .nuget\NuGet.Config 9 | .nuget\NuGet.exe = .nuget\NuGet.exe 10 | .nuget\NuGet.targets = .nuget\NuGet.targets 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {4DB6FBE6-3852-4F8C-8327-8C5A835AAD5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {4DB6FBE6-3852-4F8C-8327-8C5A835AAD5D}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {4DB6FBE6-3852-4F8C-8327-8C5A835AAD5D}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {4DB6FBE6-3852-4F8C-8327-8C5A835AAD5D}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | EndGlobal 28 | -------------------------------------------------------------------------------- /AppmetrS2S/Persister/Batch.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Persister 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Runtime.Serialization; 8 | using Actions; 9 | 10 | #endregion 11 | 12 | [DataContract] 13 | [KnownType(typeof(Event))] 14 | [KnownType(typeof(Level))] 15 | [KnownType(typeof(Payment))] 16 | public class Batch 17 | { 18 | [DataMember(Name = "batchId")] 19 | private readonly int _batchId; 20 | 21 | [DataMember(Name = "batch")] 22 | private readonly List _batch; 23 | 24 | private Batch() 25 | { 26 | 27 | } 28 | 29 | public Batch(int batchId, IEnumerable actionList) 30 | { 31 | _batchId = batchId; 32 | _batch = new List(actionList); 33 | } 34 | 35 | public int GetBatchId() 36 | { 37 | return _batchId; 38 | } 39 | 40 | public List GetBatch() 41 | { 42 | return _batch; 43 | } 44 | 45 | public override string ToString() 46 | { 47 | return String.Format("Batch{{events={0}, batchId={1}}}", _batch.Count, _batchId); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /AppmetrS2S/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AppmetrS2S")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("AppmetrS2S")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("594a7c70-c6e3-427b-8eb0-4d09a66e347a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /AppmetrS2S/AppMetrTimer.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Threading; 7 | using log4net; 8 | 9 | #endregion 10 | 11 | public class AppMetrTimer 12 | { 13 | private static readonly ILog Log = LogManager.GetLogger(typeof (AppMetrTimer)); 14 | 15 | private readonly int _period; 16 | private readonly Action _onTimer; 17 | private readonly String _jobName; 18 | 19 | private readonly object _lock = new object(); 20 | private bool _run; 21 | 22 | public AppMetrTimer(int period, Action onTimer, String jobName = "AppMetrTimer") 23 | { 24 | _period = period; 25 | _onTimer = onTimer; 26 | _jobName = jobName; 27 | } 28 | 29 | public void Start() 30 | { 31 | if (Log.IsInfoEnabled) 32 | { 33 | Log.InfoFormat("Start {0} with period {1}", _jobName, _period); 34 | } 35 | 36 | _run = true; 37 | while (_run) 38 | { 39 | bool isTaken = false; 40 | Monitor.Enter(_lock, ref isTaken); 41 | try 42 | { 43 | Monitor.Wait(_lock, _period); 44 | 45 | if (Log.IsInfoEnabled) 46 | { 47 | Log.InfoFormat("{0} triggered", _jobName); 48 | } 49 | _onTimer.Invoke(); 50 | } 51 | catch (ThreadInterruptedException e) 52 | { 53 | Log.WarnFormat("{0} interrupted", _jobName); 54 | _run = false; 55 | } 56 | finally 57 | { 58 | if (isTaken) Monitor.Exit(_lock); 59 | } 60 | } 61 | } 62 | 63 | public void Trigger() 64 | { 65 | bool isTaken = false; 66 | Monitor.Enter(_lock, ref isTaken); 67 | try 68 | { 69 | Monitor.Pulse(_lock); 70 | } 71 | finally 72 | { 73 | if (isTaken) Monitor.Exit(_lock); 74 | } 75 | 76 | } 77 | 78 | public void Stop() 79 | { 80 | if (Log.IsInfoEnabled) 81 | { 82 | Log.InfoFormat("{0} stop triggered", _jobName); 83 | } 84 | 85 | _run = false; 86 | Thread.CurrentThread.Interrupt(); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /AppmetrS2S/Actions/AppMetrAction.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Actions 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Runtime.Serialization; 8 | 9 | #endregion 10 | 11 | [DataContract] 12 | public abstract class AppMetrAction 13 | { 14 | [DataMember(Name = "action")] 15 | private String _action; 16 | 17 | [DataMember(Name = "timestamp")] 18 | private long _timestamp = Utils.GetNowUnixTimestamp(); 19 | 20 | [DataMember(Name = "properties")] 21 | private IDictionary _properties = new Dictionary(); 22 | 23 | [DataMember(Name = "userId")] 24 | private String _userId; 25 | 26 | protected AppMetrAction() 27 | { 28 | } 29 | 30 | protected AppMetrAction(string action) 31 | { 32 | _action = action; 33 | } 34 | 35 | public long GetTimestamp() 36 | { 37 | return _timestamp; 38 | } 39 | 40 | public AppMetrAction SetTimestamp(long timestamp) 41 | { 42 | _timestamp = timestamp; 43 | return this; 44 | } 45 | 46 | public IDictionary GetProperties() 47 | { 48 | return _properties; 49 | } 50 | 51 | public AppMetrAction SetProperties(IDictionary properties) 52 | { 53 | _properties = properties; 54 | return this; 55 | } 56 | 57 | public String GetUserId() 58 | { 59 | return _userId; 60 | } 61 | 62 | public AppMetrAction SetUserId(String userId) 63 | { 64 | _userId = userId; 65 | return this; 66 | } 67 | 68 | //http://codeblog.jonskeet.uk/2011/04/05/of-memory-and-strings/ 69 | public virtual int CalcApproximateSize() 70 | { 71 | int size = 40 + (40 * _properties.Count); //40 - Map size and 40 - each entry overhead 72 | 73 | size += GetStringLength(_action); 74 | size += GetStringLength(Convert.ToString(_timestamp)); 75 | size += GetStringLength(_userId); 76 | 77 | foreach (KeyValuePair pair in _properties) { 78 | size += GetStringLength(pair.Key); 79 | size += GetStringLength(pair.Value != null ? Convert.ToString(pair.Value) : null); //toString because sending this object via json 80 | } 81 | 82 | return 8 + size + 8; //8 - object header 83 | } 84 | 85 | protected int GetStringLength(String str) 86 | { 87 | return str == null ? 0 : str.Length * 2 + 26; //24 - String object size, 16 - char[] 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /AppmetrS2S/Actions/Payment.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Actions 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Runtime.Serialization; 7 | 8 | #endregion 9 | 10 | [DataContract] 11 | public class Payment : AppMetrAction 12 | { 13 | private const String ACTION = "trackPayment"; 14 | 15 | [DataMember(Name = "orderId")] 16 | private String _orderId; 17 | 18 | [DataMember(Name = "transactionId")] 19 | private String _transactionId; 20 | 21 | [DataMember(Name = "processor")] 22 | private String _processor; 23 | 24 | [DataMember(Name = "psUserSpentCurrencyCode")] 25 | private String _psUserSpentCurrencyCode; 26 | 27 | [DataMember(Name = "psUserSpentCurrencyAmount")] 28 | private String _psUserSpentCurrencyAmount; 29 | 30 | [DataMember(Name = "appCurrencyCode")] 31 | private String _appCurrencyCode; 32 | 33 | [DataMember(Name = "appCurrencyAmount")] 34 | private String _appCurrencyAmount; 35 | 36 | protected Payment() 37 | { 38 | } 39 | 40 | public Payment(String orderId, 41 | String transactionId, 42 | String processor, 43 | String psUserSpentCurrencyCode, 44 | String psUserSpentCurrencyAmount) 45 | : this(orderId, transactionId, processor, psUserSpentCurrencyCode, psUserSpentCurrencyAmount, null, null) 46 | { 47 | } 48 | 49 | public Payment(String orderId, 50 | String transactionId, 51 | String processor, 52 | String psUserSpentCurrencyCode, 53 | String psUserSpentCurrencyAmount, 54 | String appCurrencyCode, 55 | String appCurrencyAmount) : base(ACTION) 56 | { 57 | _orderId = orderId; 58 | _transactionId = transactionId; 59 | _processor = processor; 60 | _psUserSpentCurrencyCode = psUserSpentCurrencyCode; 61 | _psUserSpentCurrencyAmount = psUserSpentCurrencyAmount; 62 | _appCurrencyCode = appCurrencyCode; 63 | _appCurrencyAmount = appCurrencyAmount; 64 | } 65 | 66 | public String GetOrderId() 67 | { 68 | return _orderId; 69 | } 70 | 71 | public String GetTransactionId() 72 | { 73 | return _transactionId; 74 | } 75 | 76 | public String GetProcessor() 77 | { 78 | return _processor; 79 | } 80 | 81 | public String GetPsUserSpentCurrencyCode() 82 | { 83 | return _psUserSpentCurrencyCode; 84 | } 85 | 86 | public String GetPsUserSpentCurrencyAmount() 87 | { 88 | return _psUserSpentCurrencyAmount; 89 | } 90 | 91 | public String GetAppCurrencyCode() 92 | { 93 | return _appCurrencyCode; 94 | } 95 | 96 | public String GetAppCurrencyAmount() 97 | { 98 | return _appCurrencyAmount; 99 | } 100 | 101 | public override int CalcApproximateSize() 102 | { 103 | return base.CalcApproximateSize() 104 | + GetStringLength(_orderId) 105 | + GetStringLength(_transactionId) 106 | + GetStringLength(_processor) 107 | + GetStringLength(_psUserSpentCurrencyCode) 108 | + GetStringLength(_psUserSpentCurrencyAmount) 109 | + GetStringLength(_appCurrencyCode) 110 | + GetStringLength(_appCurrencyAmount); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /AppmetrS2S/HttpRequestService.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO.Compression; 8 | using System.Net; 9 | using System.Runtime.Serialization; 10 | using System.Runtime.Serialization.Json; 11 | using System.Text; 12 | using System.Web; 13 | using log4net; 14 | using Persister; 15 | 16 | #endregion 17 | 18 | internal class HttpRequestService 19 | { 20 | private static readonly ILog Log = LogManager.GetLogger(typeof (HttpRequestService)); 21 | 22 | private const String ServerMethodName = "server.trackS2S"; 23 | 24 | public static bool SendRequest(String httpURL, String token, Batch batch) 25 | { 26 | var @params = new Dictionary(2); 27 | @params.Add("method", ServerMethodName); 28 | @params.Add("token", token); 29 | @params.Add("timestamp", Convert.ToString(Utils.GetNowUnixTimestamp())); 30 | 31 | var request = (HttpWebRequest) WebRequest.Create(httpURL + "?" + MakeQueryString(@params)); 32 | request.Method = "POST"; 33 | request.ContentType = "application/octet-stream"; 34 | 35 | using (var stream = request.GetRequestStream()) 36 | using (var deflateStream = new DeflateStream(stream, CompressionLevel.Optimal)) 37 | { 38 | Utils.WriteBatch(deflateStream, batch); 39 | } 40 | 41 | try 42 | { 43 | var response = (HttpWebResponse) request.GetResponse(); 44 | 45 | var serializer = new DataContractJsonSerializer(typeof (JsonResponseWrapper)); 46 | var jsonResponse = (JsonResponseWrapper) serializer.ReadObject(response.GetResponseStream()); 47 | 48 | if (jsonResponse.Error != null) 49 | { 50 | Log.ErrorFormat("Server return error with message: {0}", jsonResponse.Error.Message); 51 | } 52 | else if (jsonResponse.Response != null && "OK".Equals(jsonResponse.Response.Status)) 53 | { 54 | return true; 55 | } 56 | } 57 | catch (Exception e) 58 | { 59 | Log.Error("Send error", e); 60 | } 61 | 62 | return false; 63 | } 64 | 65 | private static String MakeQueryString(Dictionary @params) 66 | { 67 | StringBuilder queryBuilder = new StringBuilder(); 68 | 69 | int paramCount = 0; 70 | foreach (KeyValuePair param in @params) 71 | { 72 | if (param.Value != null) 73 | { 74 | paramCount++; 75 | if (paramCount > 1) 76 | { 77 | queryBuilder.Append("&"); 78 | } 79 | 80 | queryBuilder.Append(param.Key).Append("=").Append(HttpUtility.UrlEncode(param.Value, Encoding.UTF8)); 81 | } 82 | } 83 | return queryBuilder.ToString(); 84 | } 85 | } 86 | 87 | [DataContract] 88 | [KnownType(typeof (ErrorWrapper))] 89 | [KnownType(typeof (ResponseWrapper))] 90 | internal class JsonResponseWrapper 91 | { 92 | [DataMember(Name = "error")] public ErrorWrapper Error; 93 | [DataMember(Name = "response")] public ResponseWrapper Response; 94 | } 95 | 96 | [DataContract] 97 | internal class ErrorWrapper 98 | { 99 | [DataMember(Name = "message", IsRequired = true)] public String Message; 100 | } 101 | 102 | [DataContract] 103 | internal class ResponseWrapper 104 | { 105 | [DataMember(Name = "status", IsRequired = true)] public String Status; 106 | } 107 | } -------------------------------------------------------------------------------- /AppmetrS2S/AppmetrS2S.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {4DB6FBE6-3852-4F8C-8327-8C5A835AAD5D} 8 | Library 9 | Properties 10 | AppmetrS2S 11 | AppmetrS2S 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | false 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | ..\packages\log4net.2.0.3\lib\net40-full\log4net.dll 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 75 | 76 | 77 | 78 | 85 | -------------------------------------------------------------------------------- /AppmetrS2S/AppMetr.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | using Actions; 9 | using log4net; 10 | using Persister; 11 | 12 | #endregion 13 | 14 | public class AppMetr 15 | { 16 | private static readonly ILog Log = LogManager.GetLogger(typeof (AppMetr)); 17 | 18 | private String _token; 19 | private String _url; 20 | private IBatchPersister _batchPersister; 21 | 22 | private bool _stopped = false; 23 | private readonly List _actionList = new List(); 24 | 25 | private readonly object _flushLock = new object(); 26 | private readonly object _uploadLock = new object(); 27 | 28 | private readonly AppMetrTimer _flushTimer; 29 | private readonly AppMetrTimer _uploadTimer; 30 | 31 | private int _eventSize = 0; 32 | private const int MaxEventsSize = 1024*500*20;//2 MB 33 | 34 | private const int MillisPerMinute = 1000*60; 35 | private const int FlushPeriod = MillisPerMinute/2; 36 | private const int UploadPeriod = MillisPerMinute/2; 37 | 38 | public AppMetr(String token, String url, IBatchPersister batchPersister = null) 39 | { 40 | Log.InfoFormat("Start Appmetr for token={0}, url={1}", token, url); 41 | 42 | _token = token; 43 | _url = url; 44 | _batchPersister = batchPersister ?? new MemoryBatchPersister(); 45 | 46 | _flushTimer = new AppMetrTimer(FlushPeriod, Flush, "FlushJob"); 47 | new Thread(_flushTimer.Start).Start(); 48 | 49 | _uploadTimer = new AppMetrTimer(UploadPeriod, Upload, "UploadJob"); 50 | new Thread(_uploadTimer.Start).Start(); 51 | } 52 | 53 | public void Track(AppMetrAction action) 54 | { 55 | if (_stopped) 56 | { 57 | throw new Exception("Trying to track after stop!"); 58 | } 59 | 60 | try 61 | { 62 | bool flushNeeded; 63 | lock (_actionList) 64 | { 65 | Interlocked.Add(ref _eventSize, action.CalcApproximateSize()); 66 | _actionList.Add(action); 67 | 68 | flushNeeded = IsNeedToFlush(); 69 | } 70 | 71 | if (flushNeeded) 72 | { 73 | _flushTimer.Trigger(); 74 | } 75 | } 76 | catch (Exception e) 77 | { 78 | Log.Error("Track failed", e); 79 | } 80 | } 81 | 82 | public void Stop() 83 | { 84 | Log.Info("Stop appmetr"); 85 | 86 | _stopped = true; 87 | 88 | lock (_uploadLock) 89 | { 90 | _uploadTimer.Stop(); 91 | } 92 | 93 | lock (_flushLock) 94 | { 95 | _flushTimer.Stop(); 96 | } 97 | 98 | Flush(); 99 | } 100 | 101 | private bool IsNeedToFlush() 102 | { 103 | return _eventSize >= MaxEventsSize; 104 | } 105 | 106 | private void Flush() 107 | { 108 | lock (_flushLock) 109 | { 110 | if (Log.IsDebugEnabled) 111 | { 112 | Log.DebugFormat("Flush started for {0} actions", _actionList.Count); 113 | } 114 | 115 | List copyActions; 116 | lock (_actionList) 117 | { 118 | copyActions = new List(_actionList); 119 | _actionList.Clear(); 120 | _eventSize = 0; 121 | } 122 | 123 | if (copyActions.Count > 0) 124 | { 125 | _batchPersister.Persist(copyActions); 126 | _uploadTimer.Trigger(); 127 | } 128 | else 129 | { 130 | Log.Info("Nothing to flush"); 131 | } 132 | } 133 | } 134 | 135 | private void Upload() 136 | { 137 | lock (_uploadLock) 138 | { 139 | if (Log.IsDebugEnabled) 140 | { 141 | Log.Debug("Upload started"); 142 | } 143 | 144 | Batch batch; 145 | int uploadedBatchCounter = 0; 146 | int allBatchCounter = 0; 147 | while ((batch = _batchPersister.GetNext()) != null) 148 | { 149 | allBatchCounter++; 150 | 151 | if (HttpRequestService.SendRequest(_url, _token, batch)) 152 | { 153 | _batchPersister.Remove(); 154 | uploadedBatchCounter++; 155 | 156 | if (Log.IsDebugEnabled) 157 | { 158 | Log.DebugFormat("Batch {0} successfully uploaded", batch.GetBatchId()); 159 | } 160 | } 161 | else 162 | { 163 | Log.ErrorFormat("Error while upload batch {0}", batch.GetBatchId()); 164 | break; 165 | } 166 | } 167 | 168 | if (Log.IsDebugEnabled) 169 | { 170 | Log.DebugFormat("{0} from {1} batches uploaded", uploadedBatchCounter, allBatchCounter); 171 | } 172 | } 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /AppmetrS2S/Persister/FileBatchPersister.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S.Persister 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.IO.Compression; 9 | using System.Linq; 10 | using System.Threading; 11 | using Actions; 12 | using log4net; 13 | 14 | #endregion 15 | 16 | public class FileBatchPersister : IBatchPersister 17 | { 18 | private static readonly ILog Log = LogManager.GetLogger(typeof(FileBatchPersister)); 19 | 20 | private readonly ReaderWriterLock _lock = new ReaderWriterLock(); 21 | 22 | private const String BatchFilePrefix = "batchFile#"; 23 | 24 | private readonly String _filePath; 25 | private readonly String _batchIdFile; 26 | 27 | private Queue _fileIds; 28 | private int _lastBatchId; 29 | 30 | public FileBatchPersister(String filePath) 31 | { 32 | if (!Directory.Exists(filePath)) 33 | { 34 | Directory.CreateDirectory(filePath); 35 | } 36 | 37 | _filePath = filePath; 38 | _batchIdFile = Path.Combine(Path.GetFullPath(_filePath), "lastBatchId"); 39 | 40 | InitPersistedFiles(); 41 | } 42 | 43 | public Batch GetNext() 44 | { 45 | _lock.AcquireReaderLock(-1); 46 | try 47 | { 48 | if (_fileIds.Count == 0) return null; 49 | 50 | int batchId = _fileIds.Peek(); 51 | string batchFilePath = Path.Combine(_filePath, GetBatchFileName(batchId)); 52 | 53 | if (File.Exists(batchFilePath)) 54 | { 55 | using (var fileStream = new FileStream(batchFilePath, FileMode.Open)) 56 | using (var deflateStream = new DeflateStream(fileStream, CompressionMode.Decompress)) 57 | { 58 | Batch batch; 59 | if (Utils.TryReadBatch(deflateStream, out batch)) 60 | { 61 | return batch; 62 | } 63 | } 64 | 65 | if (Log.IsErrorEnabled) 66 | { 67 | Log.ErrorFormat("Error while reading batch for id {0}", batchId); 68 | } 69 | } 70 | 71 | return null; 72 | } 73 | finally 74 | { 75 | _lock.ReleaseReaderLock(); 76 | } 77 | } 78 | 79 | public void Persist(List actions) 80 | { 81 | _lock.AcquireWriterLock(-1); 82 | 83 | string batchFilePath = Path.Combine(_filePath, GetBatchFileName(_lastBatchId)); 84 | try 85 | { 86 | using (var fileStream = new FileStream(batchFilePath, FileMode.CreateNew)) 87 | using (var deflateStream = new DeflateStream(fileStream, CompressionLevel.Optimal)) 88 | { 89 | if (Log.IsDebugEnabled) 90 | { 91 | Log.DebugFormat("Persis batch {0}", _lastBatchId); 92 | } 93 | Utils.WriteBatch(deflateStream, new Batch(_lastBatchId, actions)); 94 | _fileIds.Enqueue(_lastBatchId); 95 | 96 | UpdateLastBatchId(); 97 | } 98 | } 99 | catch (Exception e) 100 | { 101 | if (Log.IsErrorEnabled) 102 | { 103 | Log.Error("Error in batch persist", e); 104 | } 105 | 106 | if (File.Exists(batchFilePath)) 107 | { 108 | File.Delete(batchFilePath); 109 | } 110 | } 111 | finally 112 | { 113 | _lock.ReleaseWriterLock(); 114 | } 115 | } 116 | 117 | public void Remove() 118 | { 119 | _lock.AcquireWriterLock(-1); 120 | 121 | try 122 | { 123 | if (Log.IsDebugEnabled) 124 | { 125 | Log.DebugFormat("Remove file with index {0}", _fileIds.Peek()); 126 | } 127 | 128 | File.Delete(Path.Combine(_filePath, GetBatchFileName(_fileIds.Dequeue()))); 129 | } 130 | finally 131 | { 132 | _lock.ReleaseWriterLock(); 133 | } 134 | } 135 | 136 | private void InitPersistedFiles() 137 | { 138 | String[] files = Directory.GetFiles(_filePath, String.Format("{0}*", BatchFilePrefix)); 139 | 140 | var ids = 141 | files.Select(file => Convert.ToInt32(Path.GetFileName(file).Substring(BatchFilePrefix.Length))).ToList(); 142 | ids.Sort(); 143 | 144 | String batchId; 145 | if (File.Exists(_batchIdFile) && (batchId = File.ReadAllText(_batchIdFile)).Length > 0) 146 | { 147 | _lastBatchId = Convert.ToInt32(batchId); 148 | } 149 | else if (ids.Count > 0) 150 | { 151 | _lastBatchId = ids[ids.Count - 1]; 152 | } 153 | else 154 | { 155 | _lastBatchId = 0; 156 | } 157 | 158 | Log.InfoFormat("Init lastBatchId with {0}", _lastBatchId); 159 | 160 | if (Log.IsInfoEnabled) 161 | { 162 | Log.InfoFormat("Load {0} files from disk", ids.Count); 163 | if (ids.Count > 0) 164 | { 165 | Log.InfoFormat("First batch id is {0}, last is {1}", ids[0], ids[ids.Count - 1]); 166 | } 167 | } 168 | 169 | _fileIds = new Queue(ids); 170 | } 171 | 172 | private void UpdateLastBatchId() 173 | { 174 | _lastBatchId++; 175 | File.WriteAllText(_batchIdFile, Convert.ToString(_lastBatchId)); 176 | } 177 | 178 | private String GetBatchFileName(int batchId) 179 | { 180 | return String.Format("{0}{1:D11}", BatchFilePrefix, batchId); 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /AppmetrS2S/Utils.cs: -------------------------------------------------------------------------------- 1 | namespace AppmetrS2S 2 | { 3 | #region using directives 4 | 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Reflection; 10 | using System.Runtime.Serialization; 11 | using System.Text; 12 | using System.Web.Script.Serialization; 13 | using Actions; 14 | using Persister; 15 | 16 | #endregion 17 | 18 | internal class Utils 19 | { 20 | private static JavaScriptSerializer serializer; 21 | 22 | static Utils() 23 | { 24 | serializer = new JavaScriptSerializer(); 25 | serializer.RegisterConverters(new[] {new BatchJsonConverter()}); 26 | } 27 | 28 | public static long GetNowUnixTimestamp() 29 | { 30 | return (long) (DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalMilliseconds; 31 | } 32 | 33 | public static void WriteBatch(Stream stream, Batch batch) 34 | { 35 | var json = serializer.Serialize(batch); 36 | byte[] data = Encoding.UTF8.GetBytes(json); 37 | stream.Write(data, 0, data.Length); 38 | } 39 | 40 | public static bool TryReadBatch(Stream stream, out Batch batch) 41 | { 42 | try 43 | { 44 | batch = serializer.Deserialize(new StreamReader(stream).ReadToEnd()); 45 | return true; 46 | } 47 | catch (Exception) 48 | { 49 | batch = null; 50 | return false; 51 | } 52 | } 53 | 54 | /// 55 | /// If you want to add new Object types for this serializer, you should add this type to , and write a little bit of code in method 56 | /// 57 | internal class BatchJsonConverter : JavaScriptConverter 58 | { 59 | private const string TypeFieldName = "___type"; 60 | //We couldn't use __ prefix, cause this prefix are used for DataContractSerializer and Deserialize method throw Exception 61 | 62 | public override object Deserialize(IDictionary dictionary, Type type, 63 | JavaScriptSerializer serializer) 64 | { 65 | return ConvertDictionaryToObject(dictionary, type); 66 | } 67 | 68 | public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) 69 | { 70 | if (ReferenceEquals(obj, null)) return null; 71 | 72 | Type objType = obj.GetType(); 73 | if (Attribute.GetCustomAttribute(objType, typeof (DataContractAttribute)) == null) return null; 74 | 75 | var result = new Dictionary() {{TypeFieldName, objType.AssemblyQualifiedName}}; 76 | 77 | ProcessFieldsAndProperties(obj, 78 | (attribute, info) => result.Add(attribute.Name, info.GetValue(obj)), 79 | (attribute, info) => result.Add(attribute.Name, info.GetValue(obj))); 80 | 81 | return result; 82 | } 83 | 84 | public override IEnumerable SupportedTypes 85 | { 86 | get { return new[] {typeof (Batch), typeof (AppMetrAction)}; } 87 | } 88 | 89 | private static object ConvertDictionaryToObject(IDictionary dictionary, Type type) 90 | { 91 | var objType = GetSerializedObjectType(dictionary); 92 | if (objType == null) return null; 93 | 94 | ConstructorInfo constructor = objType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, 95 | null, new Type[0], null); 96 | var result = constructor.Invoke(null); 97 | 98 | Action action = (attribute, info) => 99 | { 100 | Type fieldType = info is FieldInfo 101 | ? (info as FieldInfo).FieldType 102 | : info is PropertyInfo ? (info as PropertyInfo).PropertyType : null; 103 | MethodInfo setValue = info.GetType() 104 | .GetMethod("SetValue", new Type[] {typeof (object), typeof (object)}); 105 | 106 | if (fieldType == null || setValue == null) return; 107 | 108 | object value = GetValue(dictionary, attribute.Name); 109 | 110 | if (typeof (ICollection).IsAssignableFrom(fieldType)) 111 | { 112 | var serializedActions = value as ArrayList; 113 | 114 | if (serializedActions != null) 115 | { 116 | var actions = (ICollection) Activator.CreateInstance(fieldType); 117 | foreach (var val in serializedActions) 118 | { 119 | if (val is IDictionary) 120 | actions.Add( 121 | (AppMetrAction) 122 | ConvertDictionaryToObject(val as IDictionary, 123 | GetSerializedObjectType(dictionary))); 124 | } 125 | setValue.Invoke(info, new object[] {result, actions}); 126 | } 127 | } 128 | else 129 | { 130 | setValue.Invoke(info, new object[] {result, value}); 131 | } 132 | }; 133 | 134 | ProcessFieldsAndProperties(result, 135 | action, 136 | action); 137 | 138 | return result; 139 | } 140 | 141 | private static Type GetSerializedObjectType(IDictionary dictionary) 142 | { 143 | object typeName; 144 | if (!dictionary.TryGetValue(TypeFieldName, out typeName) || typeName as string == null) 145 | return null; 146 | 147 | return Type.GetType(typeName as string); 148 | } 149 | 150 | private static object GetValue(IDictionary dictionary, string key) 151 | { 152 | object value; 153 | dictionary.TryGetValue(key, out value); 154 | 155 | return value; 156 | } 157 | 158 | private static void ProcessFieldsAndProperties(object obj, 159 | Action fieldProcessor, 160 | Action propertiesProcessor) 161 | { 162 | Type objType = obj.GetType(); 163 | 164 | 165 | const BindingFlags bindingFlags = 166 | BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public | 167 | BindingFlags.NonPublic; 168 | while (!(typeof (object) == objType)) 169 | { 170 | foreach (FieldInfo field in objType.GetFields(bindingFlags)) 171 | { 172 | var dataMemberAttribute = 173 | (DataMemberAttribute) field.GetCustomAttribute(typeof (DataMemberAttribute)); 174 | if (dataMemberAttribute != null) 175 | { 176 | fieldProcessor.Invoke(dataMemberAttribute, field); 177 | } 178 | } 179 | 180 | foreach (PropertyInfo property in objType.GetProperties(bindingFlags)) 181 | { 182 | var dataMemberAttribute = 183 | (DataMemberAttribute) property.GetCustomAttribute(typeof (DataMemberAttribute)); 184 | if (dataMemberAttribute != null) 185 | { 186 | propertiesProcessor.Invoke(dataMemberAttribute, property); 187 | } 188 | } 189 | 190 | objType = objType.BaseType; 191 | } 192 | } 193 | } 194 | } 195 | } --------------------------------------------------------------------------------