打印UDF日志

在开发UDF的过程中打印日志有助于用户进行UDF代码调试,本文为您介绍在MaxCompute中如何打印及查看UDF日志。

景信息

当前MaxCompute的UDF运行在远端的机器上,很多用户代码在本地测试成功以后,提交到分布式的环境下,可能由于数据不同或者考虑不周,导致结果不符合预期,因此用户需要反复修改UDF进行代码调试,此时Log日志的打印就显得尤为重要。

目前MaxCompute提供的日志查看方法是通过在Logview中查看对应Worker的StdOut和StdErr来查看对应日志输出,即用户需要通过System.out.println()或者System.err.println()方式来打印。

部分用户会使用第三方依赖(比如Log4j)打印日志,这就需要用户进行一定的配置,重定向到StdOut和StdErr中,如果将日志打印到文件则无法进行查看。但是Log4j大多会在多个三方依赖中被使用,当多个配置同时存在时,可能导致用户无法确定日志最终的输出位置。所以本文为您提供一个简单的方法,即封装System.out和System.err形成一个日志类,来满足日志打印的需求。

如果您需要在开发UDF的过程中打印日志信息,可参考如下开发流程:

  1. 编写代码

    封装一个Logger类,并在UDF代码中引入该Logger。如果需要打印DEBUG日志,您可在MyUdf类的setup方法中自定义DEBUG相关的Flag。以便后续执行SQL查询时控制DEBUG日志的打印。

  2. 调试代码

    在本地执行测试代码,查看执行结果是否符合预期。

  3. 打包注册

    将Logger类和UDF类打入一个JAR包中,将该JAR包作为资源上传至MaxCompute,并注册UDF函数。

  4. 在MaxCompute中运行

    在查询数据代码中调用UDF函数,并在Logview中查看StdOut和StdErr中打印的日志信息。

编写代码

  1. 封装Logger类。

    您可以参照一般Log的功能,封装对应的INFO、WARNING、ERROR及DEBUG信息。此处以封装一个简单的MyLogger类为例,日志打印信息包含当前类名、时间和日志级别等信息。代码示例如下:

    import java.io.PrintWriter;
    import java.io.StringWriter;
    import java.text.SimpleDateFormat;
    
    public class MyLogger {
    
      private static final SimpleDateFormat DATEFORMATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
      public static MyLogger getLogger(String name) {
        return new MyLogger(name);
      }
    
      private static boolean debug = false;
    
      private String prefix;
    
      public static void setDebug(boolean enableDebug) {
        debug = enableDebug;
      }
    
    
      public MyLogger(String name) {
        if (name == null) {
          this.prefix = "";
        } else {
          this.prefix = name + " - ";
        }
      }
    
      private String logCurrentTime() {
        return "[" + dateFormat.format(System.currentTimeMillis()) + "]";
      }
    
      public void log(String msg) {
        info(msg);
      }
    
      public void info(String msg) {
        System.out.println(logCurrentTime() + " " + prefix + msg);
      }
    
      public void debug(String msg) {
        if (MyLogger.debug) {
          System.out.println(logCurrentTime() + " [DEBUG] " + prefix + msg);
        }
      }
    
      public void warn(String msg) {
        System.err.println(logCurrentTime() + " [WARNING] " + prefix + msg);
      }
    
      public void warn(String msg, Throwable ex) {
        warn(msg);
        System.err.println(getStackTrace(ex));
      }
    
      public void error(String msg) {
        System.err.println(logCurrentTime() + " [ERROR] " + prefix + msg);
      }
    
      public void error(String msg, Throwable ex) {
        error(msg);
        System.err.println(getStackTrace(ex));
      }
    
      private String getStackTrace(Throwable t) {
        StringWriter sw = new StringWriter();
        t.printStackTrace(new PrintWriter(sw));
        return sw.toString();
      }
    
    }
  2. 在UDF的代码中引入已创建的Logger类。

    以创建一个MyUdf类为例,其功能为将表中的String类型数据转换为Integer类型,您可以使用如下代码引入MyLogger类。

      public static final MyLogger logger = MyLogger.getLogger(MyUdf.class.getName());

    完整的代码示例如下:

    public class MyUdf extends UDF {
      
      public static final MyLogger LOGGER = MyLogger.getLogger(MyUdf.class.getName());
    
      @Override
      public void setup(ExecutionContext ctx) throws UDFException, IOException {
        logger.info("setup MyUdf");
      }
        
      public Integer evaluate(String s) {
        logger.debug("input s: " + s);
        int ret = 0;
        try {
          ret = Integer.parseInt(s);
        } catch (Exception e) {
          logger.error("parseInt error!", e);
        }
        return ret;
      }
    
    }
  3. 打印DEBUG日志。

    说明

    MaxCompute中处理的数据量一般较大,如果每一条数据都打印,则很快会到达数据打印上限,同时会占据大量的存储空间,此时您可以通过传入Flag的方式自由控制日志的打印。

    如果后续需要打印DEBUG日志,您可在MyUdf类的setup方法中自定义DEBUG相关的Flag,并在后续执行SQL查询时使用set语句设置该Flag,以控制本次提交的任务是否打印DEBUG日志。代码示例如下:

    • setup方法中自定义Flag

      @Override
        public void setup(ExecutionContext ctx) throws UDFException, IOException {
          logger.info("setup MyUdf");
          Properties properties = ctx.getConfigurations();
          boolean debug = Boolean.parseBoolean(properties.getProperty("odps.user.properties.mylogger.debug", "false"));
          MyLogger.setDebug(debug);
        }

      其中odps.user.properties.mylogger.debug为用户自定义的Flag参数。

    • 执行SQL时使用set语句进行设置

      set odps.user.properties.mylogger.debug=true;

