莫莫第一次看电影

看的电影是 巧虎大飞船历险记

从莫莫七个月开始定巧虎,去年满两年了又续订了两年,最近一期的 DVD 末尾就有推这个电影了。在公司里某天 HR 同学问我说我家娃能不能看电影,说怎么想起来问这个,说弄到巧虎这个电影的首映场,但是她没注意时间跟他家娃的围棋课冲突了,我说担心莫莫可能坐不住,那边各种怂恿说没事的总有第一次的,这个看着似乎对小朋友更友好,于是就这么愉快的弄了票过去,因为我们确定能不能去花了比较久,等换票时看只有第一排了,给小朋友也选了位置坐在爸妈中间

首映场在西湖文化广场的新远国际影城,一个以前从来没去过也没关注过的影城,西湖文化广场倒是带莫莫去自然博物馆去过两次,时间是下午两点到三点半,感觉略尴尬,这个点很多小朋友不是午睡没睡醒,或刚睡起来怎么赶过去。不过我们家这一只在家都不午睡的,刚好上午就过去,逛逛自然博物馆然后吃个饭就可以去看电影了

按首映场约定提前二十分钟到影城,换实体票,领加油道具(一个纸卷话筒),还拿了贴纸小礼品,按巧虎 DVD 里预告的情况,会有人偶上台互动,所以进去后看坐第一排也还好,不是 IMAX 也没有那么仰着看的问题

可能照顾是儿童场,开始前没有任何广告,直接就龙标播放了,一开始有一小段电影自己的观影注意事项,比如全程不会关灯,只是把亮度稍微调低了点,不限制说话啥的,中间还有需要互动的环节(前面那个话筒有用了)。也不像一般的电影开场直接讲故事,这边开场前还有出场人物介绍(主要都是巧虎各种系列里一句出现的常规角色),给一个剪影和介绍,让猜是谁,鼓励大家拿起话筒一起喊出角色名字,这个环节莫莫的互动感还挺好的

电影进入正题后出现第一次反派角色,大概二十来分钟时,莫莫开始坐不住,先幽幽的说「我害怕」,一开始爸妈想是不是灯有点暗或怎样,说抱着你一起看不害怕的,然后莫莫还是控制不住的哭闹起来,一直说「我害怕」,为了安抚情绪和不影响其他看电影的小朋友和家长,和妈妈一起把莫莫抱到影厅外面坐下,给吃了点小零食和酸奶,不过情绪一直没缓过来,后面爸爸抱着在后门处看了一下还是闹着要回去,确认好回家的话就没得看了还是要回家,只能悻悻的回来(爸爸妈妈都还想看完的呢 XD)

回家路上在车上很快睡着了,估计除了电影冲击力,这个时间要求好好坐着是容易闹觉,可能下午再晚点的场次让小朋友睡过再来可能会好一点?

等晚上吃过饭在家各种嗨时,又会突然冒出来高兴的大喊「卡比是个大坏蛋!」,就是看电影时引起哭闹的那个反派角色名字,这,小朋友你那时候可是闹着要走,真的是被大坏蛋吓到的关系么?但是现在这个情绪明显是很兴奋的啊,爸爸妈妈继续怨念没有看完电影

记一次诡异的被 yarn 坑的过程

事情的起源是 @tdzl2003 说他用 VSCode 的 remote 模式用的很开心,放弃 Hyper-V 回到 VirtualBox 的怀抱,笨狗去看了下 remote 相关功能好像还在 VSCode Insider 里,可以装来试试看,弄好后看了下文档和教程,知道插件分装在本地和装在远端,但是项目里 eslint 插件就一直在报错,说找不到模块

找不到就去补吧,到远端的目录下 yarn install 装好依赖,回到本地一看,还是报错,怎么会这样?在远端也执行下看看?

$ ./node_modules/.bin/eslint index.js
Error: Cannot find module 'jsx-ast-utils/elementType'
...

What the fuck?! 一定是哪里不对,把 jsx-ast-utils 这个包也重装一下?再来

$ yarn add jsx-ast-utils
$ ./node_modules/.bin/eslint index.js
Error: Cannot find module 'jsx-ast-utils/elementType'
...

你特么是在逗我?去看看这个包到底怎么回事,里面只有 lib src 两个目录,其他都没有,看起来好像是不太对

确认了一下环境,Windows 10 Pro 1903, WSL Ubuntu 18.04.2, node 12.3.1, yarn 1.16.0,都是最新的,换个环境试试看?在自己另一台 MacBook Pro 上 yarn install 后就是正常的,hmmm,难道是 Windows 目录挂载到 WSL 下后文件权限啥的锅?试了下在 WSL 里用 WSL 的文件系统重新来过,问题依旧。在 Windows 里把 WSL 映射成网络盘,用 PowerShell 进到 WSL 文件系统里 yarn install 就是好的,嗯?好了?回到 WSL 下执行 PowerShell 安装好的 node_modules 也成功

难道是 WSL 的奇怪问题,搜了下好像也没有任何相关信息?难道是最近新崩的问题所以大家都找不到,我印象中之前确实也能跑来着?那就是这个锅了。等几乎认定就是 WSL 有奇怪的问题后,晚上回家后拿另一台同样环境的机器,在 WSL 下就正常的装好了

这这这,等会,冷静一下,一定有哪里不对,要不用下重启大法?把所有相关的东西都干掉重来一下?在有问题的机器上,把 git 仓库清空重来(虽然之前已经换不同的路径做过了),把 nodejs 和 yarn 都卸载重装,对了是不是跟 yarn 源还有关系?确认一下

