日志是应用的镜子,可以发现应用中的问题,重要性不言而喻。
打造一智能日志模块,让运维朝着自动化方向大步迈进。提高效率,降低成本,这也是一种创造利润的途径。
如今网络越来越方便,这个日志组件除了常规的记录日志外,可以对日志分等级显示不同的颜色,支持按日期切割,支持控制文件大小及定时清理若干天的记录文件。更重要的是,一种远程诊断的方案。公司开辟一公共外网ftp服务器,这不费不少钱吧,也放心这服务器占不满空间。现场遇到问题了,只要网络不是问题,就都不是问题,这日志组件可由运维人员去触发机器一下,或让客户手工摁一下,机器主动ftp上去一个日志文件。或者应用里增加自主触发机制,当应用执行到FATAL,严重错误不该出现的地方时,或应用异常崩溃时,主动触发上报日志文件FTP到后台服务器。后台服务器增加监视,发现是致命bug日志,主动推送邮件到开发或运维人员邮箱。开发或运维人员总是盯数据肯定不感兴趣,往往不能及时发现问题,那么这种让问题主动去找你的思路,让你主动的去关注问题,而不是等到问题暴露了才去解决。
这样运维人员还用大热天到处跑吗?还用动不动就抓包?出差?都不用的。只要网络ok,数据跑路的都能实现不用人参与。
那么一个日志模块,什么是你想要的功能?
对我而言,这几点是必须的。
1·.日志分等级记录,可控制等级。
2.不同等级日志显示不同颜色。
3.增加是否启用日志输出到文件开关,可以选择把日志保存到文件中.
4.写文件属于耗时操作,这块要考虑异步写日志,不能阻塞应用或影响应用运行时间效率。
5.按日期生成日志文件,可配置保留多少天,超过设定的天数则自动清除超过天数的日志。
6.可增加参数设定限制日志文件的大小,超过限制大小可选择是从头覆盖还是删除重记。
以下为增强功能,
7.发现应用异常时,不但主动记录日志,而且主动上报异常日志文件。可通过FTP服务自动上报日志到后台FTP服务器,
把问题的发现,变被动为主动。当应用出现不该出现的问题时,主动上报。助力自动化运维。
即只要网络是OK的,不再让用户跑路。不再被动的发现问题。让问题解决于被暴露之前,提高产品的口碑与竞争力。
8.可以由运维人员去简单的触发一下,就把终端的日志通过网络传上去,不用再去找线,不用再去找U盘,不用再去想法把日志拷贝出来再带回电脑上发给开发人员。
9.可以由后台参数控制或比如发送短信,自动控制让某一台终端上报日志。
10.支持日志压缩,压缩为zip或7z等文件,缩小体积,便于储存和通过网络传输。
日志的意义在于排查问题和运维。
若所有机器都把日志上传上去则失去了意义,且服务器也顶不住。而这种被动的触发,针对某个机器的上送是可行的且有价值的。并且还可以在应用中增加当应用出现客户没发现缺不该出现的问题时,主动触发上报异常日志。这为提高产品的稳定性,杜绝问题造成的严重性而未发现提供先机。当某天发现一机器偶然吐出一异常的bug日志时,且这日志暴露的问题若不解决将造成严重后果,而你恰好在你的邮箱里看到,这就在不知不觉中主动发现了问题。不用运维人员去找你,客户去找你,机器向你求救了。那么,救救它吧。
OK,按着这个思想,以下是一个实现,c语言的log模块:
至于FTP部分,文件压缩为zip部分,用go来写,更容易。这也就是为啥用go来开发嵌入式很合适。要是让你用c写一个ftp,你试试?
这就体现了用go开发嵌入式linux的强大之处。用go,简短的几行代码就ok了。且在终端上跑的很溜。
/** 日志打印示例。 使用: mylog(DEBUG, "This is debug info\n"); 结果: [2018-07-22 23:37:27:172] [DEBUG] [main.cpp:5] This is debug info 默认打印当前时间(精确到毫秒)、文件名称、行号。 */ #include <stdarg.h> #include <stdio.h> #include <string.h> #include <time.h> #include <unistd.h> #include <sys/time.h> #include <sys/stat.h> #include <pthread.h> #include <sys/msg.h> #include <sys/ipc.h> #include <errno.h> #include <dirent.h> #include <stdlib.h> #include "log.h" //#ifndef LOGLEVEL //#define LOGLEVEL DEBUG //#endif // 使用了GNU C扩展语法,只在gcc(C语言)生效, // g++的c++版本编译不通过 static const char* s_loginfo[] = { [ERROR] = "ERROR", [WARN] = "WARN", [INFO] = "INFO", [DEBUG] = "DEBUG", }; static char file_names[LOGFILE_MAXCOUNT][LOGFILE_NAMELENTH]; //记录文件名前缀(最好取自终端编号) static char file_prifix[LOGFILE_NAMELENTH]; //linux消息队列 static int s_msg_id; static int r_msg_id; #define MSG_TYPE 1001 #define MAX_TEXT 1024 struct msg_st{ long int msg_type; char text[MAX_TEXT]; }; static pthread_t tid; //============================================= static void get_timestamp(char *buffer) { time_t t; struct tm *p; struct timeval tv; int len; int millsec; t = time(NULL); p = localtime(&t); gettimeofday(&tv, NULL); millsec = (int)(tv.tv_usec / 1000); /* 时间格式:[2011-11-15 12:47:34:888] */ len = snprintf(buffer, 32, "[%04d-%02d-%02d %02d:%02d:%02d:%03d] ", p->tm_year+1900, p->tm_mon+1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec, millsec); buffer[len] = '\0'; } //获取当前时间 static int get_curtime( char* outTimeStr ) { int ret = 0; time_t tTime; struct tm *tmTime; struct timeval mTime; time( &tTime ); tmTime = localtime( &tTime ); gettimeofday( &mTime, NULL ); sprintf( outTimeStr, "%04d%02d%02d%02d%02d%02d", tmTime->tm_year + 1900, tmTime->tm_mon + 1, tmTime->tm_mday, tmTime->tm_hour, tmTime->tm_min, tmTime->tm_sec ); return ret; } //创建文件夹 static int create_dir(const char *sPathName) { char dirName[256]; strcpy(dirName, sPathName); int i,len = strlen(dirName); for(i=1; i<len; i++) { if(dirName[i]=='/') { dirName[i] = 0; if(access(dirName, 0)!=0) { if(mkdir(dirName, 0755)==-1) { fprintf(stderr,"mkdir error\n"); return -1; } } dirName[i] = '/'; } } return 0; } //获取文件大小 static unsigned long get_size(const char *path) { unsigned long filesize = -1; struct stat statbuff; if(stat(path, &statbuff) < 0){ return filesize; }else{ filesize = statbuff.st_size; } return filesize; } //文件是否存在,返回 1文件存在,返回0,文件不存在 static int file_exists(char *filename) { return (access(filename, 0) == 0); } static int read_filelist(char *basePath) { DIR *dir; struct dirent *ptr; char base[1000]; int count = 0; if ((dir=opendir(basePath)) == NULL) { fprintf(stderr,"Open dir error..."); return -1; } while ((ptr=readdir(dir)) != NULL) { //printf("count=%d\n",i++); if(strcmp(ptr->d_name,".")==0 || strcmp(ptr->d_name,"..")==0) ///current dir OR parrent dir continue; else if(ptr->d_type == 8) ///file { printf("f_name:%s/%s\n",basePath,ptr->d_name); sprintf(file_names[count],"%s",ptr->d_name); count++; if(count > LOGFILE_MAXCOUNT -1){ fprintf(stderr,"error!only allow files count=%d\n",LOGFILE_MAXCOUNT); return -2; } } else if(ptr->d_type == 10) ///link file printf("l_name:%s/%s\n",basePath,ptr->d_name); else if(ptr->d_type == 4) ///dir { printf("d_name:%s/%s\n",basePath,ptr->d_name); printf("this is a dir\n"); continue; //是目录,继续递归读目录下面的 //memset(base,'\0',sizeof(base)); //strcpy(base,basePath); //strcat(base,"/"); //strcat(base,ptr->d_name); //read_filelist(base); } } closedir(dir); return 0; } //处理日志文件是否保留 //原理算法:把日期转换成时间戳,然后由配置的允许保留的天数换算出一个时间范围, //在遍历日志目录中所有的文件名,提取出日期,在这个时间范围内的保留,否则删除 //关键的地方,算出这个允许保留文件的时间范围,原理是日期转时间戳,再时间戳转日期 static int file_alives_proc() { int ret = 0; char curtime[20]; //当前日期时间 char deadtime[20]; //截止的历史日期 printf("file_alives_proc:\n"); get_curtime(curtime); //只取日期,省去时间 memset(&curtime[8],0x30,6); printf("ailve maxdays:%d\n",LOGFILE_ALIVEDAYS); printf("curtime:%s\n",curtime); struct tm* tmp_time = (struct tm*)malloc(sizeof(struct tm)); //字符串转时间 strptime(curtime,"%Y%m%d%H%M%S",tmp_time); time_t t = mktime(tmp_time); printf("t now = %ld\n",t); free(tmp_time); time_t t1 = t - LOGFILE_ALIVEDAYS*24*60*60; //再把t1转换为时间,即时间戳转时间 struct tm *p; p = gmtime(&t1); //日期时间转字符串,由于只比较日期,因此忽略时间 strftime(deadtime, sizeof(deadtime), "%Y%m%d000000", p); printf("deadtime:%s\n",deadtime); //以上获取到了curtime和deadtime,有了这个时间范围,接下来就去找这个范围的日志 //日志文件日期在这个范围内的保留,否则删除 for(int i = 0; i < LOGFILE_MAXCOUNT; i++ ) { if(strlen(file_names[i]) > 0) { printf("file_name=%s\n",file_names[i]); char ftime[20]; memset(ftime,0,20); memset(ftime,0x30,8); //关键,这个截取不能错 memcpy(ftime,&file_names[i][strlen(LOGFILE_PREFIX)+1+strlen(file_prifix)],8); printf("file_time=%s\n",ftime); //开始比较 是否在日期范围内 if(memcmp(ftime,deadtime,8) > 0) { //大于截止日期的文件保留 printf("%s------keep alive\n",file_names[i]); }else{ printf("%s----------------dead,need remove!\n",file_names[i]); //删除文件 char dfname[50]; sprintf(dfname,"%s/%s",LOGFILE_PATH,file_names[i]); remove(dfname); } // }else{ //printf("fname=NULL\n"); } } return ret; } int open_msg(void) { key_t msgkey; if ((msgkey = ftok("/tmp", 'a')) < 0) { printf("send ftok failed!\n"); return -1; } printf("----msgkey is %d\n",msgkey); if ((s_msg_id = msgget(msgkey, IPC_CREAT | 0666)) == -1) { printf("msgget failed!\n"); return -1; } printf("----s_msg_id is %d\n",s_msg_id); msgctl(s_msg_id,IPC_RMID,0);//先删除,否则可能满,因其生命周期同内核 if ((s_msg_id = msgget(msgkey, IPC_CREAT | 0666)) == -1) { printf("msgget failed!\n"); return -1; } r_msg_id = s_msg_id; return 0; } static int send_msg(char *buf,int len){ struct msg_st data; data.msg_type = MSG_TYPE; strcpy(data.text,buf); //s_msg_id = 0; if(msgsnd(s_msg_id,&data,len,IPC_NOWAIT) == -1){ printf("msgsnd failed.\n"); perror("msgsnd"); return -1; } return 0; } //若大量连续发的太快,收的太慢,会导致发送失败 static int recv_msg(void) { int rsize; struct msg_st data; int msgtype = MSG_TYPE; char tmpfname[ 50 ]; char tmpTime[ 14 + 1 ]; FILE* fp; unsigned long filesize; memset(data.text,0,sizeof(data.text)); /*接收消息队列*/ //阻塞接收 rsize = msgrcv(r_msg_id,&data,sizeof(data.text),msgtype,MSG_NOERROR ); if(rsize == -1) { if (errno != ENOMSG) { perror("msgrcv"); } printf("No message available for msgrcv()\n"); return -1; } else { //printf("message received: %s\n", data.text); get_curtime( tmpTime ); sprintf( tmpfname, "%s/%s%s_%8.8s.log", LOGFILE_PATH, LOGFILE_PREFIX,file_prifix, tmpTime ); fp = fopen( tmpfname, "a" ); if ( NULL == fp ) { fprintf(stderr,"failed to open file,filename=%s\n",tmpfname); return -2; } else { filesize = get_size(tmpfname); // printf("filesize=%u\n",filesize); if((filesize/1024) > LOGFILE_MAXSIZE){ fprintf(stderr,"failed to write log,only allow maxsize=%ukb,current size=%ukb\n",LOGFILE_MAXSIZE,filesize/1024); fclose(fp); return -3 ; } //printf("%s\n",tmpfname); fprintf( fp, "%s", data.text ); fclose(fp); } } return 0; } void mylog1(const char* filename, int line, enum LogLevel level, const char* fmt, ...) { if(level > LOGLEVEL) return; va_list arg_list; char buf[1024]; char sbuf[MAX_TEXT]; memset(buf, 0, 1024); memset(sbuf,0,MAX_TEXT); va_start(arg_list, fmt); vsnprintf(buf, 1024, fmt, arg_list); char time[32] = {0}; // 去掉*可能*存在的目录路径,只保留文件名 const char* tmp = strrchr(filename, '/'); if (!tmp) tmp = filename; else tmp++; get_timestamp(time); switch(level){ case DEBUG: //绿色 sprintf(sbuf,"\033[1;32m%s[%s] [%s:%d] %s\n\033[0m", time, s_loginfo[level], tmp, line, buf); break; case INFO: //蓝色 sprintf(sbuf,"\033[1;34m%s[%s] [%s:%d] %s\n\033[0m", time, s_loginfo[level], tmp, line, buf); break; case ERROR: //红色 sprintf(sbuf,"\033[1;31m%s[%s] [%s:%d] %s\n\033[0m", time, s_loginfo[level], tmp, line, buf); break; case WARN: //黄色 sprintf(sbuf,"\033[1;33m%s[%s] [%s:%d] %s\n\033[0m", time, s_loginfo[level], tmp, line, buf); break; } printf("%s",sbuf); #if(LOGFILE_ENABLE) //记录日志到文件 send_msg(sbuf,strlen(sbuf)); #endif va_end(arg_list); } //考虑了一点儿效率,操作IO比较耗时,那就异步写入吧 //想用单独的线程,负责写日志到文件,自己实现消息队列? //不这么做了,直接使用linux的消息队列吧 static void* thread_writelog(void* args) { mylog(INFO,"thread_writelog begin...\n"); while(1) { recv_msg(); usleep(20*1000); } } //参数prifix,日志文件名前缀,传终端编号 int init_log(char *prifix) { int ret = 0; printf("init log:\n"); #if(LOGFILE_ENABLE) //检查目录是否存在 ret = create_dir(LOGFILE_PATH); if(0 != ret){ return ret; } printf("create dir %s\n success!\n",LOGFILE_PATH); ret = read_filelist(LOGFILE_PATH); if(0 != ret){ printf("read_filelist err!,ret =%d\n",ret); //return ret; } //文件名前缀 if(strlen(prifix) > 0){ strcpy(file_prifix,prifix); } //处理是否保留历史记录文件 file_alives_proc(); //for(int i = 0; i<30;i++){ // printf("%s\n",file_names[i]); //} //创建消息队列 ret = open_msg(); if(0 != ret){ return ret; } printf("create msg quene success!\n"); //创建写日志文件线程 ret = pthread_create(&tid, NULL, thread_writelog, NULL); if(0 != ret) { fprintf(stderr, "couldn't create thread_writelog, errno %d\n", ret); } else { printf("create thread_writelog success!\n"); } printf("init log success!enabled log file...\n"); #else printf("init log success!no enable log file...\n"); #endif return ret; } // //
/* //使用demo: //mylog(DEBUG,"hello world!\n"); //输出:[2019-07-26 14:31:51:882] [DEBUG] comLib.c:1257] hello world! // //目前只为个人使用,暂无考虑线程安全,高效率和高并发 //考虑了一点儿效率,写文件操作IO比较耗时,因此日志使用了异步写入,linux消息队列。 //因linux的消息队列,容量和长度有限制,因此若单个消息超1024byte或并发发送几千个消息 //且发送速度很快,大于了队列的接收速度,那么肯定,会发送失败 */ #ifndef LOG_H_ #define LOG_H_ #ifdef __cplusplus extern "C" { #endif enum LogLevel { ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4, }; void mylog1(const char* filename, int line, enum LogLevel level, const char* fmt, ...) __attribute__((format(printf,4,5))); #define mylog(level, format, ...) mylog1(__FILE__, __LINE__, level, format, ## __VA_ARGS__) //================================================================ //============日志模块操作接口,配置和使用都在下面================ //日志级别定义,小于该级别的日志方能输出 #ifndef LOGLEVEL #define LOGLEVEL DEBUG #endif //目前暂只支持管理目录下的30个日志文件名,文件名长度50以内。大了不处理 #define LOGFILE_MAXCOUNT 30 #define LOGFILE_NAMELENTH 50 //===========日志文件的配置======================================= //是否启用记录日志到文件功能 #define LOGFILE_ENABLE 1 //日志文件的路径,后面不能带"/" #define LOGFILE_PATH "/log" //日志文件名称的前缀 #define LOGFILE_PREFIX "log_b503_" //日志文件存在的时间 单位(天),会自动删除当前日期-ALIVEDAYS 之前的文件 //限制日志最长保留时间不能超 LOGFILE_MAXCOUNT 天 #define LOGFILE_ALIVEDAYS 7 //单个日志文件的限制大小 单位kb #define LOGFILE_MAXSIZE 1*1024 //================================================================ //日志模块初始化,若要记录日志到文件中,必须init_log且开启LOGFILE_ENABLE //若不需要记录日志到文件功能,则不要调用init_log(); //调用了也没事,只是别开启LOGFILE_ENABLE //参数prifix,日志文件名前缀,最好传终端唯一编号 extern int init_log(char *prifix); #ifdef __cplusplus }; #endif #
附:
go实现的简易FTP功能,先对日志文件进行zip压缩,然后ftp至后台服务器。
package main import ( "archive/zip" "flag" "fmt" "io" "log" "os" "strings" "github.com/dutchcoders/goftp" "github.com/larspensjo/config" ) var ( ftp *goftp.FTP conFile = flag.String("ftpcfg", "/ftpcfg.ini", "config file") Server string = "127.0.0.1:21" User string = "" Pwd string = "" ) func checkErr(err error) { if err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error()) } } /** @files:需要压缩的文件 @compreFile:压缩之后的文件 */ func CompressZip(files []*os.File, zipfileName string) (err error) { zipfile, err := os.Create(zipfileName) if err != nil { return err } defer zipfile.Close() zw := zip.NewWriter(zipfile) defer zw.Close() for _, file := range files { err := compressZip(file, zw) if err != nil { return err } file.Close() } return nil } /** 功能:压缩文件 @file:压缩文件 @prefix:压缩文件内部的路径 @tw:写入压缩文件的流 */ func compressZip(file *os.File, zw *zip.Writer) error { info, err := file.Stat() if err != nil { log.Println("压缩文件失败:", err.Error()) return err } // 获取压缩头信息 head, err := zip.FileInfoHeader(info) if err != nil { log.Println("压缩文件失败:", err.Error()) return err } // 指定文件压缩方式 默认为 Store 方式 该方式不压缩文件 只是转换为zip保存 head.Method = zip.Deflate fw, err := zw.CreateHeader(head) if err != nil { log.Println("压缩文件失败:", err.Error()) return err } // 写入文件到压缩包中 _, err = io.Copy(fw, file) file.Close() if err != nil { log.Println("压缩文件失败:", err.Error()) return err } return nil } func main() { fmt.Println("Hello Go") defer func() { if r := recover(); r != nil { fmt.Printf("crash occurred!capture:%s\n", r) } }() // fmt.Println(os.Args) //打印切片内容 // for i := 0; i < len(os.Args); i++ { // fmt.Println(os.Args[i]) // } var fname string flag.StringVar(&fname, "fname", "", "file name") flag.Parse() flag.Usage() //获取当前路径 file, _ := os.Getwd() cfg, err := config.ReadDefault(file + *conFile) checkErr(err) //获取配置文件中的配置项 Server, err = cfg.String("SERVERCONFIG", "Server") User, err = cfg.String("USER", "User") Pwd, err = cfg.String("USER", "Pwd") //压缩ZIP var filezips []*os.File var filezip *os.File if filezip, err = os.Open(fname); err != nil { panic(err) } filezips = append(filezips, filezip) fmt.Println("CompressZip:" + fname + ".zip") err = CompressZip(filezips, fname+".zip") if err != nil { panic(err) } fmt.Println("User:" + User) fmt.Println("->begin connect server:" + Server) // For debug messages: goftp.ConnectDbg("ftp.server.com:21") if ftp, err = goftp.ConnectDbg(Server); err != nil { panic(err) } //ftp.debug = true defer ftp.Close() fmt.Println("->Successfully connected !!") // Username / password authentication if err = ftp.Login(User, Pwd); err != nil { panic(err) } fmt.Println("->Login success!") if err = ftp.Cwd("/"); err != nil { panic(err) } var curpath string if curpath, err = ftp.Pwd(); err != nil { panic(err) } fmt.Printf("Current path: %s\n", curpath) // Get directory listing // var files []string // if files, err = ftp.List(""); err != nil { // panic(err) // } // fmt.Println("Directory listing:/n", files) // Upload a file var fileup *os.File if fileup, err = os.Open(fname + ".zip"); err != nil { panic(err) } fmt.Println("->begin upload file...") fpath := fname + ".zip" lastidx := strings.LastIndex(fname+".zip", "/") if lastidx != -1 { fpath = fname[lastidx:] + ".zip" } fmt.Printf("fpath:%s\n", fpath) if err := ftp.Stor(fpath, fileup); err != nil { panic(err) } fmt.Println("->upload file over!") err = os.Remove(fname + ".zip") //上传成功,删除zip文件 if err != nil { fmt.Println("file remove Error!") panic(err) } }
编译与使用:
GOOS=linux GOARCH=arm GOARM=7 go build ftp.go ftp -fname=/log/log_b503_20190730.log root@b_lcd:/app/opt ./ftp -fname=/log/log_b503_20190730.log Hello Go Usage of ./ftp: -fname string file name -ftpcfg string config file (default "/ftpcfg.ini") User:qq8864 ->begin connect server:015.3vftp.com:21 2019/07/30 17:44:08 begin receiveLine... 2019/07/30 17:44:09 < 220 Serv-U FTP Server v6.4 for WinSock ready... 2019/07/30 17:44:09 receiveLine over! 2019/07/30 17:44:09 220 Serv-U FTP Server v6.4 for WinSock ready... ->Successfully connected !! 2019/07/30 17:44:09 > USER qq8864 2019/07/30 17:44:09 begin receiveLine... 2019/07/30 17:44:09 < 331 User name okay, need password. 2019/07/30 17:44:09 receiveLine over! 2019/07/30 17:44:09 > PASS sars1212 2019/07/30 17:44:09 begin receiveLine... 2019/07/30 17:44:09 < 230 User logged in, proceed. 2019/07/30 17:44:09 receiveLine over! ->Login success! 2019/07/30 17:44:09 > CWD / 2019/07/30 17:44:09 begin receiveLine... 2019/07/30 17:44:09 < 250 Directory changed to / 2019/07/30 17:44:09 receiveLine over! 2019/07/30 17:44:09 > PWD 2019/07/30 17:44:09 begin receiveLine... 2019/07/30 17:44:09 < 257 "/" is current directory. 2019/07/30 17:44:09 receiveLine over! Current path: / ->begin upload file... fpath:/log_b503_20190730.log 2019/07/30 17:44:09 > TYPE I 2019/07/30 17:44:09 begin receiveLine... 2019/07/30 17:44:11 < 200 Type set to I. 2019/07/30 17:44:11 receiveLine over! 2019/07/30 17:44:11 > PASV 2019/07/30 17:44:11 begin receiveLine... 2019/07/30 17:44:11 < 227 Entering Passive Mode (52,128,243,181,6,1) 2019/07/30 17:44:11 receiveLine over! 2019/07/30 17:44:11 > STOR /log_b503_20190730.log 2019/07/30 17:44:11 Connecting to 015.3vftp.com:1537 2019/07/30 17:44:11 begin receiveLine... 2019/07/30 17:44:11 < 150 Opening BINARY mode data connection for log_b503_20190730.log. 2019/07/30 17:44:11 receiveLine over! 2019/07/30 17:44:11 a1:150 Opening BINARY mode data connection for log_b503_20190730.log. 2019/07/30 17:44:11 begin receiveLine... 2019/07/30 17:44:13 < 226-Maximum disk quota limited to 102400 kBytes 2019/07/30 17:44:13 receiveLine over! 2019/07/30 17:44:13 begin receiveLine... 2019/07/30 17:44:13 < Used disk quota 64 kBytes, available 102335 kBytes 2019/07/30 17:44:13 receiveLine over! 2019/07/30 17:44:13 begin receiveLine... 2019/07/30 17:44:13 < 226 Transfer complete. 2019/07/30 17:44:13 receiveLine over! 2019/07/30 17:44:13 a2:226-Maximum disk quota limited to 102400 kBytes Used disk quota 64 kBytes, available 102335 kBytes 226 Transfer complete. ->upload file over!