ELK收集Docker项目的日志

现在主流的项目部署都是采用 docker 部署了,一般项目使用 STDOUT 输出的日志都被 docker 来收集和保存。我们需要使用著名的 ELK (opens new window) 来分析日志,所以如何部署 ELK 并且把 docker 的日志转发给 ELK 呢? 这就是本篇文章我们需要解决的问题。

# 搭建 ELK

既然我们使用 docker 进行部署,那我们当然也要使用 docker 部署 ELK。docker-elk (opens new window) 是一个配置好的项目,我们可以基于这个项目迅速搭建起一个 ELK 项目。说实话, ELK 项目非常消耗内存,所以至少保证机器有 4GB 内存,否则可能会宕机。搭建好 ELK 之后,记得按照教程重新设置密码。

$ docker-compose exec -T elasticsearch bin/elasticsearch-setup-passwords auto --batch
1

初始化密码之后,并修改 kibanalogstash 的配置文件更新密码。最后再重启这两个服务:

$ docker-compose restart kibana logstash
1

# 使用 GELF 收集日志

GELF (opens new window) 是一个经典的日志格式并且 docker 原生支持这个格式,首先我们需要配置 logstash 采取这个格式来收集日志。

修改 logstash/pipeline/logstash.conf 文件的 input 部分:

input {
	beats {
		port => 5044
	}

	gelf {
		port => 5000
	}
}
1
2
3
4
5
6
7
8
9

这样收集 gelf 的端口就是 5000 默认使用 UDP 格式。修改配置之后别忘了重启 logstash

$ docker-compose restart logstash
1

接下来实际项目要把日志转发到 ELK 上,如果我们使用 docker-compose.yml 需要在当前的 service 上添加 logging 相关的配置:

logging:
  driver: gelf
  options:
    gelf-address: 'udp://0.0.0.0:5000'
1
2
3
4

如果你的 ELK 和 实际项目并不是在同一台主机上,记得修改 0.0.0.0 成相应的主机名或 IP 地址。这样配置之后就可以在 kibana 上查看收集到的日志了。 不过要记得配置 Index patterns 然后在 Discover 界面上就能看到日志记录了。

# 总结

ELK 收集 docker 日志并不是很复杂,但是摸索的时候很难调试,例如 ELK 配置了 gelf 方法收集日志,但却找不到好的方法来测试这个接口是否正常。 我只是通过 nmap 命令来检测 5000 端口是否开放,并不知道是否正常运行。

扫描当前主机的 UDP 端口:

nmap -sU 0.0.0.0
1

启动项目的 docker 的时候还出现了警告信息 warning: no logs are available with the 'gelf' log driver 为了这个问题我查了半天,但发现 这个信息即使出现了也可以正常收集。浪费了不少时间。最终的解决方案总是很简单,但查找的过程中难免绕弯路。

# 参考

https://ibm-cloud-architecture.github.io/b2m-nodejs/logging/ (opens new window)

Using Free Let’s Encrypt SSL/TLS Certificates with NGINX (opens new window)

Django使用CKEditor的相关经验

最近在使用 Django 进行后台管理系统的开发,其中有需求要使用所见即所得的富文本编辑器来让用户上传图片和文字并排版。这里我们选择了CKEditor (opens new window),原因很简单,因为这个第三方库提供了很好的 AWS S3 的支持,而我们目前也确实把静态资源例如图片放在 S3 上。

# 配置 AWS S3

我们一般使用django-storages (opens new window)这个库来集成 AWS S3, 首先需要在设置里面配置 AWS 的 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_S3_REGION_NAME

然后自定义一个 Storage class 方便配置,代码如下:

class MaterialStorage(S3Boto3Storage):
    bucket_name = 'material-bucket'
    default_acl = 'private'
1
2
3

默认都是私有桶,避免用户拿到图片的 URL 就可以分享给别人。S3 默认开启 AWS_QUERYSTRING_AUTH, 访问资源的格式类似下面这种带有查询参数:

https://s3.amazonaws.com/photos/puppy.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Signature=NpgCjnDzrM%2BWFzoENXmpNDUsSn8%3D&Expires=1175139620
1