$ yarn config get registry
https://registry.yarnpkg.com

果然这里不一样,忘了改成淘宝源,改一个再来一次(官方源为啥可能会有坑嘛,难道是最近撞墙撞的?不管了先死马当活马医好了)

$ yarn config set registry https://registry.npm.taobao.org
yarn config v1.16.0
success Set "registry" to "https://registry.npm.taobao.org".
Done in 0.06s.

这下应该没问题了,走你,等 yarn install 跑完,先检查下 node_modules/jsx-ast/utils/ 路径下文件对不对,嗯?还是只有 lib src 这两个文件夹?这到底是为什么啊,yarn 你这个坑货,要不换 npm 试试看?

npm install 跑一下,提示有文件权限不对,看了下,哦是之前有 sudo npm install -g 装过东西,把 ~/.npm 目录下相关内容清掉就好了。用 npm 装完一看,好了。。。呃,果然 yarn 你个坑货到底哪里不对啊!?

缓存?去吧 ~/.yarn 下的内容都清掉,/tmp 下有一堆 yarn- 开头的不知道是啥应该也都可以干掉,再来,还挂?去搜 yarn install package missing files,终于看到了一个很相似的情况,不过他这个最后也是清了 yarn 的 cache 就好了。看了下除了 ~/.yarn 还有人说 Cache 目录是 ~/.yarn-cache 目录,但是我压根都没这个目录啊。顺着这条线索继续搜,找到 yarn cache clean 这个命令,执行后再装就好了

那看来就是之前的缓存有问题,而我弄这么多各种重装也并没有清掉 cache,或者触发重新从远端拉包。那么 yarn 的 cache 到底在哪里?到用户根目录下查看所有文件,发现有一个目录叫 .cache,看着挺可疑,进去看看果然里面有 pipyarn 等目录,看了下里面文件时间,就是这了,应该就是之前某次安装过程中被强制退出,导致那个点在装的一个或几个包是不对的,但是目录还在 yarn 就一直傻乎乎的用本地的

结论:yarn 的 cache 机制对包的完整性保证不够,如果之前安装过程中有异常退出,赶上异常退出过程中的包就有可能损坏,后面如果不手动清 cache,就会一直挂

恐龙专家

男孩子们的爱好里,车和恐龙应该是两大主流,我们家莫就走上了恐龙这条路

最早应该只是某些绘本或碰碰狐的英文儿歌里有出现恐龙,以及一部叫恐龙列车的动画片,然后是去书店看书有看到恐龙书,后面就一发不可收拾,在家需要各种看恐龙的动画片和科普片,都能自己熟练的找到妈妈的 iPad,麻溜的解锁打开爱奇艺从播放历史里找自己之前看过的,还特别沉迷看各种恐龙打架的模拟,去书店一定要把各种恐龙书都找出来看一遍,现在家附近几个小朋友能去看书的地方,哪几本恐龙书有什么内容爸爸都能背下来了

带来的结果之一是经常喜欢学恐龙嗷嗷叫吓唬人,说我要把你吃掉

