Month: 五月 2019

恐龙专家

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

最早应该只是某些绘本或碰碰狐的英文儿歌里有出现恐龙,以及一部叫恐龙列车的动画片,然后是去书店看书有看到恐龙书,后面就一发不可收拾,在家需要各种看恐龙的动画片和科普片,都能自己熟练的找到妈妈的 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