You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
400 lines
9.1 KiB
400 lines
9.1 KiB
/*
|
|
* Copyright 2020-2021 the original author(https://github.com/wj596)
|
|
*
|
|
* <p>
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
* </p>
|
|
*/
|
|
package endpoint
|
|
|
|
import (
|
|
"bytes"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/siddontang/go-mysql/canal"
|
|
"github.com/siddontang/go-mysql/mysql"
|
|
"github.com/siddontang/go-mysql/schema"
|
|
|
|
"go-mysql-transfer/global"
|
|
"go-mysql-transfer/model"
|
|
"go-mysql-transfer/service/luaengine"
|
|
"go-mysql-transfer/util/logs"
|
|
"go-mysql-transfer/util/stringutil"
|
|
)
|
|
|
|
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
|
|
|
const defaultDateFormatter = "2006-01-02"
|
|
|
|
type Endpoint interface {
|
|
Connect() error
|
|
Ping() error
|
|
Consume(mysql.Position, []*model.RowRequest) error
|
|
Stock([]*model.RowRequest) int64
|
|
Close()
|
|
}
|
|
|
|
func NewEndpoint(ds *canal.Canal) Endpoint {
|
|
cfg := global.Cfg()
|
|
luaengine.InitActuator(ds)
|
|
|
|
if cfg.IsRedis() {
|
|
return newRedisEndpoint()
|
|
}
|
|
|
|
if cfg.IsMongodb() {
|
|
return newMongoEndpoint()
|
|
}
|
|
|
|
if cfg.IsRocketmq() {
|
|
return newRocketEndpoint()
|
|
}
|
|
|
|
if cfg.IsRabbitmq() {
|
|
return newRabbitEndpoint()
|
|
}
|
|
|
|
if cfg.IsKafka() {
|
|
return newKafkaEndpoint()
|
|
}
|
|
|
|
if cfg.IsEls() {
|
|
if cfg.ElsVersion == 6 {
|
|
return newElastic6Endpoint()
|
|
}
|
|
if cfg.ElsVersion == 7 {
|
|
return newElastic7Endpoint()
|
|
}
|
|
}
|
|
|
|
if cfg.IsScript() {
|
|
return newScriptEndpoint()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func convertColumnData(value interface{}, col *schema.TableColumn, rule *global.Rule) interface{} {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
switch col.Type {
|
|
case schema.TYPE_ENUM:
|
|
switch value := value.(type) {
|
|
case int64:
|
|
eNum := value - 1
|
|
if eNum < 0 || eNum >= int64(len(col.EnumValues)) {
|
|
// we insert invalid enum value before, so return empty
|
|
logs.Warnf("invalid binlog enum index %d, for enum %v", eNum, col.EnumValues)
|
|
return ""
|
|
}
|
|
return col.EnumValues[eNum]
|
|
case string:
|
|
return value
|
|
case []byte:
|
|
return string(value)
|
|
}
|
|
case schema.TYPE_SET:
|
|
switch value := value.(type) {
|
|
case int64:
|
|
bitmask := value
|
|
sets := make([]string, 0, len(col.SetValues))
|
|
for i, s := range col.SetValues {
|
|
if bitmask&int64(1<<uint(i)) > 0 {
|
|
sets = append(sets, s)
|
|
}
|
|
}
|
|
return strings.Join(sets, ",")
|
|
}
|
|
case schema.TYPE_BIT:
|
|
switch value := value.(type) {
|
|
case string:
|
|
if value == "\x01" {
|
|
return int64(1)
|
|
}
|
|
return int64(0)
|
|
}
|
|
case schema.TYPE_STRING:
|
|
switch value := value.(type) {
|
|
case []byte:
|
|
return string(value[:])
|
|
}
|
|
case schema.TYPE_JSON:
|
|
var f interface{}
|
|
var err error
|
|
switch v := value.(type) {
|
|
case string:
|
|
err = json.Unmarshal([]byte(v), &f)
|
|
case []byte:
|
|
err = json.Unmarshal(v, &f)
|
|
}
|
|
if err == nil && f != nil {
|
|
return f
|
|
}
|
|
case schema.TYPE_DATETIME, schema.TYPE_TIMESTAMP:
|
|
var vv string
|
|
switch v := value.(type) {
|
|
case string:
|
|
vv = v
|
|
case []byte:
|
|
vv = string(v)
|
|
}
|
|
if rule.DatetimeFormatter != "" {
|
|
vt, err := time.Parse(mysql.TimeFormat, vv)
|
|
if err != nil || vt.IsZero() { // failed to parse date or zero date
|
|
return nil
|
|
}
|
|
return vt.Format(rule.DatetimeFormatter)
|
|
}
|
|
return vv
|
|
case schema.TYPE_DATE:
|
|
var vv string
|
|
switch v := value.(type) {
|
|
case string:
|
|
vv = v
|
|
case []byte:
|
|
vv = string(v)
|
|
}
|
|
if rule.DateFormatter != "" {
|
|
vt, err := time.Parse(defaultDateFormatter, vv)
|
|
if err != nil || vt.IsZero() { // failed to parse date or zero date
|
|
return nil
|
|
}
|
|
return vt.Format(rule.DateFormatter)
|
|
}
|
|
return vv
|
|
case schema.TYPE_NUMBER:
|
|
switch v := value.(type) {
|
|
case string:
|
|
vv, err := strconv.ParseInt(v, 10, 64)
|
|
if err != nil {
|
|
logs.Error(err.Error())
|
|
return nil
|
|
}
|
|
return vv
|
|
case []byte:
|
|
str := string(v)
|
|
vv, err := strconv.ParseInt(str, 10, 64)
|
|
if err != nil {
|
|
logs.Error(err.Error())
|
|
return nil
|
|
}
|
|
return vv
|
|
}
|
|
case schema.TYPE_DECIMAL, schema.TYPE_FLOAT:
|
|
switch v := value.(type) {
|
|
case string:
|
|
vv, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
logs.Error(err.Error())
|
|
return nil
|
|
}
|
|
return vv
|
|
case []byte:
|
|
str := string(v)
|
|
vv, err := strconv.ParseFloat(str, 64)
|
|
if err != nil {
|
|
logs.Error(err.Error())
|
|
return nil
|
|
}
|
|
return vv
|
|
}
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
func encodeValue(rule *global.Rule, kv map[string]interface{}) string {
|
|
if rule.ValueTmpl != nil {
|
|
var tmplBytes bytes.Buffer
|
|
err := rule.ValueTmpl.Execute(&tmplBytes, kv)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return tmplBytes.String()
|
|
}
|
|
|
|
var val string
|
|
switch rule.ValueEncoder {
|
|
case global.ValEncoderJson:
|
|
data, err := json.Marshal(kv)
|
|
if err != nil {
|
|
logs.Error(err.Error())
|
|
val = ""
|
|
} else {
|
|
val = string(data)
|
|
}
|
|
case global.ValEncoderKVCommas:
|
|
var ls []string
|
|
for k, v := range kv {
|
|
str := stringutil.ToString(k) + "=" + stringutil.ToString(v)
|
|
ls = append(ls, str)
|
|
}
|
|
val = strings.Join(ls, ",")
|
|
case global.ValEncoderVCommas:
|
|
var ls []string
|
|
for _, v := range kv {
|
|
ls = append(ls, stringutil.ToString(v))
|
|
}
|
|
val = strings.Join(ls, ",")
|
|
}
|
|
|
|
return val
|
|
}
|
|
|
|
func rowMap(req *model.RowRequest, rule *global.Rule, primitive bool) map[string]interface{} {
|
|
kv := make(map[string]interface{}, len(rule.PaddingMap))
|
|
|
|
if rule.DefaultColumnValueConfig != "" {
|
|
for k, v := range rule.DefaultColumnValueMap {
|
|
if primitive {
|
|
kv[k] = v
|
|
} else {
|
|
kv[rule.WrapName(k)] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
if primitive {
|
|
for _, padding := range rule.PaddingMap {
|
|
kv[padding.ColumnName] = convertColumnData(req.Row[padding.ColumnIndex], padding.ColumnMetadata, rule)
|
|
}
|
|
} else {
|
|
for _, padding := range rule.PaddingMap {
|
|
kv[padding.WrapName] = convertColumnData(req.Row[padding.ColumnIndex], padding.ColumnMetadata, rule)
|
|
}
|
|
}
|
|
return kv
|
|
}
|
|
|
|
func oldRowMap(req *model.RowRequest, rule *global.Rule, primitive bool) map[string]interface{} {
|
|
kv := make(map[string]interface{}, len(rule.PaddingMap))
|
|
|
|
if rule.DefaultColumnValueConfig != "" {
|
|
for k, v := range rule.DefaultColumnValueMap {
|
|
if primitive {
|
|
kv[k] = v
|
|
} else {
|
|
kv[rule.WrapName(k)] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
if primitive {
|
|
for _, padding := range rule.PaddingMap {
|
|
kv[padding.ColumnName] = convertColumnData(req.Old[padding.ColumnIndex], padding.ColumnMetadata, rule)
|
|
}
|
|
} else {
|
|
for _, padding := range rule.PaddingMap {
|
|
kv[padding.WrapName] = convertColumnData(req.Old[padding.ColumnIndex], padding.ColumnMetadata, rule)
|
|
}
|
|
}
|
|
return kv
|
|
}
|
|
|
|
func primaryKey(re *model.RowRequest, rule *global.Rule) interface{} {
|
|
if rule.IsCompositeKey { // 组合ID
|
|
var key string
|
|
for _, index := range rule.TableInfo.PKColumns {
|
|
key += stringutil.ToString(re.Row[index])
|
|
}
|
|
return key
|
|
} else {
|
|
index := rule.TableInfo.PKColumns[0]
|
|
data := re.Row[index]
|
|
column := rule.TableInfo.Columns[index]
|
|
return convertColumnData(data, &column, rule)
|
|
}
|
|
}
|
|
|
|
func elsHosts(addr string) []string {
|
|
var hosts []string
|
|
splits := strings.Split(addr, ",")
|
|
for _, split := range splits {
|
|
if !strings.HasPrefix(split, "http:") {
|
|
hosts = append(hosts, "http://"+split)
|
|
} else {
|
|
hosts = append(hosts, split)
|
|
}
|
|
}
|
|
|
|
return hosts
|
|
}
|
|
|
|
func buildPropertiesByRule(rule *global.Rule) map[string]interface{} {
|
|
properties := make(map[string]interface{})
|
|
for _, padding := range rule.PaddingMap {
|
|
property := make(map[string]interface{})
|
|
switch padding.ColumnType {
|
|
case schema.TYPE_BINARY:
|
|
property["type"] = "binary"
|
|
case schema.TYPE_NUMBER:
|
|
property["type"] = "long"
|
|
case schema.TYPE_DECIMAL:
|
|
property["type"] = "double"
|
|
case schema.TYPE_FLOAT:
|
|
property["type"] = "float"
|
|
case schema.TYPE_DATE:
|
|
property["type"] = "date"
|
|
property["format"] = "yyyy-MM-dd"
|
|
case schema.TYPE_DATETIME, schema.TYPE_TIMESTAMP:
|
|
property["type"] = "date"
|
|
property["format"] = "yyyy-MM-dd HH:mm:ss"
|
|
default:
|
|
property["type"] = "keyword"
|
|
}
|
|
properties[padding.WrapName] = property
|
|
}
|
|
|
|
if len(rule.DefaultColumnValueMap) > 0 {
|
|
for key, _ := range rule.DefaultColumnValueMap {
|
|
property := make(map[string]interface{})
|
|
property["type"] = "keyword"
|
|
properties[key] = property
|
|
}
|
|
}
|
|
|
|
for _, mapping := range rule.EsMappings {
|
|
property := make(map[string]interface{})
|
|
property["type"] = mapping.Type
|
|
if mapping.Format != "" {
|
|
property["format"] = mapping.Format
|
|
}
|
|
if mapping.Analyzer != "" {
|
|
property["analyzer"] = mapping.Analyzer
|
|
}
|
|
properties[mapping.Field] = property
|
|
}
|
|
|
|
return properties
|
|
}
|
|
|
|
func buildPropertiesByMappings(rule *global.Rule) map[string]interface{} {
|
|
properties := make(map[string]interface{})
|
|
for _, mapping := range rule.EsMappings {
|
|
property := make(map[string]interface{})
|
|
property["type"] = mapping.Type
|
|
if mapping.Format != "" {
|
|
property["format"] = mapping.Format
|
|
}
|
|
if mapping.Analyzer != "" {
|
|
property["analyzer"] = mapping.Analyzer
|
|
}
|
|
properties[mapping.Field] = property
|
|
}
|
|
return properties
|
|
}
|
|
|