另外的结果就是真的成了恐龙专家,要买各种恐龙的绘本和书,知道食草动物和食肉动物区别,认识各种恐龙,属于什么分类,书上一翻出来立马就能说出来这是什么,妈妈买过一套十本的恐龙书,上面似乎就没有不认识的了,估计得有小一百种了?好多种爸爸妈妈都还分不太清呢(手动捂脸

去年十一前妈妈和伯伯带着去过一次浙江省自然博物馆,现在也心心念念要去看恐龙骨架,周末爸爸带着去,果然还是有不少,但是其实也只是自然博物馆其中一层的一小部分,而且好多变成骨架后就不好分辨了,只有一些有特别显著特征的才好认识,不过小朋友还是很满足啦

莫莫三岁

三岁生日都过了好久才来写这篇,因为感冒咳嗽弄了快一个月,去体检就拖了大半个月,身高 94cm 体重 13.0kg 还是稳定在 15 到 30 分位之间徘徊,一直怨念没生病前好像都有 14kg 了,小毛病弄弄还是吃不胖

现在大了会自己表达后,带娃的很多时候就简单很多,至少他的诉求不用只靠猜。另一面因为自己有各种主见,有时候想哄住也需要更多的技巧

就说感冒咳嗽弄的比较厉害后,还是打算带去医院看看,坐上车一出小区发现方向不对,立马抗议「我不要去医院」,不过其实又还好,等到了那边,问诊什么的还能配合,听诊器过来都咯咯笑。对于打针抽血还是各种怂,事先各种不要,闹情绪扮可怜能用的招都用上,但是完事后又要装坚强,「我不怕打针的」

说到嘴硬,现在还是各种小男子汉的风格,事先各种不要不要,等过了那阵其实自己也能接受后,又不好意思反悔,这时候就需要等人给台阶下了,比如早上有起床气,会说我不要出去玩,等缓过去后再问他要不要出去玩,还是会弱弱的哼哼「我就是不要出去玩」,但换个问法「我们去小游乐场玩滑滑梯吧」,立马就会很高兴的说「我最喜欢玩滑滑梯了」

例行的吃喝拉撒睡的记录。吃现在很多东西都和大人一起吃了,已经有一些特别的偏好,比如土豆丝、茭白、豆腐干,对于鸡腿也还是持续的热爱,只是让莫莫妈妈又没得鸡腿吃了。每天一盒酸奶也变成了例行的安排,晚上睡前会要求一瓶配方奶,一般是 200ml,早上醒来前再喝一瓶 200ml 或 150ml 的配方奶,上午下午不定期补一些小零食和水果,有一段时间下午两三点还会主动要求喝奶,后面还是稳定改成酸奶。吃东西可以自己动手,奶瓶自己抱,喝水啥的只要不是自己玩疯了,还是知道渴了喝水,自己抱吸管杯自己打开喝。家里说还是让他戒奶瓶用普通的杯子喝水喝奶,但是小朋友睡前的这一口瘾还是奶瓶才止得住,有跟大人一起喝纯牛奶,但是没有定时定量,主要还是他看到我们喝和自己要求喝也每个准数

白天撒尿拉臭都会主动说,彻底摆脱尿不湿和拉拉裤,自己还不会在撒尿拉臭时脱穿裤子,可能也跟我们一直有去照顾帮忙有关。在家拉臭用小马桶,会要求照顾的人帮按着小鸡鸡免得尿腿上。晚上睡觉还是穿拉拉裤,早上有时候看也并没有尿,但不稳定,还是先穿着吧。不愿意在外面拉臭,憋可能也要憋回家,或者一般都在家拉好过才出去,现在一般都是稳定的早晚各一次

在家时不午睡,偶尔有过两次自己睡了的,但发现真的只是偶尔,并没有成习惯,不过周末带出去玩,下午坐车回去多半还是要睡一下,但是到楼下停好车抱回家时醒了就又不会睡。晚上习惯性的九点左右喝奶,然后床上要求读书讲故事或自己滚滚滚,一般得十点后才睡着,早上不喊的话还是八九点才醒,如果叫的早了还是会有起床气

数数现在能比较正常的数到 20 往上,少于五个的现在多半不挨个数而是直接报结果,往上数还是没耐心一直数下去,中间总会走神给数乱掉。逻辑倒是一等一的好,各种理由一套一套自圆其说没有任何问题

之前被爸爸各种玩的木质火车轨道现在自己也可以玩起来了,一些细节需要帮忙而已

莫言莫语 7

其实小朋友还是长的很快的,特别是脑力发育,最近偷懒没有写 blog 记,发现在手机上零零散散记的片段关键都好多了,而且,有一些现在感觉又没有那么激动了(果然长太快嫌弃更小时有点弱?)

大人不可以吃的

小馋猫各种贪吃,贪吃就算了,对某些分量不多的看只剩一点了特别护食,发觉有大人盯着想要,或逗他说给我吃一点好不好,立马很严肃的来这么一句

我会骂你的
我会打你的

某段时间对于大人的反抗就是这样,会严肃的跟你这么说,如果真生气了也确实会打一下,不过更多时候都只是警告,而大人这时候都是被萌到笑翻,并没有怎样

打人是不对的,骂人也是不对的

遇到他说会骂人会打人或者真的有打人时,会跟他讲打人不对骂人也不对,然后这个娃就经常这样反过来教育大人严肃提醒

男孩子跟男孩子睡,女孩子跟女孩子睡。爸爸你跟我睡,bebe 你去跟妈妈睡

跟他讲了男女有别,这娃有时候想黏爸爸,到晚上洗漱好准备睡觉时就会这么要求,也是让人笑的不行

那我的老婆呢?

续上,跟他讲爸爸和妈妈睡是因为老公和老婆一起睡的,这娃很自然的就接上来这么一问。呃,我也不知道你老婆在哪,这个以后要靠你自己了

你放一点白糖吧

早上喝粥,爸爸喜欢用小鱼干当浇头,伯伯喜欢加白糖,看多了等伯伯端碗出来时,会主动建议下

你怕辣吗?

续上,有时候会问伯伯为什么不吃小鱼干而放白糖

你是湖南人吗?湖南人不怕辣的

续上,伯伯说我怕辣不要吃小鱼干时,这娃立马又开始教育作为湖南人不要怂

爸爸你的老婆是谁呀?
是我的妈妈!

现在对于家庭成员关系的各种转换要清楚和熟练很多了,晚上陪他睡觉玩时这娃精力充沛还是会自问自答各种嗨

你老婆来了

续上,爸爸在房间里哄睡时,妈妈推门进来看这娃是不是还在嗨,这娃抬头一看立马淡定的告诉爸爸

梁咏琪 C’est La Vie

各个音乐平台都没有好的分享方式了,QQ 音乐连 Web 端搜索浏览都各种受限,只能先给单曲链接 梁咏琪 – C’est La Vie

想到这首歌是因为前两天下班跑去洗车,还惊奇怎么洗车店人这么少,都不用排队的,然后到家晚上就听到外面在下雨,正是这首歌里唱的

刚洗完车通常就下雨
要找的人他常常没开机
难忘的人总是忘了你
别介意 这是定理

果然这才是人生啊,C’est La Vie 应该是法语「这就是人生」,好多歌手都拿这个做过歌名

至于梁咏琪,则是另一个有念想的故事,感觉大众对她的印象应该都还是「胆小鬼」和「短发」两首歌?或者熟悉一点的再加上「花火」「中意他」「透明」?老实说梁咏琪作为歌手并没有太多特别的,更像是港台娱乐工业下批量生产的其中一员,有一些小清新小伤感,高妹这个外形定位有助于提升辨识度。但是,架不住某只喵喜欢她,所以也爱屋及乌去听了挺多梁咏琪的歌,也这样才能知道这样应该算小众的歌词

说远一点,在上高中开始听流行歌曲后,听梁咏琪比较多也比较耐听的应该也是《透明》这一张专辑,里面我喜欢的比例高一点,后面包括魔幻旅行、我想我唱、归属感里面,也都只是能有那么一两首可以一直听下去,再往后,似乎就是《顺时针》这一张精选集的好歌率高一点

09 年去香港,在理大呆着时才突然发现校友里也有 Gigi,仔细看了下应该比较偏职业教育,而且也几乎没有从事过专业相关的工作。不过对于正经学历高一点的艺人,整体的印象还是要好很多,不是看不起下九流,而是正经上学,确实还是要知书达理很多

都忘了 04 年夏天去武汉,然后回去时带给喵的到底是哪一张,确认过发行时间,再看看好多年没听的歌词,确认应该是《归属感》,再摘录里面《只是因为爱你》的一段

只是因为爱你
可以回答所有的怀疑
只是因为爱你
所以付出所有不在意

如何得到一个自适应宽度的高宽比固定的 HTML 容器

写上一篇文章时遇到了一个问题,我想插入一个 bilibili 的 iframe 来嵌入视频,如果直接用 B 站给的 iframe 代码,出来的是很小的一块,强行把 iframe 的 width 变成 100% 后,宽度是自适应撑开了,高度还是只有一点点高,然后 height 怎么写都不太对,如果再考虑手机等小屏幕设备需要自适应宽度,这个更无解了。搜了一圈,找到了一篇文章【前端笔记】使用iframe嵌入等比缩放的哔哩哔哩视频,按这个做法解决了问题

仔细看了下实现,其实挺有意思的,应该可以算前端的一个经典问题,就是如何设定一个 HTML 元素,能做到自适应宽度,且高宽比固定

一般我们需要维持高宽比的是图片,这个在 <img> 标签上可以有各种缩放选项能满足需求。对于视频,或者 iframe 嵌入,则没有比较好的原生实现方式。在上面找到的那个文章里,其实是先构建一个能自适应宽度且高宽比固定的 HTML 容器(一个 position: relative<div>),然后把真正要做到自适应的元素放到这个容器里,并用绝对定位占满空间,那么问题从怎么设定这个元素属性变成怎么得到可以自适应宽度且高宽比固定的一个容器

按搜到的那个实现做,是用一个空 <div>,先设定宽度 width: 100% 可以做到自适应宽度,接着为了维持高宽比,先强行设定高度 height: 0,然后用 padding-bottom: 75% 来做到等比撑开。这里的百分比计算是按照宽度来计算的,详细的定义可以看 w3.org 里关于 box 元素 padding-properties 的定义 http://www.w3.org/TR/2011/REC-CSS2-20110607/box.html#padding-properties。搜索结果里也都有这么来介绍,比如这两例:

今天问公司的前端担当 @tdzl2003,给出了另一个思路清奇的操作,直接塞一个比例图片进去,然后把这个图片撑开就行了,图片可以用透明色,而且 base64 编码后的图并没有多大,不会影响视觉效果也不影响性能,比如要 4:3 就弄一个 4 像素宽 3 像素高的空白图

果然前端都是各种骚操作,各种给跪

郝云 活着

郝云的活着,其实也很有段时间的歌,最近在上班路上听到,很喜欢,单曲循环了,也刚好是大部分人的心境吧

各种音乐平台都不支持往外挂播放器分享,只能找到 B 站了,刚好也看到了这个官方 MV,更好诠释一些内容,但也缺了没有画面时自己的那段想象

活着 – 郝云

词:郝云
曲:郝云

每天站在高楼上
看着地上的小蚂蚁
它们的头很大
它们的腿很细

它们拿着苹果手机
它们穿着耐克阿迪
上班就要迟到了
它们很着急

看 QQ 音乐下的评论,有人表示「我就在上班路上,就要迟到了」

我那可怜的吉普车
很久没爬山也没过河
它在这个城市里
过得很压抑

虽然它什么都没说
但我知道它很难过
我悄悄地许下愿望
带它去蒙古国

想起自己买车,或买很多东西前,都觉得自己会怎样怎样,最后也并没有怎样怎样。我那辆美利达的山地车,也就在北京陪我出过几次远门,大行的折叠,都吃灰好久了。过年买的大疆 Mavic Air,也就在小区旁边的小公园飞过几次,加起来能有半个小时?

慌慌张张匆匆忙忙
为何生活总是这样
难道说我的理想
就是这样度过一生的时光

不卑不亢不慌不忙
也许生活应该这样
难道说六十岁以后
再去寻找我想要的自由

上学时会说你大了就好了,上大学就好了,工作了就好了,有娃了就好了,其实,也并没有,该追求的,现在不做,以后多半也不会做的

一年一年飞逝而去
还是那一点点小积蓄
我喜欢的好多东西
还是买不起

生活总是麻烦不断
到现在我还没习惯
都说钱是王八蛋
可长得真好看

喜欢的很多东西买得起了,可能就不再喜欢,或者过了那个趟。想起来自己以前想着要 Walkman,想要 PSP,后来等自己能买得起时,可能又没时间,或者又没那个心境去折腾去玩。跟《我想去桂林》里唱的一样,有钱时没时间,有时间的时候没有钱

慌慌张张匆匆忙忙
为何生活总是这样
难道说我的理想
就是这样度过一生的时光

不卑不亢不慌不忙
也许生活应该这样
难道说六十岁以后
再去寻找我想要的自由

#

我不想这样活着
我不想这样活着
我不想这样活着
我不想这样活着
我不想这样活着
我不想这样活着
我不想这样活着
我不想这样活着
我不想这样活着

这一段念叨其实还挺有意思的,有评论说像念经,但也有评论说这段念的反倒能平静下来

慌慌张张匆匆忙忙
为何生活总是这样
难道说我的理想
就是这样度过一生的时光

不卑不亢不慌不忙
也许生活应该这样
难道说六十岁以后
再去寻找我想要的自由

#

其实我也常对自己说
人要学会知足而常乐
可万事都一笑而过
还有什么意思呢

知足常乐是没错,确实都一笑而过那又有什么意思呢。想起老爸,算是知足常乐的心态,很多事情无牵无挂不争不恼,但现在看有挺多事情也还是会在意。要知足常乐,也还是要努力要争取吧

flask celery logging 翻车记

虽然前面自己写过一个 Python Logging 的各种玩法(折腾 Python logging 的一些记录),结果没两个月,自己就在之前特意叮嘱过的地方翻了车

事情是这样的,我们的项目使用 Flask 作为业务框架,用 Celery 作为异步任务框架,按上篇里提到的,我们加了个 Filter 来加入 request_id 用于跟踪同一个 Web 请求或同一个 Task。所以,我们的 logging.conf 一开始是这样的,在 logfile 这个 Handler 里加入 request_id 并打印到日志文件,我们用阿里云的 logtail 把所有部署实例的日志文件都收集到一起,这一层 app Logger 往上抛是让终端都打印所有的日志,并且用 sentry 这个 Handler 在最上面收集各种异常报错

logging_conf = {
    "version": 1,
    "disable_existing_loggers": True,
    "filters": {
        "addRequestId": {
            "()": "app.utils.logging.request_filter.ContextFilter"
        }
    },
    "formatters": {
        "standard": {
            "format": "[%(asctime)s][%(levelname)s][%(pathname)s:%(lineno)s][%(funcName)s]: %(message)s"
        },
        "withRequestId": {
            "format": "[%(asctime)s][%(levelname)s][%(relpath)s:%(lineno)s][%(funcName)s][%(request_id)s]: %(message)s"
        }
    },
    "handlers": {
        "console": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "standard"
        },
        "logfile": {
            "level": "DEBUG",
            "class": "logging.handlers.WatchedFileHandler",
            "filters": ["addRequestId"],
            "formatter": "withRequestId",
            "filename": "log/app.log"
        },
        "sentry": {
            "level": "ERROR",
            "class": "raven.handlers.logging.SentryHandler",
            "dsn": "DSN_URI",
            "string_max_length": 512000
        }
    },
    "loggers": {
        "app": {
            "level": "DEBUG",
            "handlers": ["logfile"],
            "propagate": True
        }
    },
    "root": {
        "level": "DEBUG",
        "handlers": ["console", "sentry"]
    }
}

