05、logback(日志实现框架)(一)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 05、logback(日志实现框架)(一)

一、认识Logback


1.1、Logback概述


官方网站:https://logback.qos.ch/index.html


Logback:同样是log4j创始人离开Apache之后设计的开源日志组件,其性能比Log4j要好。


logback分为三个模块:logback-core,logback-classic和logback-access


logback-core:为其他两个模块奠定基础

logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API。

logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能。

现如今日志框架都是通过slf4j日志门面来搭建日志系统的,在大多引入日志实现框架中都包含了slf4j-api的依赖,之后主要通过修改配置文件以及pom.xml。



1.2、日志等级(五个)


package ch.qos.logback.classic;
public final class Level implements Serializable {
    public static final Level OFF = new Level(2147483647, "OFF");
    public static final Level ERROR = new Level(40000, "ERROR");
    public static final Level WARN = new Level(30000, "WARN");
    public static final Level INFO = new Level(20000, "INFO");
    public static final Level DEBUG = new Level(10000, "DEBUG");
    public static final Level TRACE = new Level(5000, "TRACE");
    public static final Level ALL = new Level(-2147483648, "ALL");
  ...
}


日志级别排序为:TRACE< DEBUG<INFO< WARN< ERROR。默认日志等级为Debug,其中OFF与ALL作为日志开关。



1.3、相关组件


在logback中也包含三个组件Logger、Appender以及Layout,与Log4j大同小异:


Logger:日志记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。

Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等。

Layout:负责把事件转换成字符串,格式化的日志信息的输出,在Logback中Layout对象被封装在encoder中。


二、Logback配置文件(XML)


提前准备

在使用logback日志框架会默认读取以下类型的配置文件:


logback.groovy

logback-test.xml

logback.xml

若是均不存在会采用默认配置,日志等级为DEBUG,默认输出到console。


一般创建logback.xml(Maven项目放置到resource下),需要提前准备内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  ....相关配置信息
</configuration>


下面部分内容直接就写相关配置信息并不包含上面提前准备内容!!!



RootLogger设置

root基本設置


<!--  设置root的日志等级为ALL  -->
<root level="ALL">
    <!--  指定appender引用,这里例如引用下面的appender  -->
    <appender-ref ref="console"/>
</root>



配置键值对以及进行获取对应键的值


<!-- ①配置一对键值对,其name可被引用-->
<property name="pattern"  value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"/>
<!--  配置appender过程中引用对应键值对  -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <!--   设置自定义格式布局   -->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <!--  ********************  -->
        <!--   ②注意这里使用${name}包裹能够引用property中name对应的value   -->
        <pattern>${pattern}</pattern>
        <!--  ********************  -->
    </encoder>
</appender>


注意看①②,在②中引用预先配置的property,使用${}获取其中的value值


日志输出格式


<!--
    日志输出格式:
    %-5level:日志等级
    %d{yyyy-MM-dd HH:mm:ss.SSS}:日期
    %c:类的完整名称
    %M:method
    %L:行号
    %thread:线程名称
    %m或者%msg:信息
    %n:换行
-->
<!--配置一对键值对  日志等级(值为靠左对齐5个位子) 日期 类完整名 方法 行号 线程名 信息及换行-->
<property name="pattern"  value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"/>



格式输出大致如图。


Appender

介绍



ConsoleAppender:输出到屏幕。

FileAppender:输出到指定文件。

RollingFileAppender:可定时创建文件以及按文件大小进行切割。


ConsoleAppender

目的:输出到屏幕上。


实例演示


配置logback.xml:


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--  可见root基本设置中的格式介绍  -->
    <property name="pattern"  value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"/>
    <!--  ********************  -->
    <!--  配置ConsoleAppender  -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--  输出流对象  -->
        <target>System.err</target>
        <!--   日志格式配置   -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--   设置输出格式引用上面的键值对及输出的格式   -->
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>
    <!--  ********************  -->
    <!-- 设置root,引用appender  -->
    <root level="ALL">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>
</configuration>



运行程序:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        //为了效果明显循环10000次
        for (int i = 0; i < 10000; i++) {
            LOGGER.error("error");
            LOGGER.warn("warn");
            LOGGER.info("info");
            LOGGER.debug("debug");
            LOGGER.trace("trace");
        } 
    }
}




源码初探


查看下源码中对应的配置属性:



第9行:encoder是ConsoleAppender的父类OutputStreamAppender中有setEncoder()设置方法俩设置Encoder