默认的查询参数认证有效期是一个小时,也就意味着这个链接最多一个小时有效,而且认证参数最大只能设置一周有效。可以有效避免链接泄漏导致的资源泄漏风险。

# 配置项目导入 CKEditor

按照官方指导可以很顺利的导入 CKEditor:

from django.db import models
from ckeditor.fields import RichTextUploadingField

class Post(models.Model):
    content = RichTextUploadingField()
1
2
3
4
5

# CKEditor 配置 S3

官方教程上说明必须关掉AWS_QUERYSTRING_AUTH (opens new window)才能正常使用。但是关掉之后就意味着任何人只要获得资源的链接就可以访问,这是很危险的。所以我们如何保证开启认证参数的同时又能正常使用编辑器呢?这个才是本文的重点。

在开启认证参数的时候,编辑器保存的是当时上传照片时候的认证参数,也就意味着一个小时之后再访问的时候,就没法正常显示图片。如何保证页面载入编辑器的时候,保证图片正常显示呢?

我的思路是在页面载入的时候读取编辑器里面的内容,采用正则表达式匹配存储在 AWS 上的图片,然后更新图片相关的认证信息。

# 数据库读入时自动更新认证信息

查看 Django 文档的时候发现,我们可以自定义一个 field,然后使用一个 hook 函数 from_db_value (opens new window)。在数据读入的时候进行相关操作,例如把存入的 JSON 字符串转换成 JSON 对象。通过这个方法,我们可以对数据进行加工,更新图片的认证信息。

class CKEditorField(RichTextUploadingField):
    def from_db_value(self, value, expression, connection):
        if value:
            return update_image(value)
        else:
            return value
1
2
3
4
5
6

我们自定义了一个 CKEditorField 字段,从数据库读出的时候进行图片认证信息的更新。update_image 函数的实现如下:

def update_image(content):
    storage = MaterialStorage()
    s3_regex = re.compile(r'"(https:\/\/amazonaws.com\S+)"', re.MULTILINE)
    links = s3_regex.findall(content)
    for link in links:
        match = re.search(r'\/upload\S+.(png|jpe?g)', link)
        if match:
            path = match.group()
            if storage.exists(path):
                content = content.replace(link, storage.url(path))
    return content
1
2
3
4
5
6
7
8
9
10
11

简单的解释一下代码功能,首先匹配 AWS 链接,然后匹配链接中的图片路径,知道图片路径之后。再使用 storage 对象查找是否存在,如果存在的话,获取这个资源的 URL。这时候新的 URL 就带有新的认证信息。通过这种方式,我们就能保证每次页面显示编辑器的时候,里面的图片都可以正常显示出来。

# 总结

知道答案之后,当然觉得这么做很简单。但是探索出这个答案却非常花时间,首先要明确解决问题的思路,然后顺着这个思路去想出可能的解决方案。最后,问题迎刃而解。

# 参考

Authenticating Requests: Using Query Parameters (opens new window)

File storage API (opens new window)

使用Service Worker的 Web Push 功能

现代的 Android 应用和 iOS 应用都有相应的推送系统,可以给用户推送消息。即使用户没有打开这个应用,也同样可以收到消息。当然,网页既然要向手机应用靠拢的话,那推送功能也是必不可少。 今天我们就来介绍一下,网页如何实现类似的推送功能。

# 基于 Service Worker 的推送功能

Service Worker 提供了两种推送服务,一种是基于 Firebase Cloud Messaging 的推送功能,一种是基于 VAPID 的推送功能。第一种已经过时了,所以本文只介绍第二种。

# Create a public/private key pair

首先,我们需要创建公钥和私钥,类似与 HTTPS 的 RSA 加密。客户端使用公钥订阅推送,服务端使用私钥来发送通知。创建既可以在浏览器进行,也可以使用 Node 创建。当然,推荐在后端生成更安全。

前端生成代码参考链接:util.ts (opens new window)

后端使用web-push (opens new window)可以更方便的生成公钥和私钥。代码如下:

function generateVAPIDKeys() {
  const vapidKeys = webpush.generateVAPIDKeys();

  return {
    publicKey: vapidKeys.publicKey,
    privateKey: vapidKeys.privateKey
  };
}
1
2
3
4
5
6
7
8

# Subscribing with the public key

接下来我们需要订阅推送:

navigator.serviceWorker.ready
  .then(registration => {
    return registration.pushManager.getSubscription().then(subscription => {
      if (subscription === null) {
        return registration.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: this.vapidPublicKey
        });
      } else {
        return subscription;
      }
    });
  })
  .then(subscription => {
    this.subJSON = subscription.toJSON();
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们使用公钥进行订阅,并且把订阅之后的内容打印出来,方便服务器使用。实际工程中需要发送给服务器进行保存。使用 Chrome 浏览器订阅的信息是这样的:

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/d3cMmjx0Eg8:APA91bF1i7wgLJRw-VgOh3Evn6RG1xqdOR6Y0CeTUm1xiD36BCHXaDoceVfilDYiifWdI_rWdU8IdJjqSxaCVscRp5zl9lon8u4mf9mha0fmSVKJzUOx5r5Jba2yiNmCFRxxKcTJm51S",
  "expirationTime": null,
  "keys": {
    "p256dh": "BDtGmJB0Bkyum0WJw8NiiCn4U9ckX8UjhzXPUad2HM0yID0ced8zUHKr-Yhf6p2Z7IS0G07dGG7Tnl5jlwQVog4",
    "auth": "25JGSYqTKvj_nAeodHBHSQ"
  }
}
1
2
3
4
5
6
7
8

我们可以看到推送的网址是 Chrome 提供的服务,Firefox 浏览器下订阅的信息是这样的:

{
  "endpoint": "https://updates.push.services.mozilla.com/wpush/v2/gAAAAABghCrItYXGJw0eCyp0Ae1lTxMXkb6Fxhg8tRck2cMoY4bZjvkV2j5t95FfrPftdieUgeaNthjmb0_XyoIVqWIy7cpy9lMjczHb5TYpC7sKnOw4IekwrtQbmBo6Vn54TZaUSrBIb40PEy2KXF5QlyOj2QxlTz6d6NPB6mMvJxuYNSg-5xs",
  "keys": {
    "auth": "MCeQnXlaz4A-CBuALNPbcQ",
    "p256dh": "BL4am0lzx005spT_UBbMagfWb93Cfgh8XtkCtP7y697dODFnO0wCVVI783BsiHePRTl-mrpoHolJ0gKTYR1T4SQ"
  }
}
1
2
3
4
5
6
7

endpoint 我们可以看出推送服务本身是浏览器提供的,如果无法访问这个网址,自然就无法使用推送服务。

# Server push

接下来,我们让服务端发送一个消息试试:

const webpush = require('web-push');

const vapidPublicKey = 'public key';

const vapidPrivateKey = 'private key';

// PushSubscriptionJSON Info
const pushSubscription = {
  endpoint: '',
  expirationTime: null,
  keys: {
    p256dh: '',
    auth: ''
  }
};

const payload = 'hello world!';

const options = {
  vapidDetails: {
    subject: 'mailto:example@web-push-node.org',
    publicKey: vapidPublicKey,
    privateKey: vapidPrivateKey
  },
  TTL: 60
};
webpush
  .sendNotification(pushSubscription, payload, options)
  .then(function(result) {
    console.log('success!');
    console.log(result);
  })
  .catch(function(err) {
    console.log('fail!');
    console.error(err);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

在上述代码中填入相应的认证信息,就可以使用了。浏览器的推送功能并不需要把相应的网站打开,只要浏览器是打开的就可以收到推送。

# 总结

浏览器的推送功能非常方便,但是首先需要用户授权通知权限才能使用,其次是浏览器提供的推送服务,可能在中国无法正常使用。所以现在这个功能还没有大规模的应用。

# 参考

Introduction to Push Notifications (opens new window)

Adding Push Notifications to a Web App (opens new window)

Service Worker Push Notifications Demo (opens new window)