RSDistributedLoggingLibrary 1.0.4

dotnet add package RSDistributedLoggingLibrary --version 1.0.4
                    
NuGet\Install-Package RSDistributedLoggingLibrary -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RSDistributedLoggingLibrary" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RSDistributedLoggingLibrary" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="RSDistributedLoggingLibrary" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RSDistributedLoggingLibrary --version 1.0.4
                    
#r "nuget: RSDistributedLoggingLibrary, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RSDistributedLoggingLibrary@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RSDistributedLoggingLibrary&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=RSDistributedLoggingLibrary&version=1.0.4
                    
Install as a Cake Tool

RSDistributedLoggingLibrary - 分布式日志库

📋 目录

🌟 功能特点

  • 程序员姓名记录 - 支持记录程序员姓名,用于Bug统计和责任追踪
  • 美化日志格式 - 清晰易读的控制台和文件输出,包含图标和结构化显示
  • 多种输出方式 - 支持文件、RabbitMQ、双重输出
  • AOP切面编程 - 基于PostSharp的方法拦截日志
  • 异步日志记录 - 支持异步日志记录,提高性能
  • 丰富的异常类型 - 预定义多种业务异常类型
  • JSON格式支持 - 同时支持美化格式和JSON格式
  • 灵活配置 - 支持配置文件动态配置

🚀 快速开始

1. 安装NuGet包

Install-Package RSDistributedLoggingLibrary

2. 基础配置

appsettings.json 配置
    {
      "LogSettings": {
        "LogLevel": "Info",
    "LogOutput": "FileAndQueue",
    "FilePath": "logs",
    "DefaultDeveloperName": "张三",
    "ProjectName": "我的项目",
        "RabbitMQ": {
      "Host": "localhost",
          "Port": 5672,
      "Username": "admin",
      "Password": "admin123",
          "QueueName": "rslog_queue",
          "Exchange": "rslog_exchange",
          "RoutingKey": "log_key",
          "VirtualHost": "rslog"
        }
      }
    }
初始化代码
using Microsoft.Extensions.Configuration;
using RSDistributedLoggingLibrary;

// 读取配置
var configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();

var logConfig = configuration.GetSection("LogSettings").Get<LogConfiguration>();
LogStaticHelper.Initialize(logConfig);

📚 项目集成指南

ASP.NET Core Web API 项目

Program.cs
using Microsoft.Extensions.Configuration;
using RSDistributedLoggingLibrary;

var builder = WebApplication.CreateBuilder(args);

// 添加日志配置
var logConfig = builder.Configuration.GetSection("LogSettings").Get<LogConfiguration>();
LogStaticHelper.Initialize(logConfig);

builder.Services.AddControllers();

var app = builder.Build();

// 添加全局异常处理中间件
app.UseMiddleware<LoggingMiddleware>();

app.UseRouting();
app.MapControllers();
app.Run();
控制器中使用
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly Logger _logger;

    public OrderController()
    {
        _logger = new Logger("李四", "电商系统");
    }

    [HttpPost]
    [LogException] // AOP自动记录异常
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        try
        {
            _logger.LogMessage("开始创建订单", "Info", "业务");
            
            // 业务逻辑
            var orderId = await ProcessOrder(request);
            
            _logger.LogMessage($"订单创建成功,订单号:{orderId}", "Info", "业务");
            return Ok(new { OrderId = orderId });
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning(ex, "订单验证失败");
            return BadRequest(ex.Message);
        }
        catch (BusinessException ex)
        {
            _logger.LogError(ex, "业务逻辑错误");
            return StatusCode(500, "业务处理失败");
        }
    }

    [ExecutionTimeLogger] // AOP自动记录执行时间
    private async Task<string> ProcessOrder(CreateOrderRequest request)
    {
        // 模拟业务逻辑
        if (string.IsNullOrEmpty(request.ProductId))
        {
            throw new ValidationException("产品ID不能为空", new[] { "ProductId" });
        }

        if (request.Quantity <= 0)
        {
            throw new BusinessException("订单数量必须大于0", "ORDER_QUANTITY_INVALID");
        }

        // 异步处理
        await Task.Delay(100);
        return Guid.NewGuid().ToString();
    }
}

Windows Forms 项目

Form1.cs
using Microsoft.Extensions.Configuration;
using RSDistributedLoggingLibrary;

public partial class Form1 : Form
{
    private readonly Logger _logger;

    public Form1()
    {
        InitializeComponent();
        InitializeLogging();
        _logger = new Logger("王五", "桌面应用");
    }

    private void InitializeLogging()
    {
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Application.StartupPath)
            .AddJsonFile("appsettings.json", optional: false)
            .Build();