后来,为了把 Celery 调度信息等也通过 logtail 收集到阿里云,我们就把 logtail 的源从日志文件改成了 stdout/stderr 输出,不同时收集日志文件是因为同样的日志在 logfileconsole 里会出现两次,没有必要。但是只改 stdout/stderr 又会导致收集到的信息没有我们辛苦加进去的 relpathrequest_id,一个很直接的思路就是,在 app 这层 Logger 上加一个 consoleWithIdStreamHandler,并且 formatter 用 withRequestId 不就好了,然后限制 app Logger 的 propagate 为 False 禁止上抛,问题应该完美解决?等会,这里有好几个问题

第一个问题是,如果 app Logger 不往上抛,那万一异常了,sentry Handler 也收集不到错误?头疼医头脚疼医脚,那就给 app Logger 也挂上 sentry Handler 不就解决问题

第二个问题是,handlers 的处理顺序是不是严格按我们配置的顺序来?如果不是的话,consoleWithId 进入的时候,可能 addRequestId 这个 Filter 还没执行,出现了输出时拿不到 relpathrequest_id 那不就挂了?这个简单,把 Filter 移到 Logger 这一层不就解决了。至此,配置如下(只摘录改动部分)

    # ...
    "handlers": {
        # ...
        "logfile": {
            "level": "DEBUG",
            "class": "logging.handlers.WatchedFileHandler",
            "formatter": "withRequestId",
            "filename": "log/app.log"
        },
        "consoleWithId": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "formatter": "withRequestId"
        },
        # ...
    },
    "loggers": {
        "app": {
            "level": "DEBUG",
            "filters": ["addRequestId"],
            "handlers": ["logfile", "consoleWithId", "sentry"],
            "propagate": False
        }
    },
    # ...

