一【背景】
    公司有个基于tp3的项目,此项目经历多次改版重构,有种千疮百孔的感觉,让人看着隐隐心疼,特别是在日志模块上,基于tp提供的日志功能,各种混乱不协调,且与业务的耦合非常高;
    但是这个项目又是很重要的,是最直接面向用户的入口,不出现问题则好,一出现就是一团乱,问题定位均需依赖后端接口或者第三方,也因此背过不少的锅,眼泪总是默默的流在心里;
    是时候做出改变了!
  二【选定方案】
    项目基于Thinkphp3.2版本,此版本在日志方面使用的是php内置的error_log函数实现,在不改动tp内核源码情况,很难定制自己的日志驱动,我们使用过程存在几个埋怨点:
  1)日志格式难统一,没有一个标准什么时候写日志,写什么日志等
  2)日志扩展困难,特别对于项目中特定的需要log的情况
  3)log代码与业务耦合厉害,维护起来很让人烦躁
  那么基于tp3本身的日志做些修补,意义不是太大,治标不治本,所以我们努力寻找第三方完整的开源日志库:
  1)这里我们应该首先就会想到php4log,一个很强大,很完整的日志库,网上大家对它的评价大都围绕在低性能上,在实际的测试中,性能上也确实让人堪忧,含泪略过;
  2)性能方面国产的seaslog无疑是最佳的,不论从官方测试数据还是本地的验证,也都证实seaslog是个硬货,但有几个痛点让我们放弃了它:1-需要安装扩展+添加配置;2-日志格式被限定太不自由3-扩展困难;
  3)Monolog,功能强大,可支持把日志发送到文件、socket、邮箱、数据库、各种webservices等(虽然我们就只用到文件);遵循PSR3日志接口规范,可容易与其他日志类库替换;使用composer维护相关依赖,遵循PSR4规范,自动加载;扩展性良好,可轻松定制各种格式;使用便利,这个很重要;包括symfony、laravel、cakephp框架均内置monolog;另外,我们有个基于lume框架的系统(此框架内置了monolog)已稳定使用1年多,且每天日志量再千万级,轻松支撑住;
  最终,我们就选择了monolog
  三【方案实现】
  这是一个不断挖坑,并埋坑的过程,痛并快乐
  Tp3日志模块是直接耦合在核心源码里,特别是错误或异常处理,框架本身没有提供入口给你去重新实现或扩展日志功能(tp5添加了日志的扩展口,通过钩子的方式,可怜的是tp3到tp5的升级是不可能平滑的,看到这里真是有一万只草泥马飘过)
  这里有个很重要的前提:不修改tp本身的源码,不用问为什么
  下面我们开始说明实现流程:
  1、Composer安装monolog
  #composer requrie snowair/think-monolog:dev-master
  这里我找了个对monolog针对tp3的一层封装,github如下:(其实只是做了一层monolog的启动操作)
  https://github.com/snowair/think-monolog
  注意:composer安装是一项苦力活,特别是在国内神奇的网络下,那么有解决么,伟大的开源斗士为你准备了下面代码:
  找到根目录composer.json,添加国内源,其实还是慢的,这时候需要耐心、耐性、耐操
  另外,记得添加下面的autoload,这是一个神奇的东西,它会自动生成一个autoload.php文件,完全支持PSR4规范,后面我们就要用到它
  {
     "name": "test",
     "require": {
         "snowair/think-monolog": "dev-master"
     },
     "autoload": {
       "autoload": {
         "psr-4": {"App\\": "app/"}
       }
     },
     "repositories": {
       "packagist": {
         "type": "composer",
         "url": "https://packagist.phpcomposer.com"
       }
     }
 }
  装好后,根目录下就有vendor,完美拥有monolog。革命成功走出第一步
  
  2、将monolog添加到框架自动加载
  根目录index.php,添加上面自动加载文件autoload.php
   
  此操作,可实现后面调用monolog过程可直接引入,如
   
  3、首先我们要接管tp本身的错误处理器
  研究了下tp核心代码,他的错误处理是直接在代码里写死的,在Think.class.php下:
   
 
  这也是我们很难去扩展他的原因,所以我们第一想法就是直接改源码,这太不和谐了。经过一夜的冥思,想到了一个绝妙的方案:
  1)入口文件index.php添加了一个异常处理类,所以异常可方便的可一个类中处理掉
  感兴趣的我这可以提供此类
   
 
  2)异常类中接管register_shutdown_function
  注意,此处不能接管set_error_handler、set_exception_handler,原因跟他们3个的机制有关,这个我后面另外详细说明:
   
 
  3)添加tp的action_begin行为来接管set_error_handler、set_exception_handler
   
  到此tp的错误及异常情况均已接管到异常类下,我们可以自由实现异常逻辑,包括写log
  4)通用的错误页面
  支持了pc和h5端通用的错误页,在出现致命错误时记录log的同时也会重定向到错误页的展示,以便在用户出错情况下,可以提供给我们查下bug的依据
   
  错误页效果:其中error code为每次请求的位置requestid,可通过日志系统唯一定位到
   
  4、统一访问log的实现
  1)同样用到tp的行为,添加2个行为,分别对应action_beigin和app_end
  即在php进程的开始和结束我们做行为操作,行为其实就类似钩子,执行到某个点时会自动触发
   
 
   
 
  2)明确定义了日志的格式,字段等,保证所有访问均可记录详细log,且不需要修改业务代码
  3)为了更好的使用日志,我们定义了几个可变化的字段,可在业务过程中使用:
  elk_mor_info:需要额外记录的log均放置在此字段下,使用例子如下
  record($msg, $key = '', $type = 'elk')
  $msg-内容,json格式
  $key-二级json格式字段名,几个通用定义
  curl_request curl请求第三方参数
  curl_response curl请求返回结果
  curl_time curl请求处理时间
  curl_getinfo curl请求info信息
  error_sys 系统错误
  Info_xxxx 其他信息类型
  $type-需定义为’elk_more_info’
  大家需严格按照定义的规则进行记录,否则容易导致日志格式混乱
   
 
   
  response:tp3没找到捕捉最终返回的content值,这里使用的方法是,在controller的基类basecontroller里设置统一的返回格式:
   
 
  在此处做response的收集,这里收集是针对ajax,对于面向用户访问的项目,很多是直接访问html页面,这时候其实是没有response这个东西。
  另外,如果没有调用到通用返回方法,但又想添加response值,也可以类似elk_more_info方式进行设置收集
  5、需要注意的点
  1)业务开发过程,尽量不要使用exit()、die()来结束,因为这2个方法都是直接终止脚本执行,表示结束一个php进程,之后的代码将都不会被执行,包括行为里面的函数或者各种钩子函数都不会被触发到。
  此情况,一般情况均可使用return来代替,不过在业务逻辑上就需要做相应处理
  2)如果非要用exit或die时,又要记录log,这里也提供了在exit之前强制写log的函数供调用:
  Logger::foreWrite() -- 无需带参数,它的作用是将内存中的log及当前需要记录的信息强制写入的文件中
  3)为了更好控制log,添加了几个log相关的配置,配置文件在每个模块下,即只对相应模块有效,如Event模块/Apps/Event/Conf/config.php
   
 
  在实际环境中,可根据业务需要自行调整
  6、日志的后续
  日志存放位置:/home/www/log/xxxx/  
  并根据模块划分如Event/2017-07-21.log,每天每个模块一份日志
  公司已有完善的log监控系统,基于ELK做的二次开发,添加log采集程序即可完成log系统接入,这时候查log就各种方便:
  
  从此妈妈再也不担心查不到bug了
  四【最后的话】
  以上是基于thinkphp3上monolog实现的详细思路,大家如果有更好的方案欢迎探讨。
  另外,注意下,最终我们log是有落地到log监控系统的,大家在实际项目中需要评估好是否有这样条件,以免不能满足实际要求
  我们项目中也有用到thinkphp5,它就是一个完全跟随laravel思想的国货,已经能很好的扩展自己的日志系统,包括扩展monolog支持也就不需要上面那么复杂了。但此文实现思路还是值得探讨的,希望对有需要的人有帮助