        var logConfig = configuration.GetSection("LogSettings").Get<LogConfiguration>();
        LogStaticHelper.Initialize(logConfig);

        _logger.LogMessage("应用程序启动", "Info", "系统");
    }

    private async void btnSave_Click(object sender, EventArgs e)
    {
        try
        {
            _logger.LogMessage("用户点击保存按钮", "Info", "用户操作");
            
            await SaveDataAsync();
            
            _logger.LogMessage("数据保存成功", "Info", "业务");
            MessageBox.Show("保存成功!");
        }
        catch (DatabaseException ex)
        {
            _logger.LogError(ex, "数据库保存失败");
            MessageBox.Show("保存失败,请重试");
        }
    }

    [LogException]
    private async Task SaveDataAsync()
    {
        // 模拟数据保存
        if (string.IsNullOrEmpty(txtName.Text))
        {
            throw new ValidationException("姓名不能为空", new[] { "Name" });
        }

        await Task.Delay(500); // 模拟异步操作
    }
}

控制台应用项目

Program.cs
using Microsoft.Extensions.Configuration;
using RSDistributedLoggingLibrary;

class Program
{
    private static Logger _logger;

    static async Task Main(string[] args)
    {
        // 初始化日志系统
        var configuration = new ConfigurationBuilder()
            .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
            .AddJsonFile("appsettings.json", optional: false)
            .Build();

        var logConfig = configuration.GetSection("LogSettings").Get<LogConfiguration>();
        LogStaticHelper.Initialize(logConfig);

        _logger = new Logger("赵六", "批处理系统");

        try
        {
            _logger.LogMessage("批处理任务开始", "Info", "系统");
            
            await ProcessBatchJob();
            
            _logger.LogMessage("批处理任务完成", "Info", "系统");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "批处理任务失败");
        }

        Console.WriteLine("按任意键退出...");
        Console.ReadKey();
    }

    [ExecutionTimeLogger]
    static async Task ProcessBatchJob()
    {
        for (int i = 1; i <= 10; i++)
        {
            try
            {
                _logger.LogMessage($"处理第 {i} 个任务", "Info", "业务");
                
                // 模拟处理
                await Task.Delay(100);
                
                if (i == 7) // 模拟第7个任务失败
                {
                    throw new BusinessException($"第 {i} 个任务处理失败", "TASK_FAILED");
                }
            }
            catch (BusinessException ex)
            {
                _logger.LogWarning(ex, $"任务 {i} 处理异常,继续下一个");
            }
        }
    }
}

🔧 使用方式详解

方式1:静态方法调用(兼容老代码)

// 基础异常记录
try
{
    // 业务代码
}
catch (Exception ex)
{
    // 不指定程序员姓名,使用配置文件默认值
    LogStaticHelper.LogError(ex, "发生异常");
    
    // 指定程序员姓名
    LogStaticHelper.LogError(ex, "发生异常", "张三");
}

// 不同级别的日志
LogStaticHelper.LogDebug(exception, "调试信息", "开发者A");
LogStaticHelper.LogInfo(exception, "一般信息", "开发者B");
LogStaticHelper.LogWarning(exception, "警告信息", "开发者C");
LogStaticHelper.LogError(exception, "错误信息", "开发者D");

// 自定义消息日志
LogStaticHelper.LogMessage("用户登录成功", "Info", "业务", "李四");
LogStaticHelper.LogMessage("系统性能监控", "Warning", "性能", "王五");

方式2:Logger实例方式(推荐)

// 创建Logger实例
var logger = new Logger("张三", "电商系统");

// 记录异常
try
{
    // 业务代码
}
catch (Exception ex)
{
    logger.LogError(ex, "订单处理失败");
}

// 记录业务日志
logger.LogMessage("订单创建成功", "Info", "业务");
logger.LogMessage("库存不足警告", "Warning", "库存");

// 异步记录(推荐在高并发场景使用)
await logger.LogErrorAsync(ex, "异步异常处理");
await logger.LogMessageAsync("异步业务日志", "Info", "异步业务");

// 可以动态指定程序员姓名(覆盖构造函数中的默认值)
logger.LogError(ex, "特殊异常", "临时开发者");

方式3:AOP特性方式(自动记录)

[LogException]  // 自动记录异常
[ExecutionTimeLogger]  // 自动记录执行时间
public async Task<string> BusinessMethod(string param)
{
    // 业务代码
    // 如果发生异常,会自动记录日志
    // 方法执行时间也会自动记录
    
    await Task.Delay(100);
    return "success";
}