翻车就翻在「这个简单」上,按这个思路配置后,跑起来还是在 consoleWithId 的 Handler 上输出报错,而且报的就是 relpathrequest_id 字段不存在。怀疑自己的配置有问题,跑到代码里打日志的地方用 logging.getLogger(__name__) 看拿到的到底是哪个 Logger,以及上面挂了哪些 Handler,还有 Logger 和 Handler 的 Filters 都配的啥,发现除了 app 根上,如果是 app.foo 这样的路径,拿到的都是一个叫 celery.utils.log.ProcessAwareLogger 的 Logger,而且没有任何 Handler 和 Filter 挂在上面,所以,Celery 你这个坏人,到底对我的代码做了什么?

跑去翻 celery.utils.log.ProcessAwareLogger 这个东西的源码都没看出个所以然,似乎只是为了保证 Flask 的 signal handler 机制正常,排查思路也断掉,再跑去看看我们那个 app.utils.logging.request_filter 的处理,有没有哪里不对的,在这个自定义的 filter 里裸用 print 打印,发现这个 filter 压根没被调用到?嗯?没被调用到?

回去看自己的上一篇,果然里面自己就提到过这里有坑(主流程解释的第 5 步)

如果开启了日志往上传递,则判断当前 Logger 是否有父 Logger,如果有的话,直接将当前 LogRecord 传给父 Logger 从 4 开始处理(跳过 1/2/3,注意此处级别控制 1 会不生效,绑定在父 Logger 上的 Filter 也不执行)

