MongoDB Go Driver 如何记录日志

如果你也有这些疑问:golang 官方的 mongo driver 怎么记录日志,2. Golang mongo driver 如何打印sql,如何打印执行的命令,那么这篇文章会告诉你

为什么

如果你有这些问题:
1. golang 官方的 mongo driver 怎么执行了没有日志输出啊
2. golang mongo driver 如何记录sql,如何打印执行的命令
3. 想通过日志来检查 golang mongo 调用API是否符合预期

那么这篇文章可以继续看下去。

背景

在这之前我使用 MySQL 是比较多的,后来遇到有记录用户操作日志需求,随着数据量越来越大,MySQL 有些扛不住(慢),就换成了 Mongo,在使用Mongo过程中,发现没法记录日志,就是无法将打印代码发起的sql,之前使用gorm打印execute sql是很方便的。

在网上查了好久也没查到相关资料,就开始翻官方文档,和源码。

终于在不懈~ 打住,其实很简单,就在 ClientOptions 字段里:

// ClientOptions contains options to configure a Client instance. Each option can be set through setter functions. See
// documentation for each setter function for an explanation of the option.
type ClientOptions struct {
    AppName                  *string
    Auth                     *Credential
    AutoEncryptionOptions    *AutoEncryptionOptions
    ...
    MaxConnecting            *uint64
    PoolMonitor              *event.PoolMonitor
    Monitor                  *event.CommandMonitor // 执行的命令监视器(日志)
    ServerMonitor            *event.ServerMonitor
    ...

    err error
    uri string
    cs  *connstring.ConnString

github 代码地址

是的—— ClientOptions.Monitor 字段。

使用方法如下:

package mongolearn

import (
    "context"
    "fmt"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/event"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

var (
    mongoDsn = "mongodb://admin:123456@127.0.0.1:27017"
)

// TestConnUseDb sql monitor
func TestConnWithMonitor() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // client option
    var clientOpt = options.Client().ApplyURI(mongoDsn)

    // log monitor
    var logMonitor = event.CommandMonitor{
        Started: func(ctx context.Context, startedEvent *event.CommandStartedEvent) {
            log.Printf("mongo reqId:%d start on db:%s cmd:%s sql:%+v", startedEvent.RequestID, startedEvent.DatabaseName,
                startedEvent.CommandName, startedEvent.Command)
        },
        Succeeded: func(ctx context.Context, succeededEvent *event.CommandSucceededEvent) {
            log.Printf("mongo reqId:%d exec cmd:%s success duration %d ns", succeededEvent.RequestID,
                succeededEvent.CommandName, succeededEvent.DurationNanos)
        },
        Failed: func(ctx context.Context, failedEvent *event.CommandFailedEvent) {
            log.Printf("mongo reqId:%d exec cmd:%s failed duration %d ns", failedEvent.RequestID,
                failedEvent.CommandName, failedEvent.DurationNanos)
        },
    }
    // cmd monitor set
    clientOpt.SetMonitor(&logMonitor)
    client, err := mongo.Connect(ctx, clientOpt)
    if nil != err {
        fmt.Printf("mongo connect err %v\n", err)
    } else {
        fmt.Printf("mongo connect success~\n")
        defer func() {
            if err = client.Disconnect(ctx); err != nil {
                panic(err)
            }
        }()
    }

    // update test
    if re, err := client.Database("test").Collection("test").UpdateOne(ctx, bson.M{"name": "cc"}, bson.M{"$set": bson.M{"age": 12}}); err != nil {
        log.Printf("%v", err)
    } else {
        log.Printf("mongo update one re %+v", re)
    }
}

输出结果

mongo connect success~
2023/08/20 13:22:43 mongo reqId:6 start on db:test cmd:update sql:{"update": "test","ordered": true,"lsid": {"id": {"$binary":{"base64":"qfrrzSt7SkCN5ChY04/T5A==","subType":"04"}}},"$db": "test","updates": [{"q": {"name": "cc"},"u": {"$set": {"age": {"$numberInt":"12"}}}}]}
2023/08/20 13:22:43 mongo reqId:6 exec cmd:update success duration 44489114 ns
2023/08/20 13:22:43 mongo update one re &{MatchedCount:0 ModifiedCount:0 UpsertedCount:0 UpsertedID:<nil>}

# 下面是断开 mongo 时触发的命令
2023/08/20 13:22:43 mongo reqId:7 start on db:admin cmd:endSessions sql:{"endSessions": [{"id": {"$binary":{"base64":"qfrrzSt7SkCN5ChY04/T5A==","subType":"04"}}}],"$db": "admin"}
2023/08/20 13:22:43 mongo reqId:7 exec cmd:endSessions success duration 58037162 ns

通过以上日志可以看到,mongomonitor 按照 Started, Succeeded 顺序记录,最后才会执行函数外的 日志 。

结束

我看网上资料少,很多小伙伴都不知道,特此写一篇小文记录下。

Cannot create namespace centrum.policy in multi-document transaction.

Cannot create namespace centrum.policy in multi-document transaction.

Cannot create namespace xxx.xxx in multi-document transaction.

问题

看报错信息,就是说不能在 mongo 事务中创建表。

解决否

已解决

方案

这是说: 不能在事务中创建 collection 。但你一看 sql,没有创建表语句啊。

mongo 中,如果你操作的 collection 不存在是会自动创建的。但有个例外就是事务。

在事务中是无法创建表结构的。

所以,解决这个问题,就是在执行 mongo sql 前,先创建好要操作的 collection

参考

  • https://stackoverflow.com/questions/52585715/cannot-create-namespace-in-multi-document-transactionmongodb-4-0-spring-data-2

Mongo 副本集 选择副本后 连不上 server selection error

failed: server selection error: context deadline exceeded, current topology: { Type: ReplicaSetNoPrimary, Servers: [{ Addr: 127.0.0.1:27017, Type: Unknown, Last error: connection() error occurred during connection handshake: dial tcp 127.0.0.1:27017: connect: connection refused

Mongo 副本集 选主错误 server selection error

问题

failed: server selection error: context deadline exceeded, current topology: { Type: ReplicaSetNoPrimary, Servers: [{ Addr: 127.0.0.1:27017, Type: Unknown, Last error: connection() error occurred during connection handshake: dial tcp 127.0.0.1:27017: connect: connection refused

解决否

已解决

方案

登陆 mongo server 输入命令

rs.status()

rs.status

这里看图片里,name是: mongolab:27021, 那么应用也会用这个地址去连 mongodb,如果 dns 里没有 mongolab,那么应用是连不上这个 mongo 的,最终报错就是链接超时。

查看一下Mongo集群之间的通讯 IP,或者地址是怎么配置,是否配置的IP的应用端能访问的IP段。

例如,三个 mongo 集群配置在同一网段内—— 192.168.1.0 ,如果你也按照这个 IP 来配置副本集,那么 mongo 之间通信是没问题的,但如果应用端可能读取 replica 信息后,会那副本集配置的 IP 信息去访问其他节点,这个时候应用会拿着 192.168.1.0 这个网断的 IP 去访问 mongo,如果 mongo 和应用不在一个网段,就会导致如上问题。所以,mongoreplica 配置的IP也需是应用可访问的 IP

这也是,往往运维部署,发现,mongo 互通 OK,运维溜了。等应用来连,却连不上问题。

参考

  • https://www.mongodb.com/docs/manual/replication/
  • https://www.mongodb.com/docs/manual/reference/method/rs.initiate/

Ubuntu 安装 MongoDB

Ubuntu 下如何用 apt-get 安装最新的 MongoDB

Ubuntu 安装 MongoDB

官网:https://www.mongodb.com/

社区版下载地址: https://www.mongodb.com/try/download/community

安装命令

## 安装依赖
curl -fsSL https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -

## 查看添加成功没
apt-key list

echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list

## 更新 apt
sudo apt update

## 安装 mongo
sudo apt install mongodb-org

## 启动服务
sudo systemctl start mongod.service

## 查看服务状态
sudo systemctl status mongod

## 启用状态
sudo systemctl enable mongod

## 停用状态
sudo systemctl stop mongod

初始化配置

vim /etc/mongod.conf
# network interfaces
net:
  port: 27017
#  bindIp: 127.0.0.1
  bindIp: 0.0.0.0

编辑了用户记得重启

sudo systemctl restart mongod

登陆 mongo

在安装了mongod的服务本机,mongo是无密码的,可输入如下命令直接登陆

$ mongo

远端通过如下命令登陆,未设置密码情况下,绑定了外网地址情况下,也能登陆

$ mongo "mongodb://192.168.0.x:27017"

创建用户

  • 产看所用用户
> use admin
switched to db admin
> db.system.users.find().pretty()

注意:用户相关的操作都需要 在admin db下操作!

创建 root 用户

> db.createUser(
    {
        user:"root",
        pwd:"pwd",
        roles:["root"]
    }
)

注意: 密码最好不要使用 & ? # : $ ; / . @ 等符号,因为 mongo DSN 格式是个 url,url 相关的关键词都不要用哦!

创建 admin 用户

> db.createUser(  
  { user: "admin",  
    customData:{description:"superuser"},
    pwd: "admin",  
    roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]  
  }  
)  

创建业务用户

> db.createUser({
    user:"user001",
    pwd:"123456",
    customData:{
        name:'jim',
        email:'jim@qq.com',
        age:18,
    },
    roles:[
        {role:"readWrite",db:"db001"},
        {role:"readWrite",db:"db002"},
        'read'// 对其他数据库有只读权限,对db001、db002是读写权限
    ]
})

用户管理

  • 改密码
use admin
db.changeUserPassword("username", "xxx")
  • 为用户追加权限
db.grantRolesToUser( "<username>", [ <roles> ], { <writeConcern> } )
  • 删除用户
use admin
db.dropUser('user001')

安全设置

访问mongo认证配置

在无密码的mongo上配置好用户后,就可以启用安全认证了。

vim /etc/mongod.conf
#security:

修改为:

security:
  authorization: enabled

如果是 mongo 低于 2.6 那么在配置文件最后一行加上:

auth = true

参考

  • https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-20-04
  • https://segmentfault.com/a/1190000015603831
  • https://stackoverflow.com/questions/25325142/how-to-set-authorization-in-mongodb-config-file

float64 can only be truncated to an integer type when truncation is enabled

使用 mongo-go-dirver 反序列化到 float32 报错

error decoding key weight: float64 can only be truncated to an integer type when truncation is enabled

问题描述

通过 mongo-go-dirver findOne 查数据并反序列化到 结构体时,有个字段:

type User struct {
    Name      string    `bson:"name"`
    Age       uint32    `bson:"age"`
    Weight    float32   `bson:"weight"`
    Studying  bool      `bson:"studying"`
    Tag       []string  `bson:"tag"`
    CreatedAt time.Time `bson:"created_at"`
}

weight 字段类型为 float32,就提示:

float64 类型的值只能在启用截断时截断为整数类型

问题如何产生的

存入数据时,手写 68.1,在 Golang 中手写的 float 值默认为 float64, 所以就当 float64 存入mongo了,待你想反序列化到 float32的值时,就报错了。

在 mgo 中,程序会自动帮你截断转换了,但是在 mongo-go-driver 中需要手动处理,或者显示得配置解决方案,不然会导致报错,不输出数据!

解决否

已解决

方案

问题原因时 浮点数 decode时,超出了精度,mongo-go-driver就把这个问题抛了个error出来。那么针对这个报错,我们可以从两方面去解决。要么提高精度,要么高速golang,多的截断,咋不要了。

  1. 把float32改为 float64
  2. 在bson tag 追加 ,truncate
    Weight    float32   `bson:"weight,truncate"`

参考

  • https://segmentfault.com/a/1190000038451313