// 只记录异常
[LogException]
public void ValidateData(string data)
{
    if (string.IsNullOrEmpty(data))
    {
        throw new ValidationException("数据不能为空", new[] { "data" });
    }
}

// 只记录执行时间
[ExecutionTimeLogger]
public void PerformanceMonitorMethod()
{
    // 性能敏感的代码
    Thread.Sleep(200);
}

💼 实际案例

案例1:电商订单系统

public class OrderService
{
    private readonly Logger _logger;

    public OrderService()
    {
        _logger = new Logger("电商开发组", "订单系统");
    }

    [LogException]
    [ExecutionTimeLogger]
    public async Task<OrderResult> CreateOrderAsync(CreateOrderRequest request)
    {
        _logger.LogMessage($"开始创建订单,用户ID:{request.UserId}", "Info", "订单");

        try
        {
            // 1. 验证请求
            ValidateOrderRequest(request);
            
            // 2. 检查库存
            await CheckInventoryAsync(request.Items);
            
            // 3. 计算价格
            var totalAmount = CalculateOrderAmount(request.Items);
            _logger.LogMessage($"订单金额计算完成:{totalAmount}", "Info", "计算");
            
            // 4. 创建订单
            var orderId = await CreateOrderInDatabaseAsync(request, totalAmount);
            
            // 5. 扣减库存
            await ReduceInventoryAsync(request.Items);
            
            _logger.LogMessage($"订单创建成功,订单号:{orderId}", "Info", "订单");
            
            return new OrderResult { OrderId = orderId, Success = true };
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning(ex, "订单验证失败");
            throw;
        }
        catch (BusinessException ex)
        {
            _logger.LogError(ex, "订单业务逻辑错误");
            throw;
        }
        catch (DatabaseException ex)
        {
            _logger.LogError(ex, "订单数据库操作失败");
            throw new BusinessException("系统繁忙,请稍后重试", "SYSTEM_BUSY", ex);
        }
    }

    private void ValidateOrderRequest(CreateOrderRequest request)
    {
        var errors = new List<string>();
        
        if (string.IsNullOrEmpty(request.UserId))
            errors.Add("用户ID不能为空");
            
        if (request.Items == null || !request.Items.Any())
            errors.Add("订单商品不能为空");
            
        if (errors.Any())
        {
            throw new ValidationException("订单验证失败", errors);
        }
    }

    private async Task CheckInventoryAsync(List<OrderItem> items)
    {
        foreach (var item in items)
        {
            try
            {
                var stock = await GetProductStockAsync(item.ProductId);
                if (stock < item.Quantity)
                {
                    throw new BusinessException($"商品 {item.ProductId} 库存不足", "INSUFFICIENT_STOCK");
                }
            }
            catch (ExternalServiceException ex)
            {
                _logger.LogError(ex, $"检查商品 {item.ProductId} 库存时外部服务异常");
                throw new BusinessException("库存服务暂时不可用", "INVENTORY_SERVICE_UNAVAILABLE", ex);
            }
        }
    }
}

案例2:数据同步服务

public class DataSyncService
{
    private readonly Logger _logger;

    public DataSyncService()
    {
        _logger = new Logger("数据组", "同步服务");
    }

    [LogException]
    public async Task StartSyncJobAsync()
    {
        _logger.LogMessage("数据同步任务开始", "Info", "同步");

        try
        {
            var totalRecords = await GetTotalRecordsAsync();
            _logger.LogMessage($"总共需要同步 {totalRecords} 条记录", "Info", "同步");

            var batchSize = 1000;
            var processedCount = 0;
            var errorCount = 0;

            for (int offset = 0; offset < totalRecords; offset += batchSize)
            {
                try
                {
                    var records = await GetRecordsBatchAsync(offset, batchSize);
                    await ProcessBatchAsync(records);
                    
                    processedCount += records.Count;
                    _logger.LogMessage($"已处理 {processedCount}/{totalRecords} 条记录", "Info", "进度");
                }
                catch (DatabaseException ex)
                {
                    errorCount++;
                    _logger.LogError(ex, $"处理批次 {offset}-{offset + batchSize} 时数据库异常");
                    
                    if (errorCount > 5)
                    {
                        throw new BusinessException("连续错误次数过多,停止同步", "TOO_MANY_ERRORS", ex);
                    }
                }
                catch (NetworkException ex)
                {
                    _logger.LogWarning(ex, $"处理批次 {offset}-{offset + batchSize} 时网络异常,等待重试");
                    await Task.Delay(5000); // 等待5秒重试
                    offset -= batchSize; // 重试当前批次
                }
            }

            _logger.LogMessage($"数据同步完成,成功:{processedCount},错误:{errorCount}", "Info", "同步");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "数据同步任务失败");
            throw;
        }
    }

    [ExecutionTimeLogger]
    private async Task ProcessBatchAsync(List<DataRecord> records)
    {
        foreach (var record in records)
        {
            try
            {
                await ProcessSingleRecordAsync(record);
            }
            catch (ValidationException ex)
            {
                _logger.LogWarning(ex, $"记录 {record.Id} 验证失败,跳过");
            }
        }
    }
}