WTF!果然坑都是自己不掉一遍,别人说千万遍也不会记得的,哪怕说的这个人是自己。那好咯,把 addRequestId 这个 Filter 还是从 app Logger 上移到 Handler 层面上好了,每个需要的 Handler 都给挂上,多点性能开销就多点吧

不过这样配的感觉还是怪怪的,比如有些错误会被 sentry 收集两次,因为在 app 里一直往上抛会被 app Logger 里的 sentry 收集,如果这个错误还继续往上抛到了框架层面,框架的错误还会被 rootLogger 的 sentry 又收集一次。而且,既然 app 的里面和外面都有终端和 sentry,为啥不在最外面一次处理好,中间拦着不往上抛没有任何意义。调整了下,直接把 standard 这个 Formatter 和 console 这个 Handler 给去掉,在 rootLogger 上挂 consoleWithIdsentry 就好,最后完整的配置如下

logging_conf = {
    "version": 1,
    "disable_existing_loggers": True,
    "filters": {
        "addRequestId": {
            "()": "app.utils.logging.request_filter.ContextFilter"
        }
    },
    "formatters": {
        "request": {
            "format": "[%(asctime)s][%(levelname)s][%(relpath)s:%(lineno)s][%(funcName)s][%(request_id)s]: %(message)s"
        }
    },
    "handlers": {
        "logfile": {
            "level": "DEBUG",
            "class": "logging.handlers.WatchedFileHandler",
            "filters": ["addRequestId"],
            "formatter": "request",
            "filename": "log/app.log"
        },
        "consoleWithId": {
            "level": "DEBUG",
            "class": "logging.StreamHandler",
            "filters": ["addRequestId"],
            "formatter": "request"
        },
        "sentry": {
            "level": "ERROR",
            "class": "raven.handlers.logging.SentryHandler",
            "dsn": "DSN_URI",
            "string_max_length": 512000
        }
    },
    "loggers": {
        "app": {
            "level": "DEBUG",
            "handlers": ["logfile"],
        }
    },
    "root": {
        "level": "DEBUG",
        "handlers": ["consoleWithId", "sentry"]
    }
}

因为 Logger 的 propagate 默认就是 True,所以相对于第一版在 app 这个 Logger 上去掉了这条配置也没关系

最后,因为 addRequestId 这个 Filter 还是会被调两次,想优化下性能,就在 Filter 做完后加一个标记,下次再进来如果看到有这个标记就直接跳过,以及,对于非项目内的日志就不要用项目内的相对路径而用绝对路径替代。代码如下

# coding: utf8

import logging
import os.path

from celery import current_task
from flask import g, has_app_context, has_request_context


_proj_root_path = os.path.abspath(os.path.join(__file__, './../../../../'))
_proj_root_length = len(_proj_root_path)


class ContextFilter(logging.Filter):
    def filter(self, record):
        # ignore duplicate filter
        if hasattr(record, 'filter_by_yewen'):
            return True

        # request_id for flask web or celery task
        request_id = 'Standalone'

        if has_app_context():
            if has_request_context():
                request_id = g.get('request_id', 'UnknownRequest')
            elif current_task:
                request_id = current_task.request.id or 'UnknownTask'

        record.request_id = request_id

        # handle log_decorator pass
        record.funcName = getattr(record, 'orig_funcName', record.funcName)
        record.pathname = getattr(record, 'orig_pathname', record.pathname)
        record.lineno = getattr(record, 'orig_lineno', record.lineno)

        # relative path
        if record.pathname.startswith(_proj_root_path):
            record.relpath = record.pathname[_proj_root_length:]
        else:
            record.relpath = record.pathname

        record.filter_by_yewen = True

        return True

折腾 Python logging 的一些记录

Python 自己有成熟的日志模块 logging,使用中遇到一些原生组件无法满足的功能,或有一些使用方式上的坑,记录一下

0. 复习一下 logging 的实现