调试代码

您可以在本地使用以下代码测试上述Logger类和UDF的执行结果是否符合预期,具体如下:

  • 测试代码1:执行结果中只打印INFO信息和报错栈信息,不打印DEBUG日志。

     public static void main(String[] args) throws UDFException, IOException {
        MyUdf myUdf = new MyUdf();
        myUdf.setup(new LocalExecutionContext());
        myUdf.evaluate("");
        myUdf.evaluate("1");
        myUdf.evaluate("2");
        myUdf.evaluate("3");
      }

    执行结果:image.png

  • 测试代码2:执行结果中打印DEBUG日志。

    说明

    您可以通过MyLogger.setDebug(true);语句开启DEBUG开关,方便对代码进行调试,并且在DEBUG完成以后关闭该开关,避免不必要的日志打印。

    public static void main(String[] args) throws UDFException, IOException{
            MyLogger.setDebug(true); // 打印DEBUG日志
            MyUdf MyUdf = new MyUdf();
            MyUdf.setup(new LocalExecutionContext());
            MyUdf.evaluate("");
            MyUdf.evaluate("1");
            MyUdf.evaluate("2");
            MyUdf.evaluate("3");
        }

    执行结果:image.png

打包注册

本地测试成功后,您可以将MyLogger类和MyUdf类打入一个JAR包中,然后提交至MaxCompute并进行函数注册,以便后续调用该函数。具体如下:

  1. 添加资源

    将打好的JAR包作为JAR资源添加至MaxCompute,添加资源操作请参见添加资源

  2. 注册函数

    在MaxCompute中注册名为stringToInt的函数,具体操作请参见注册函数

在MaxCompute中运行

您可以在MaxCompute中调用上述注册的stringToInt函数,并通过Logview查看打印的日志信息。

准备表数据

您需要提前准备好表数据,以供后续执行SQL查询时调用UDF。建表语句如下:

create table string_table(s string);
insert overwrite table string_table values (''), ('1'), ('2'), ('3');

通用场景(不打印DEBUG日志)

  1. 调用UDF。将string_table表的数据转换成对应的Integer类型,SQL语句如下:

    set odps.sql.type.system.odps2=true;
    select stringToInt(s) from string_table;

    运行结果:

    +------+
    | _c0  |
    +------+
    | 0    |
    | 1    |
    | 2    |
    | 3    |
    +------+
  2. 在Logview中查看StdOut和StdErr,分别打印出了INFO信息和报错栈信息。image.pngimage.png

打印DEBUG日志

  1. 调用UDF。将string_table表的数据转换成对应的Integer类型,SQL语句如下:

    set odps.sql.type.system.odps2=true;
    set odps.user.properties.mylogger.debug=true;
    
    select stringToInt(s) from string_table;

    运行结果:

    +------+
    | _c0  |
    +------+
    | 0    |
    | 1    |
    | 2    |
    | 3    |
    +------+
  2. 在Logview中查看StdOut和StdErr,可以看到StdOut中打印出了INFO信息和DEBUG信息,StdErr中打印出了报错栈信息。image.pngimage.png

    说明

    通过该方法控制DEBUG信息的打印,DEBUG开关只在代码调试时打开,而在调试完成后不设置对应的Flag,即可实现本次任务的日志打印。