最近工作过程中需要实现一个日志埋点的功能,采集用户行为及相关行为Log以便后续的报表分析。
首先整理下实现日志埋点必须具备的功能:
1.行为采集注册-
2.行为采集实时写入
3.行为采集异步上传
实现过程中可能会出现的问题:文本文件追加和读取的并发问题。
具体实现:
private static object loker = new object();
private static System.Timers.Timer aTimer = new System.Timers.Timer();
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
private static string file = System.Environment.CurrentDirectory + "Log";
1.注册获取登陆用户的信息,开启异步线程,定时读取本地生成的txt文件上传到数据库,我采用mongoDB存储埋点信息。
public bool RegisterCollect()
{
var response = Register(注册的参数); //读取采集配置信息(是否采集,是否上传,上传频率)
if (response.Sucess)
{
AddActionRecord();
//根据读取到的配置信息 开启异步线程定时读取本地文件
AddActionRecord();
}
//注册成功后判断文件夹路径是否存在,不存在则创建
private CheckDirctory()
{
if (!Directory.Exists(file))
{
Directory.CreateDirectory(file);
}
}
public void AddActionRecord()
{
CheckDirctory()
aTimer.Interval = 60000 * 上传频率;
aTimer.Elapsed += new System.Timers.ElapsedEventHandler(OnTimedEvent);
aTimer.Enabled = true;
}
private void OnTimedEvent(Object source, ElapsedEventArgs e)
{
lock (loker)
{
UploadTxtRecord();
}
}
private void UploadTxtRecord()
{
var query = (from f in Directory.GetFiles(file, "*.txt")
let fi = new FileInfo(f)
orderby fi.CreationTime descending
select fi.FullName).ToList();
var logFilePath = query.FirstOrDefault();
if (query.Count == 0 || FileStatusHelper.IsFileOccupied(logFilePath))
{
return;
}
try
{
LogWriteLock.EnterWriteLock();
FileStream fileStream = new FileStream(logFilePath, FileMode.Open, FileAccess.ReadWrite);
StreamReader reader = new StreamReader(fileStream, Encoding.GetEncoding("UTF-8"));
string line = "";
while ((line = reader.ReadLine()) != null)
{
//调用服务接口,写入埋点信息到数据库中
}
reader.Close();
fileStream.Close();
File.Delete(logFilePath);
}
catch (Exception ex)
{
}
finally
{
LogWriteLock.ExitWriteLock();
}
}
2.实时上传直接调用采集上传的方法,我这里直接调用服务端API方法实现。
public void ActionRecordCollectAsync()
{
//调用服务接口,写入埋点信息到数据库中
}
3.异步上传在需埋点的位置调用埋点的方法,将埋点数据写入本地文本文件中,以便后续的采集上传
public void WriteMessage(string actionRecordJson)
{
try
{
LogWriteLock.EnterWriteLock();
var query = (from f in Directory.GetFiles(file, "*.txt")
let fi = new FileInfo(f)
orderby fi.CreationTime descending
select fi.FullName).ToList();
var logFilePath = query.FirstOrDefault();
if (query.Count == 0)
{
logFilePath = file + "\\" + DateTime.Now.ToString("yyyyMMdd-hhmm") + ".txt";
}
//判断文件是否被占用
if (query.Count > 0 && FileStatusHelper.IsFileOccupied(logFilePath))
{
logFilePath = file + "\\" + DateTime.Now.ToString("yyyyMMdd-hhmm") + ".txt";
}
using (FileStream fs = new FileStream(logFilePath, FileMode.OpenOrCreate, FileAccess.Write))
{
using (StreamWriter sw = new StreamWriter(fs))
{
sw.BaseStream.Seek(0, SeekOrigin.End);
sw.WriteLine(actionRecordJson);
sw.Flush();
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
//退出写入模式,释放资源占用
//注意:一次请求对应一次释放
// 若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]
// 若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]
LogWriteLock.ExitWriteLock();
}
}
并发问题的解决(解决思路):
1.写入的时候,判断文件是否被占用,若被占用,重新创建一个文件写入
2.读取的时候,获取文件夹下所有文件,根据时间进行倒叙排序,读取之后上传数据库删除文件。
public class FileStatusHelper{
[DllImport("kernel32.dll")]
public static extern IntPtr _lopen(string lpPathName, int iReadWrite);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
public const int OF_READWRITE = 2;
public const int OF_SHARE_DENY_NONE = 0x40;
public static readonly IntPtr HFILE_ERROR = new IntPtr(-1);
///
/// 查看文件是否被占用
///
///
///
public static bool IsFileOccupied(string filePath)
{
IntPtr vHandle = _lopen(filePath, OF_READWRITE | OF_SHARE_DENY_NONE);
CloseHandle(vHandle);
return vHandle == HFILE_ERROR ? true : false;
}
}
总结:暂时实现了自己所需的功能,但是因为自己对多线程的薄弱,所以实际并没有经过大量数据和并发情况的考验。大家有时间可以自己拿下来测试一下,欢迎大家反馈改进