【求指点】写日志类

不胖到130不改名字 2019-09-18 02:25:48
自己写了一个日志类
用是可以用
就是不知道哪里有需要优化的
还有,现有写法有内存泄露的可能吗?



public class LogInfo
{
public string AddTime;
public object Content;
public string Type;

public LogInfo(string addTime, object content, string type)
{
AddTime = addTime;
Content = content;
Type = type;
}
}

interface ILogger
{
void Warn(object msg);
void Info(object msg);
void Debug(object msg);
void Error(object msg);
}

public class SimpleLogger : ILogger
{
//貌似ConcurrentQueue在framework低版本有问题,使用StrongBox在出列后标记其值为NULL
private ConcurrentQueue<StrongBox<LogInfo>> _queue = new ConcurrentQueue<StrongBox<LogInfo>>();

private readonly string _filePath = $@"{AppDomain.CurrentDomain.BaseDirectory}\Logs\";
private bool _bThreadState = true;

//使用Lazy创建线程安全单例模式
//如果不用Lazy,则需要lock来判断对象是否需要实例化
private static readonly Lazy<SimpleLogger> _instance = new Lazy<SimpleLogger>(() => new SimpleLogger());
public static SimpleLogger Instance => _instance.Value;

private SimpleLogger()
{
Task.Factory.StartNew(HandleLogQueue);
}

/// <summary>
/// 关闭日志队列线程
/// </summary>
public void CloseLogThread()
{
_bThreadState = false;
}

/// <summary>
/// 判断日志队列是否为空
/// </summary>
/// <returns></returns>
public bool QueueIsEmpty()
{
return _queue.IsEmpty;
}

public void Debug(object msg)
{
_queue.Enqueue(new StrongBox<LogInfo>(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Debug")));
}

public void Warn(object msg)
{
_queue.Enqueue(new StrongBox<LogInfo>(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Warn")));
}

public void Info(object msg)
{
_queue.Enqueue(new StrongBox<LogInfo>(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Info")));
}

public void Error(object msg)
{
_queue.Enqueue(new StrongBox<LogInfo>(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Error")));
}

private void HandleLogQueue()
{
try
{
while (true)
{
if (_queue.IsEmpty)
{
if (!_bThreadState)
{
break;
}

Thread.Sleep(10);
}
else if (_queue.TryDequeue(out StrongBox<LogInfo> logInfo))
{
WriteLine(logInfo.Value.Content, logInfo.Value.Type, logInfo.Value.AddTime);
logInfo.Value = null;
}
}
}
catch
{
// ignored
}
}

private void WriteLine(object obj, string category, string time)
{
string path = $@"{_filePath}\{DateTime.Now.ToString("yyyyMMdd")}\";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}

//File.AppendAllText(path + $"Log-{category}.txt", Format(obj, category, time));
using (StreamWriter sw = new StreamWriter(path + $"Log-{category}.txt", true, Encoding.UTF8))
{
sw.WriteLine(Format(obj, category, time));
}
}

private string Format(object obj, string category, string time)
{
StringBuilder builder = new StringBuilder();
builder.AppendFormat("{0} ", time);
if (!string.IsNullOrEmpty(category))
{
builder.AppendFormat("[{0}]", category);
}

if (obj is Exception)
{
var ex = obj as Exception;
builder.Append(ex.Message + "\r\n");
builder.Append(ex.StackTrace + "\r\n");
}
else
{
builder.Append(obj + "\r\n");
}

return builder.ToString();
}
}
...全文
1935 30 打赏 收藏 转发到动态 举报
写回复
用AI写文章
30 条回复
切换为时间正序
请发表友善的回复…
发表回复
「已注销」 2019-09-21
  • 打赏
  • 举报
回复
完全不需要想内存泄漏好吧
weixin_45666376 2019-09-20
  • 打赏
  • 举报
回复
mark mark
秋的红果实 2019-09-19
  • 打赏
  • 举报
回复
不知道lz看我#11写的没 你的写法,至少Task.Factory.StartNew(HandleLogQueue);=>Task.Factory.StartNew(HandleLogQueue,_cancellToken); HandleLogQueue方法里面要判断令牌请求的,否则,令牌没用 关键是你打算用令牌取消什么,日志写了一半,你要强行取消吗? 猜测你的意思是:当队列空时,取消线程

private void HandleLogQueue()
        {
            try
            {
                foreach (var logInfo in _queue.GetConsumingEnumerable(_cancellToken))
                {
                    WriteLine(logInfo.Content, logInfo.Type, logInfo.AddTime, logInfo.Ex);
                }

                if(_queue.QueueIsEmpty())
                {
                    _cancellationTokenSource.Cancel();
                }

            }
            catch
            {
                // ignored
            }
        }
  • 打赏
  • 举报
回复
不知道是多大的访问量,我以前接触运营商省级官方网站都是iis日志跟踪做访问分析的,也不知道是不是省级运营商这种量级不够看啊。
by_封爱 版主 2019-09-19
  • 打赏
  • 举报
回复
所以,以后我绝对不会打开IIS日志..nginx日志 以及写txt这种东西... 流量大的时候 这些东西 能抗住.. 但是日志这玩意 真扛不住.没用的就都关了吧...
by_封爱 版主 2019-09-19
  • 打赏
  • 举报
回复
相信我 只要是往硬盘上这么写 都不是一个好的解决方案... 我之前用log4.net 或者线程池 你知道是什么效果吗? 日志并发有点大. 5台服务器瞬间全满.. 网站全瘫痪.. 最后没办法,日志关了 网站才能正常运行.. 只要打开 流量大时候 基本就是瞬间阵亡.. 后来换了一种别的方式..写日志的时候 就丢给redis的队列. 然后做了一个exe去消费. 外挂mysql. 消费10条 组成一个insert log values ()()()()()()()()() 然后使用ado执行插入.. 采用MyIsam,速度还可以 基本上一秒可以插入1W条左右...
jeson_86 2019-09-19
  • 打赏
  • 举报
回复
顺便写啊,想怎么写怎么写
weixin_45661246 2019-09-19
  • 打赏
  • 举报
回复
这个还真不找到
XBodhi. 2019-09-19
  • 打赏
  • 举报
回复
nlog 或 log4net 都可以,如果你要自己些,推荐你 自己实现一个 附加类到 exception 中。大部分其实还是只捕获 异常的日志。
正怒月神 2019-09-19
  • 打赏
  • 举报
回复
我用习惯了 log4net 。 其实要看有没有问题。 最简单的就是你开10个线程,去书写文件,写1小时看看结果嘛
  • 打赏
  • 举报
回复
1、使用异步写入日志行功能。

            using (var sw = new System.IO.StreamWriter(path + $"Log-{category}.txt", true, Encoding.UTF8))
            {
                sw.WriteLineAsync(Format(content, category, time, ex));
            }
2、没有数据库等写入需求,完全可以抛弃队列、多线程等操作,只需要在发成错误的时候委托到异步写入方法中。 当然如果你担心长时间文件锁定造成的日志丢失等情况,可以在文件写入这里包裹try做异常处理。 这样做之后你将把文件写入等功能交到.net控制的异步写入,从而解决了你大量的队列、线程等代码构造的非阻塞写入。日志写入只要保证完整写入,并不需要保证完全的顺序一致性。
github_36000833 2019-09-19
  • 打赏
  • 举报
回复
引用 13 楼 不胖到130不改名字 的回复:
...
这样应该是写一次打开一次文件吧
楼上有人说对文件进行独占,独占后文件就会被锁了吧,我不想文件被锁,就算程序在写日志,我也能直接打开txt文件查看日志
这样应该怎么优化好?


以练手为主要目的。如果商用我还是倾向于用第三方库。
至于文件独占写,它一般不是问题,比如纪事本notepad.exe或notepad++都能打开读。

new StreamWriter(filename, true, Encoding.UTF8; // 纪事本notepad.exe或notepad++都能打开读
new FileStream(filename, FileMode.Append); // 纪事本notepad.exe或notepad++都能打开读
new FileStream(filename, FileMode.Append, FileAccess.Write, FileShare.None); // 这样notepad才不能打开

  • 打赏
  • 举报
回复
根据大佬们的回答后修改如下, 1.取消StrongBox 2.修改接口函数 3.使用BlockingCollection替代ConcurrentQueue 4.取消while-sleep处理模式 现在不解的是,写日志时我这么写的

            using (var sw = new StreamWriter(path + $"Log-{category}.txt", true, Encoding.UTF8))
            {
                sw.WriteLine(Format(content, category, time, ex));
            }
这样应该是写一次打开一次文件吧 楼上有人说对文件进行独占,独占后文件就会被锁了吧,我不想文件被锁,就算程序在写日志,我也能直接打开txt文件查看日志 这样应该怎么优化好? 下面是修改后代码

    public class LogInfo
    {
        public string AddTime;
        public string Content;
        public string Type;
        public Exception Ex;

        public LogInfo(string addTime, string content, string type, Exception ex = null)
        {
            AddTime = addTime;
            Content = content;
            Type = type;
            Ex = ex;
        }
    }

    interface ILogger
    {
        void Warn(string msg);
        void Info(string msg);
        void Debug(string msg);
        void Error(string msg);
        void Error(Exception ex, string msg);
    }

    /// <summary>
    /// 支持多线程写日志
    /// </summary>
    public class SimpleLogger : ILogger
    {
        private BlockingCollection<LogInfo> _queue = new BlockingCollection<LogInfo>();
        private readonly string _filePath = $@"{AppDomain.CurrentDomain.BaseDirectory}\Logs\";
        private CancellationTokenSource _cancellationTokenSource;
        private CancellationToken _cancellToken;

        //使用Lazy创建线程安全单例模式
        //如果不用Lazy,则需要lock来判断对象是否需要实例化
        private static Lazy<SimpleLogger> _instance = new Lazy<SimpleLogger>(() => new SimpleLogger());
        public static SimpleLogger Instance => _instance?.Value;

        private SimpleLogger()
        {
            _cancellationTokenSource = new CancellationTokenSource();
            _cancellToken = _cancellationTokenSource.Token;
            Task.Factory.StartNew(HandleLogQueue);
        }

        /// <summary>
        /// 关闭日志队列线程
        /// </summary>
        public void CloseLogThread()
        {
            _cancellationTokenSource.Cancel();
        }

        /// <summary>
        /// 判断日志队列是否为空
        /// </summary>
        /// <returns></returns>
        public bool QueueIsEmpty()
        {
            return _queue.IsCompleted;
        }

        public int Count()
        {
            return _queue.Count;
        }

        public void Debug(string msg)
        {
            _queue?.TryAdd(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Debug"));
        }

        public void Warn(string msg)
        {
            _queue?.TryAdd(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Warn"));
        }

        public void Info(string msg)
        {
            _queue?.TryAdd(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Info"));
        }

        public void Error(string msg)
        {
            _queue?.TryAdd(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Error"));
        }

        public void Error(Exception ex, string msg = "")
        {
            _queue?.TryAdd(new LogInfo(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), msg, "Error", ex));
        }

        private void HandleLogQueue()
        {
            try
            {
                foreach (var logInfo in _queue.GetConsumingEnumerable(_cancellToken))
                {
                    WriteLine(logInfo.Content, logInfo.Type, logInfo.AddTime, logInfo.Ex);
                }
            }
            catch
            {
                // ignored
            }
        }

        private void WriteLine(string content, string category, string time, Exception ex = null)
        {
            var path = $@"{_filePath}\{DateTime.Now.ToString("yyyyMMdd")}\";
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }

            using (var sw = new StreamWriter(path + $"Log-{category}.txt", true, Encoding.UTF8))
            {
                sw.WriteLine(Format(content, category, time, ex));
            }
        }

        private string Format(string content, string category, string time, Exception ex = null)
        {
            var builder = new StringBuilder();
            builder.AppendFormat("{0} ", time);
            if (!string.IsNullOrEmpty(category))
            {
                builder.AppendFormat("[{0}]", category);
            }

            if (content != "")
            {
                builder.Append(content + "\r\n");
            }

            if (ex != null)
            {
                builder.Append(ex.Message + "\r\n");
                builder.Append(ex.StackTrace + "\r\n");
            }

            return builder.ToString();
        }
    }
秋的红果实 2019-09-19
  • 打赏
  • 举报
回复
从而决定添加新日志,活着开出新的次线程 => 从而决定往队列添加新日志,还是开出新的次线程
秋的红果实 2019-09-19
  • 打赏
  • 举报
回复
补充下:我上面写的,是在你原有基础上的,在次线程里用timer触发,没日志写的时候,就enable=false 也可以在写日志的时候,开出次线程,判断原来的次线程是否活着(活着,说明队列里还有没写完的日志),从而决定添加新日志,活着开出新的次线程
良朋 2019-09-19
  • 打赏
  • 举报
回复
收藏 。。。
独立观察员 2019-09-19
  • 打赏
  • 举报
回复
  • 打赏
  • 举报
回复
引用 23 楼 秋的红果实 的回复:
不知道lz看我#11写的没 你的写法,至少Task.Factory.StartNew(HandleLogQueue);=>Task.Factory.StartNew(HandleLogQueue,_cancellToken); HandleLogQueue方法里面要判断令牌请求的,否则,令牌没用 关键是你打算用令牌取消什么,日志写了一半,你要强行取消吗? 猜测你的意思是:当队列空时,取消线程
所以令牌就是用来取消GetConsumingEnumerable的 日志就算只写了一半也要强行取消 按照你的想法 因为现在写日志的频率有时候很快 如果要等到日志写完才关闭 这边等日志写完 那边一直在给队列添加新的日志 永远都关不了了
独立观察员 2019-09-19
  • 打赏
  • 举报
回复

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
/*
* 代码已托管:https://gitee.com/dlgcy/dotnetcodes/tree/dlgcy/DotNet.Utilities/%E6%97%A5%E5%BF%97
*/
namespace DotNet.Utilities
{
/// <summary>
/// 简易日志类;
/// </summary>
public static class LogHelper
{
/// <summary>
/// 前缀;
/// </summary>
private static readonly string _Prefix = "Prefix";

/// <summary>
/// 目录;
/// </summary>
private static readonly string _LogDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log", _Prefix);
//private static readonly string _LogDir = Path.Combine("D:\\Log", _Prefix);

/// <summary>
/// 文件名;
/// </summary>
private static string LogFile => Path.Combine(_LogDir, $"{_Prefix}_{DateTime.Now:yyyyMMdd}.log");

/// <summary>
/// 单条内容最大长度;
/// </summary>
private static readonly int _MaxLength = 5000;

/// <summary>
/// 阻塞集合;
/// </summary>
private static BlockingCollection<string> _BC = new BlockingCollection<string>();

static LogHelper()
{
if (!Directory.Exists(_LogDir))
{
Directory.CreateDirectory(_LogDir);
}

Task.Run(() =>
{
foreach (var item in _BC.GetConsumingEnumerable())
{
try
{
File.AppendAllText(LogFile, item);
}
catch (Exception ex)
{
//Console.WriteLine($"{ex}");
File.AppendAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogErr.txt"), $"{ex}");
}
}
});
}

/// <summary>
/// 记录日志;
/// </summary>
/// <param name="content">日志内容</param>
/// <param name="isNewLine">前面是否空行</param>
public static void Write(string content, bool isNewLine = true, [CallerFilePath]string filePath = "", [CallerMemberName]string memberName = "", [CallerLineNumber]int lineNumber = 0)
{
int len = content.Length;
if (len > _MaxLength)
{
content = $"{content.Substring(0, _MaxLength / 2)}【...】{content.Substring(len - _MaxLength / 2)}";
}

//File.AppendAllText(LogFile, $"{(isNewLine?"\r\n":"")}\r\n[{DateTime.Now:HH:mm:ss ffff}][{len}]{content}");

_BC.Add($"{(isNewLine ? $"\r\n\r\n{Path.GetFileNameWithoutExtension(filePath)}/{memberName}/{lineNumber}---->" : "")}\r\n[{DateTime.Now:HH:mm:ss ffff}][{len}]{content}");
}
}

/// <summary>
/// Console.Write 重定向到 日志
/// 用法:
/// 在构造器里加上:Console.SetOut(new ConsoleWriter());
/// </summary>
public class ConsoleWriter : TextWriter
{
// 使用 UTF-16 避免不必要的编码转换
public override Encoding Encoding => Encoding.Unicode;

// 最低限度需要重写的方法
public override void Write(string value)
{
LogHelper.Write(value, false);
}

// 为提高效率直接处理一行的输出
public override void WriteLine(string value)
{
LogHelper.Write(value);
}
}
}

  • 打赏
  • 举报
回复
引用
你的写法,至少Task.Factory.StartNew(HandleLogQueue);=>Task.Factory.StartNew(HandleLogQueue,_cancellToken); HandleLogQueue方法里面要判断令牌请求的,否则,令牌没用 关键是你打算用令牌取消什么,日志写了一半,你要强行取消吗? 猜测你的意思是:当队列空时,取消线程
GetConsumingEnumerable内部会一直循环等待获取新加入队列的数据 所以你写的这部分代码是不会执行的 if(_queue.QueueIsEmpty()) { _cancellationTokenSource.Cancel(); }
加载更多回复(10)

110,571

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