第11行:对应在PatternLayoutEncoder的父类PatternLayoutEncoderBase中有setPattern()设置方法所以可以在对应标签中设置pattern。


FileAppender(自定义格式与HTML)

目的:日志输出到文件中。


针对于FileAppender其中append = true(可追加)、FileSize bufferSize = new FileSize(8192);(缓冲大小)。



FileAppender本身类中包含setFile(),其父类OutputStreamAppender包含setEncoder。

输出到文件其格式为自定义格式(PatternLayoutEncoder)


<!-- 配置路径以及日志格式  -->
<property name="log_dir"  value="d:/logs"/>
<property name="pattern"  value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"/>
<!--  ********************  -->
<!--  配置fileAppender  -->
<appender name="file" class="ch.qos.logback.core.FileAppender">
    <!--  配置文件路径  -->
    <file>${log_dir}/logback.log</file>
    <!--  日志格式设置  -->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>${pattern}</pattern>
    </encoder>
</appender>
<!--  ********************  -->
<!--  设置root的日志等级为ALL,以及引用上面的name为console的appender  -->
<root level="ALL">
    <!--  这里console只用来表示root标签可设置多个appender,无引用上面的意思 -->
    <appender-ref ref="console"/>
    <appender-ref ref="file"/>
</root>




输出程序可见下面的3.2,对应的键值对设置无非对应指定类或其父类的相关set方法可见源码。


输出到文件其格式为HTML格式(LayoutWrappingEncoder中包含Layout为HTMLLayout)


<!-- 配置路径以及日志格式  -->
<property name="log_dir"  value="d:/logs"/>
<property name="pattern"  value="%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"/>
<!--  ********************  -->
<!--  配置HTML格式的Appender  -->
<appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
    <!--  配置文件路径  -->
    <file>${log_dir}/logback.html</file>
    <!--  日志格式设置  -->
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <!--  设置了HTMLLayout的layout  -->
        <layout class="ch.qos.logback.classic.html.HTMLLayout">
            <!--  注意:输出格式为HTML时,不需要有空格或者其他符号或者换行,否则输出格式会有空格不美观  -->
            <pattern>%-5level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%m</pattern>
        </layout>
    </encoder>
</appender>
<!--  ********************  -->
<!--  设置root的日志等级为ALL,引用console和  -->
<root level="ALL">
    <!--  这里console只用来表示root标签可设置多个appender,无引用上面的意思 -->
    <appender-ref ref="console"/>
    <appender-ref ref="htmlFile"/>
</root>




测试程序见3.2,对应标签设置无非就是对应set方法。

在ch.qos.logback.core.encoder.LayoutWrappingEncoder中包含layout可设置对应的htmllayout。


RollingFileAppender

目的:可定时创建文件以及按文件大小进行切割。


配置详情


RollingFileAppender类实现了FileAppender,即具有FileAppender可设置的属性内容如Encoder、file(输出路径)


<!--  设置路径键值对 -->
<property name="log_dir"  value="d:/logs"/>
<!--  ********************  -->
<!--  日志拆分和归档的appender对象 -->
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--  配置文件路径  -->
    <file>${log_dir}/roll_logback.log</file>
    <!--  自定义日志格式设置  -->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>${pattern}</pattern>
    </encoder>
    <!--   指定日志文件拆分与压缩规则     -->
    <RollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!--   通过指定压缩文件名称来确定分割文件方式,这里是设置为年月日  
      可精确到毫秒:%d{yyyy-MM-dd-hh-mm-ss-SSS}
  -->
        <fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd-hh-mm-ss}.log%i.gz</fileNamePattern>
        <!--   文件拆分大小,超过1MB即拆分     -->
        <maxFileSize>1MB</maxFileSize>
    </RollingPolicy>
</appender>
<!--  ********************  -->
<!--  设置root的日志等级为ALL,引用appender  -->
<root level="ALL">
    <appender-ref ref="rollingFile"/>
</root>


第13行:设置的日志文件拆分与压缩规则类,其包含两个属性fileNamePattern可确定分割文件的名称以及分割的时间(分割上面file文件),maxFileSize可设置单个文件的大小,这里最大为1MB。

测试程序:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        //为了效果明显循环10000次
        for (int i = 0; i < 10000; i++) {
            LOGGER.error("error");
            LOGGER.warn("warn");
            LOGGER.info("info");
            LOGGER.debug("debug");
            LOGGER.trace("trace");
        } 
    }
}



原理分析