⚙️ 配置详解

LogOutput 选项

  • File - 仅输出到文件,适合本地开发和小型应用
  • RabbitMQ - 仅输出到消息队列,适合云环境和微服务
  • FileAndQueue - 同时输出到文件和消息队列,适合生产环境

环境特定配置

开发环境 (appsettings.Development.json)
{
  "LogSettings": {
    "LogLevel": "Debug",
    "LogOutput": "File",
    "FilePath": "logs",
    "DefaultDeveloperName": "开发者",
    "ProjectName": "开发环境"
  }
}
测试环境 (appsettings.Test.json)
{
  "LogSettings": {
    "LogLevel": "Info",
    "LogOutput": "FileAndQueue",
    "FilePath": "/var/logs/myapp",
    "DefaultDeveloperName": "测试组",
    "ProjectName": "测试环境",
    "RabbitMQ": {
      "Host": "test-rabbitmq.company.com",
      "Port": 5672,
      "Username": "test_user",
      "Password": "test_password",
      "QueueName": "test_logs",
      "Exchange": "test_exchange",
      "RoutingKey": "test_logs",
      "VirtualHost": "test"
    }
  }
}
生产环境 (appsettings.Production.json)
{
  "LogSettings": {
    "LogLevel": "Warning",
    "LogOutput": "RabbitMQ",
    "DefaultDeveloperName": "生产系统",
    "ProjectName": "生产环境",
    "RabbitMQ": {
      "Host": "prod-rabbitmq-cluster.company.com",
      "Port": 5672,
      "Username": "prod_logger",
      "Password": "secure_password",
      "QueueName": "production_logs",
      "Exchange": "production_exchange",
      "RoutingKey": "prod_logs",
      "VirtualHost": "production"
    }
  }
}

📝 日志格式

美化格式(文件和控制台)

================================================================================
🔴 [ERROR] 2025-06-25 10:19:34
👤 开发者: 张三  📦 项目: 电商系统
🏷️  类型: 异常  🔧 状态: 未处理
💬 消息: 订单处理失败
❌ 异常: 库存不足
🔍 类型: BusinessException
📋 代码: INSUFFICIENT_STOCK
🏗️  程序集: OrderService
📚 堆栈跟踪:
   at OrderService.CheckInventory() line 45
🔧 额外信息:
   ProductId: P001
   RequestedQuantity: 5
   AvailableStock: 2
--------------------------------------------------------------------------------

JSON格式(RabbitMQ)

{
  "Message": "订单处理失败",
  "StatusCode": "INSUFFICIENT_STOCK",
  "ExceptionMessage": "库存不足",
  "ExceptionStackTrace": "...",
  "ExceptionType": "BusinessException",
  "AssemblyName": "OrderService",
  "TimeStamp": "2025-06-25T02:19:34.123Z",
  "DeveloperName": "张三",
  "ProjectName": "电商系统",
  "LogLevel": "Error",
  "BugStatus": "未处理",
  "LogType": "异常",
  "CustomProperties": {
    "ProductId": "P001",
    "RequestedQuantity": 5,
    "AvailableStock": 2
  }
}

🎯 最佳实践

1. 程序员姓名规范

// ✅ 推荐:使用真实姓名
var logger = new Logger("张三", "订单系统");

// ✅ 推荐:使用团队名称
var logger = new Logger("后端开发组", "用户服务");

// ❌ 不推荐:使用代号或昵称
var logger = new Logger("coder001", "系统");

2. 异常处理层次

public class OrderController : ControllerBase
{
    private readonly Logger _logger;

    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        try
        {
            // 业务逻辑
            var result = await _orderService.CreateOrderAsync(request);
            return Ok(result);
        }
        catch (ValidationException ex)
        {
            // 客户端错误,记录警告级别
            _logger.LogWarning(ex, "订单验证失败");
            return BadRequest(ex.Message);
        }
        catch (BusinessException ex)
        {
            // 业务错误,记录错误级别
            _logger.LogError(ex, "订单业务逻辑失败");
            return StatusCode(422, "业务处理失败");
        }
        catch (Exception ex)
        {
            // 系统错误,记录错误级别
            _logger.LogError(ex, "订单系统异常");
            return StatusCode(500, "系统错误");
        }
    }
}