Python 官网对 logger flow 的定义如下图(来源 https://docs.python.org/3/howto/logging.html

Python logging flow

源码在 python 自己的 lib/logging/ 下,主要内容都在 __init__.py 里,先注意下几个定义

  • Logger,可以挂载若干个 Handler,可以挂载若干个 Filter,定义要响应的命名空间,和日志级别 (1)
  • Handler,可以挂载一个 Formatter,可以挂载若干个 Filter,定义了要响应日志级别 (2),和输出方式(流、文件等)
  • Filter,过滤器(其实也可以在里面搞更多事情)
  • Formatter,最终日志的格式化字符串
  • LogRecord,单条日志的结构体,所有信息都会存在这里

然后对着流程图来说,主流程如下

  1. 日志打印请求到 Logger 后,先判当前 Logger 是否要处理这个级别,不处理的直接扔掉(级别控制 1)
  2. 生成一条 LogRecord,会把包括调用来源等信息都一起打包好,传给 Logger 挂载的 Filter 挨个过滤
  3. 如果有 Filter 返回是 False,则丢弃这条日志
  4. 否则传给 Logger 挂载的 Handler 挨个处理(右上角子图)
  5. 如果开启了日志往上传递(propagate,不知道怎么翻译更精准),则判断当前 Logger 是否有父 Logger,如果有的话,直接将当前 LogRecord 传给父 Logger 从 4 开始处理(跳过 1/2/3,注意此处级别控制 1 会不生效,绑定在父 Logger 上的 Filter 也不执行)

右上角的子图是 Handler 内部的流程

  1. 判当前 Handler 是否要处理这个级别,不处理的直接扔掉(级别控制 2)
  2. 把收到的 LogRecord 交给挂载的 Filter 挨个过滤
  3. 如果 Filter 没有阻止,按挂载的 Formatter 格式化输出

这里面有一些比较好玩的地方

0.1 LogRecord 的生成

在生成之前其实 Logger 先干了两件事,一是找到原始的调用源(文件名 filename,方法名 funcName,行号 lineno),二是根据参数决定是否需要获取运行信息 exc_info

找原始调用源就是在 Python 的调用栈里一层一层往上找,直到找到调用文件不是当前文件(**/lib/logging/__init__.py)退出。印象中 C/C++ 的日志是直接编译时把当前的 __line__ 什么的展开得到,在 Python 里这么做应该还是因为 Python 是解析性语言。另外可能要注意的是这里的 filename 其实是文件绝对路径,传到 LogRecord 里后会变成 pathname,再分割得到文件名 filename 和模块名 module(这个就是 filename 去掉后缀)

生成好 LogRecord 后还会把传入的 extra 字典也挂上去,这里会限制 extra 里的字段不能和 LogRecord 原生字段冲突,否则会直接报错

0.2 不要被名字骗了的 Filter

从名字上看,Filter 应该就是一个过滤器,对输入的 LogRecord 做判断,返回 True/False 来决定挂载的 LoggerHandler 是否要处理当前日志,但是,这个东西不仅可以读 LogRecord,还可以改写,这里就有很多好玩的事情发生了(后面的很多事情都是在这里做的),而且只要被 Filter 改过的 LogRecord,都还会继续往后传递给其他的 Filter/Handler/Logger

0.3 没有被提到过的 adapter

在 Python 官方的 logging Cookbook 里,提到加上下文信息已开始推荐的是用 logging.LoggerAdapter 来做,这个东西其实是对 Logger 多了层封装,多包了一个 extra 字典进去,并且接管了 Loggerprocess 方法,实际用起来这个东西并不好用,所以在前面定义部分没说这个,官方的图也没提这块

0.4 为什么 log 还是要用 % 来格式化

Python 新一点的版本都支持 {} 格式化字符串,到 Python3.6 里更是有 literal template 这种不要太方便的字符串输出,那为什么 log 里还是坚持要用 % 加 args 的方式来处理呢?而且 pylint 等也都会对其他格式化方法报警告

没有太细究,大概想了下可能是因为这一整套 Logger 机制其实不仅仅是 Python 在用,其他语言也有在用,那么保持一致性是一个原因。另外还有查到说法是如果这条日志的等级不需要被处理,或者 Filter 直接就拦掉了,那么就不会走到 Formatter 那一步,可以减少格式化开销,不过这个原因也有站不住脚的地方,如果某条日志确定要被多个 Handler 处理,在用户端格式化就只用做一次,在 Formatter 里格式化就每个 Handler 都要重复做一次了

1. 对 logging 增加功能

1.1 增加相对路径

原生 LogRecord 里只有 filename (文件名)和 pathname (绝对路径),然而 filename 太短,我们可能在不同的目录下都有同名文件,而绝对路径又太长,把一堆有的没的都带上来,所以我们想打印出相对于项目的相对路径

一开始用了各种人肉魔改,包括接管整个 Logger 来自己做,后来发现可以简单加一个 logging.Filter 来解决。前面提到过 Filter 不仅可以过滤决定是否要输出日志,还可以改传入的 LogRecord,这样就很简单了,在我们的 Filter 里,记录下项目的根路径(这个很容易通过当前文件的 __file__ 往上指定层推出来),然后在 LogRecord 添加一个 relpath 的属性,取 LogRecord.pathname 截断掉前面非项目的部分就行了

1.2 自定义的 Filter 进配置

有了自定义 Filter 后,还需要能挂载到对应的 HandlerLogger

这里略坑的是 logging.config.fileConfig 这样的文件配置并不支持自定义 Filter,只能用 dictConfig。那么配置要么写 Python 变成原生 dict,要么用 json 写,在初始化配置的地方 json.load 读进来变成 dict。从「配置文件归配置文件」的角度说,用 json 会更合适,如果考虑到不同的环境用不同配置,用基类加继承微调的方式,可能写 Python 原生字典会更方便

1.3 保证 Formatter 匹配 Filter

增加的 relpath 可以直接在 Formatter 里用 %(relpath)s 的方式输出,但是这里也得保证,有 relpathFormatter 拿到的一定是被处理过的 LogRecord,不然就崩了

考虑到 Formatter 是一一绑定在 Handler 上的,所以我个人认为比较好的方法是在 Handler 里配置 Filter,保证如果用了自定义字段的 Formatter,一定要加上对应的 Filter,就算这个 Filter 在多个 Handler 上被多次执行,最多增加点性能开销,并不会对结果产生改变

1.4 打印 Flask 请求 ID

对于 Flask 应用,我们希望对一次请求所打的日志能有一个统一的 request_id 把所有日志串起来,方便追查。那么在 Web 请求的 app.before_request 里先加了个 g.request_id,把 request.path 拼上一个随机串记到上下文 g 里,然后在 logging.Filter 里判断是否有 app 上下文,有的话去取这个字段,并追加到 LogRecord 里,后面在 Formatter 里直接写 %(request_id)s 就可以输出了

5. 打印 celery task id

同上,对于异步任务,celery task 自己就有个 request.id 字段,直接判断是否存在上下文,摸出来挂到 LogRecord 上就行了

2. 增加易用性

2.1 log_decorator

很多时候我们希望知道一个方法的入参和返回值,如果在每个需要处理的方法前后都人肉写,未免太不 Pythonic,很自然就想到对方法调用加上装饰器,自动打调用参数和返回结果

对于怎么写装饰器,怎么摸被修饰方法的参数名和值等,之前写的几篇关于 Python 装饰器的 blog 已经写的很详细了,此处不重复

唯一需要注意的是如果不做特殊处理,打印的日志里,文件名、行号、方法都是 log_decorator 里打日志的那行,而不是原始方法。所以在这里需要先摸到原始方法的文件路径、方法名、行号,写到 log 的参数 extra 里用于构建 LogRecord,这里还特别注意因为 MakeRecord 的时候限制了不允许覆盖 LogRecord 已有的字段,所以这里必须改个名字,等到 Filter 里再去尝试看有没有自己加的字段,如果有则替换已有的

我们在实际工程中还对这里做了一些优化,支持传入方法来对入参和返回做处理后输出,特别是对复杂结构很有必要,另外对过长的 tuple/list/set/dict 也做了截断处理

2.2 before_request / after_request

有了 log_decorator 可以对方法的入参和返回很容易记录,那么对于 Web 请求,应该也可以更容易的做调用参数和返回值的记录。对于 Flask 应用,可以在 appblueprint 上往 before_requestafter_request 里增加打日志的方法来记录入参和返回

此处暂时没有很好解决的是文件路径、方法名和行号还是记录的打日志这个方法里调 log 语句时的数据,并不是最终处理 route 的方法,暂时还没去研究是否有办法可以实现,有 request.path 可以根据路由表去查,其实也还好

2.3 sentryHandler

对于用了 sentry 的项目,除了抛异常,某些时候也希望有一些错误信息能被记录到 sentry 上。最土的方法就是生成一个 raven_client 实例,然后 captureMessagecaptureException

其实 sentry 有提供 sentryHandler,就是一个 logging.Handler,直接配到 logging 的配置里,挂载到 rootLogger 上,初始化的时候就可以自动挂载上去,后面要用的时候直接 logger.error 打日志就是(什么?你需要只打 sentry 不打 log?你都打 sentry 了这么大的事情都不打条日志?也不是不可以,单独配个 logger 只挂 sentryHandler 就是,但还是不建议这么做)

如果需要打调用信息,在 log 时加上参数 exc_info=True,需要打堆栈就加 extra={"stack": True},比自己人肉搞不知道高到哪里去了

更详细的请见官方文档:https://docs.sentry.io/clients/python/integrations/logging/

3. 关于 Sentry 的补充

3.1 flask to sentry

在 Flask 应用里用 Sentry 可以参考官方文档 https://docs.sentry.io/clients/python/integrations/flask/ ,从 raven.contrib.flask 里 import 一个 Sentry 过来就行,实例化后在 init_app 的时候指定上对应的 sentry_dsn,这样就可以用这个 sentry 实例来 captureMessagecaptureException

其实这里配置好了更大的意义是在 Flask 应用抛了没有人接的异常时能往 sentry 打异常报告,这个地方一开始我配置好 sentryHandlerlogging.rootLogger 后得意忘形的把初始化 Sentry 给去掉了,然后就捕获不到异常了,弄明白怎么回事后老老实实加回来,最后异常捕获走 raven.contrib.flask.Sentry,日志走 rootLogger.sentryHandler,各行其是

这里还发现了个特别浪的操作,既然我们在 logging.rootLogger 上已经配好了 sentry_dsn,那是不是就有现成的 raven_client 可以用呢?实际上是可以的… 参考下方代码,初始化的时候直接写 Sentry(app, client) 就行,里面会自动完成 init_app 的操作的

def getRavenClient():
    _logger = logging.getLogger()
    for handler in _logger.handlers:
        if isinstance(handler, SentryHandler) and handler.client.is_enabled():
            return handler.client
    return None

3.2 celery to sentry

同样,对于 celery 异步任务,也可以参考官方文档 https://docs.sentry.io/clients/python/integrations/celery/ 来配置往 Sentry 打日志或捕获异常,因为我们已经在 logging.rootLogger 上配过 sentryHandler,所以官方文档里的 register_logger_signal 可以忽略,只要从 ravan.contrib.celery 里 import 这个 register_signal 方法并初始化就行,初始化 client 一样可以参考上面从 rootLogger 里去摸