├── 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 | }
--------------------------------------------------------------------------------