3. 性能敏感场景

public class HighPerformanceService
{
    private readonly Logger _logger;

    // ✅ 推荐:使用异步日志
    public async Task ProcessDataAsync(string data)
    {
        try
        {
            // 业务逻辑
            await ProcessBusinessLogic(data);
        }
        catch (Exception ex)
        {
            // 异步记录,不阻塞主流程
            _ = _logger.LogErrorAsync(ex, "数据处理失败");
            throw;
        }
    }

    // ✅ 推荐:批量处理时只记录关键日志
    public async Task BatchProcessAsync(List<string> dataList)
    {
        _logger.LogMessage($"开始批量处理,共 {dataList.Count} 条", "Info", "批处理");
        
        int successCount = 0;
        int errorCount = 0;

        foreach (var data in dataList)
        {
            try
            {
                await ProcessBusinessLogic(data);
                successCount++;
            }
            catch (Exception ex)
            {
                errorCount++;
                // 只记录错误,不记录每个成功的项
                if (errorCount <= 10) // 限制错误日志数量
                {
                    _ = _logger.LogErrorAsync(ex, $"处理数据失败: {data}");
                }
            }
        }

        _logger.LogMessage($"批量处理完成,成功:{successCount},失败:{errorCount}", "Info", "批处理");
    }
}

4. 多环境配置

public class ConfigurationHelper
{
    public static void InitializeLogging(IConfiguration configuration)
    {
        var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
        var logConfig = configuration.GetSection("LogSettings").Get<LogConfiguration>();
        
        // 根据环境调整配置
        switch (environment)
        {
            case "Development":
                logConfig.LogLevel = "Debug";
                logConfig.LogOutput = "File";
                break;
            case "Production":
                logConfig.LogLevel = "Warning";
                logConfig.LogOutput = "RabbitMQ";
                break;
            default:
                logConfig.LogOutput = "FileAndQueue";
                break;
        }
        
        LogStaticHelper.Initialize(logConfig);
    }
}

🛠️ 故障排除

常见问题

1. RabbitMQ连接失败
// 检查连接配置
var rabbitConfig = logConfig.RabbitMQ;
Console.WriteLine($"连接地址: {rabbitConfig.Host}:{rabbitConfig.Port}");
Console.WriteLine($"虚拟主机: {rabbitConfig.VirtualHost}");
Console.WriteLine($"队列名称: {rabbitConfig.QueueName}");

解决方案:

  • 确认RabbitMQ服务正在运行
  • 检查网络连接和防火墙设置
  • 验证用户名密码和权限
  • 确认虚拟主机存在
2. 文件日志写入失败
// 检查文件路径权限
var logPath = Path.Combine(logConfig.FilePath, $"log-{DateTime.Now:yyyy-MM-dd}.txt");
Console.WriteLine($"日志文件路径: {Path.GetFullPath(logPath)}");

// 测试目录创建权限
try
{
    Directory.CreateDirectory(logConfig.FilePath);
    Console.WriteLine("目录创建成功");
}
catch (Exception ex)
{
    Console.WriteLine($"目录创建失败: {ex.Message}");
}

解决方案:

  • 确保应用程序有文件夹写入权限
  • 检查磁盘空间是否充足
  • 验证路径格式是否正确
3. PostSharp AOP不工作
// 检查PostSharp是否正确安装
[LogException]
public void TestMethod()
{
    throw new Exception("测试异常");
}

解决方案:

  • 确认已安装PostSharp NuGet包
  • 检查PostSharp许可证是否有效
  • 重新构建项目
  • 查看编译输出是否有PostSharp相关信息
4. 性能问题
// 使用异步日志避免阻塞
_ = logger.LogErrorAsync(ex, "异步记录异常");

// 在高频场景中限制日志数量
private static int _logCount = 0;
if (Interlocked.Increment(ref _logCount) % 100 == 0)
{
    logger.LogMessage($"已处理 {_logCount} 条记录", "Info", "进度");
}

调试模式

// 启用详细日志用于调试
LogStaticHelper.LogMessage("日志系统初始化完成", "Debug", "系统", "DEBUG");
LogStaticHelper.LogMessage($"当前日志级别: {logConfig.LogLevel}", "Debug", "系统", "DEBUG");
LogStaticHelper.LogMessage($"输出方式: {logConfig.LogOutput}", "Debug", "系统", "DEBUG");
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 151 6/25/2025
1.0.3 140 11/22/2024
1.0.2 109 11/22/2024
1.0.1 106 11/22/2024
1.0.0 123 11/9/2024