首先看该类的层级图,注意到其继承了FileAppender,那么就可以知道一定有file、Encoder属性可设置。



看该类中的属性及set方法:


public class RollingFileAppender<E> extends FileAppender<E> {
    File currentlyActiveFile
    RollingPolicy rollingPolicy;
    //两个set方法
    @Override
    public void setFile(String file) {
        if (file != null && ((triggeringPolicy != null) || (rollingPolicy != null))) {
            addError("File property must be set before any triggeringPolicy or rollingPolicy properties");
            addError(MORE_INFO_PREFIX + RFA_LATE_FILE_URL);
        }
        super.setFile(file);
    }
    public void setRollingPolicy(RollingPolicy policy) {
        rollingPolicy = policy;
        if (rollingPolicy instanceof TriggeringPolicy) {
            triggeringPolicy = (TriggeringPolicy<E>) policy;
        }
    }
}



我们分析一下RollingPolicy:其是一个接口,我们可以使用它的实现类SizeAndTimeBasedRollingPolicy



public class SizeAndTimeBasedRollingPolicy<E> extends TimeBasedRollingPolicy<E> {
  //设置单个文件最大容量
    FileSize maxFileSize;   
}
//注意其父类,见1
public class TimeBasedRollingPolicy<E> extends RollingPolicyBase implements TriggeringPolicy<E> {
}
public abstract class RollingPolicyBase extends ContextAwareBase implements RollingPolicy {
    //设置文件自定义格式化名
    FileNamePattern fileNamePattern;
                                                                                                             public void setFileNamePattern(String fnp) {
        fileNamePatternStr = fnp;
    }
}




Filter

介绍

目的:用来进行过滤指定日志等级的日志记录。


Filter可设置在任意的appender中,如下:



EvaluatorFilter:临界过滤器,滤掉低于指定临界值的日志。

LevelFilter:级别过滤器,对特定某个级别的日志进行过滤。


LevelFilter

配置说明


配置LevelFilter的三个属性介绍:


level:设置过滤的日志等级。

onMatch:匹配日志等级时执行操作。

onMismatch:不匹配日志等级时执行操作。

其中onMatch、onMismatch对应表示使用的是指定的枚举值(三个):


DENY:日志将立即被抛弃不再经过其他过滤器。(否定含义)

NEUTRAL:有序列表里的下个过滤器过接着处理日志;(该级别既不处理,也不抛弃,相当于没有任何处理,日志会被保存下来并在本appender被执行)(中立的)

ACCEPT:日志会被立即处理,不再经过剩余过滤器。(通过含义)


实例使用


要求:只将error日志等级输出到文件中。


logback.xml配置如下:


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--  配置fileAppender  -->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--  配置文件路径  -->
        <file>${log_dir}/logback.log</file>
        <!--  日志格式设置  -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--  ********************  -->
        <!--  配置日志等级过滤器:LevelFilter  -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--  指定日志等级  -->
            <level>error</level>
            <!--  若是>=error(匹配)直接通过不过滤 -->
            <onMatch>ACCEPT</onMatch>
            <!--  若是<error(不匹配)过滤掉 -->
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--  ********************  -->
    </appender>
    <!--  设置root的日志等级为ALL,以及引用上面的name为console的appender  -->
    <root level="ALL">
        <appender-ref ref="file"/>
    </root>
</configuration>



测试程序:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            LOGGER.error("error");
            LOGGER.warn("warn");
            LOGGER.info("info");
            LOGGER.debug("debug");
            LOGGER.trace("trace");
        }
    }
}




原理分析


上面配置的是LevelFilter类,我们看下该类包含的属性:



可以看到LevelFilter类中包含level属性及set方法。



LevelFilter父类AbstractMatcherFilter包含了onMatch、onMismatch属性及set方法。

可以看到其默认值为FilterReply.NEUTRAL对应的是枚举类,我们再去看下枚举类:



对应着三个实例,对应含义见上。


异步日志配置

介绍


我们之前打印日志使用的都是同步日志,而对于高并发低延迟要求的系统中我们可以设置异步。


异步:打印日志时候是把打印任务放入内存队列后就直接返回了,而不像同步日志一样会每次直接进行写磁盘。



实例使用


对FileAppender使用异步日志,logback.xml配置如下:


<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <!--  配置fileAppender  -->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <!--  配置文件路径  -->
        <file>${log_dir}/logback.log</file>
        <!--  日志格式设置  -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
        <!--  配置日志等级过滤器:LevelFilter  -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--  指定日志等级  -->
            <level>error</level>
            <!--  若是>=error(匹配)直接通过不过滤 -->
            <onMatch>ACCEPT</onMatch>
            <!--  若是<error(不匹配)过滤掉 -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!--  设置异步日志 -->
    <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
        <!--设为0表示队列达到80%,也不丢弃任务-->
        <discardingThreshold>0</discardingThreshold>
        <!--队列的深度,该值会影响性能,默认256,这里设置1024-->
        <queueSize>1024</queueSize>
        <!--队列满了直接丢弃要写的消息-->
        <neverBlock>true</neverBlock>
        <!--One and only one appender may be attached to AsyncAppender,添加多个的话后面的会被忽略-->
        <appender-ref ref="file"/>
    </appender>
    <!--  设置root的日志等级为ALL,以及引用上面的name为console的appender  -->
    <root level="ALL">
        <appender-ref ref="file"/>
    </root>
</configuration>


这里的配置信息是查看的一篇博客。

测试程序:


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            LOGGER.error("error");
            LOGGER.warn("warn");
            LOGGER.info("info");
            LOGGER.debug("debug");
            LOGGER.trace("trace");
        }
    }
}




说明:通常对于在进行高并发情况下使用异步日志!



源码初探


通过使用appender为AsyncAppender来引用配置文件中定义的appender来达到异步效果,首先看下AsyncAppender的继承关系:



AsyncAppender类中包含一个配置属性:



AsyncAppender的父类AsyncAppenderBase包含的配置属性:



对应属性描述如下:


<!--可设置属性介绍-->
<!--队列的深度,该值会影响性能,默认256-->
<queueSize>512</queueSize>
<!--设为0表示队列达到80%,也不丢弃任务-->
<discardingThreshold>0</discardingThreshold>
<!--日志上下文关闭后,AsyncAppender继续执行写任务的时间,单位毫秒-->
<maxFlushTime>1000</maxFlushTime>
<!--队列满了直接丢弃要写的消息-->
<neverBlock>true</neverBlock>
<!--是否包含调用方的信息,false则无法打印类名方法名行号等-->
<includeCallerData>true</includeCallerData>
<appender-ref ref="file"/>


其中需要知道的是在异步日志器中属性包含了BlockingQueue接口即队列,该接口有多个实现类:



在AsyncAppenderBase的start()方法,默认获取了ArrayBlockingQueue这个实现类:



许多配置信息其实间接的与这个队列实现类有关!!!这里暂时了解即可,不再进行深入探究。



自定义Logger

介绍


目的:根据不同的业务需求时可进行自定义Logger,例如自己定义的消息使用自定义输出到控制台,对于系统的消息可输出到日志文件。可通过使用LoggerFactory.getLogger(指定类或包名);来获取指定配置的logger实例。


使用方式


<!--自定义logger   additivity:表示是否从 rootLogger继承配置-->
<logger name="xyz.changlu" level="debug" additivity="false">
    <appender-ref ref="console"/>
</logger>


这里使用<logger>标签来作为自定义标签。

additivity:设置一个布尔值表示是否要继承RootLogger。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
286 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
1月前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
81 5
|
2月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
318 3
|
1月前
|
Java 中间件
SpringBoot入门(6)- 添加Logback日志
SpringBoot入门(6)- 添加Logback日志
35 1
|
2月前
|
存储 Java Android开发
Android|记一个导致 logback 无法输出日志的问题
在给一个 Android 项目添加 logback 日志框架时,遇到一个导致无法正常输出日志的问题,这里记录一下。
39 2
|
2月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
132 1
|
2月前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
10天前
|
监控 安全 Apache
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。
|
20天前
|
存储 监控 安全
什么是事件日志管理系统?事件日志管理系统有哪些用处?
事件日志管理系统是IT安全的重要工具,用于集中收集、分析和解释来自组织IT基础设施各组件的事件日志,如防火墙、路由器、交换机等,帮助提升网络安全、实现主动威胁检测和促进合规性。系统支持多种日志类型,包括Windows事件日志、Syslog日志和应用程序日志,通过实时监测、告警及可视化分析,为企业提供强大的安全保障。然而,实施过程中也面临数据量大、日志管理和分析复杂等挑战。EventLog Analyzer作为一款高效工具,不仅提供实时监测与告警、可视化分析和报告功能,还支持多种合规性报告,帮助企业克服挑战,提升网络安全水平。
|
2月前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1698 14
下一篇
DataWorks