blob: 167b5edd804fd5b4b319e86d13815d1b4f698e1c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package filter_impl
import (
"context"
"os"
"reflect"
"strings"
"time"
)
import (
"github.com/apache/dubbo-go/common/constant"
"github.com/apache/dubbo-go/common/extension"
"github.com/apache/dubbo-go/common/logger"
"github.com/apache/dubbo-go/filter"
"github.com/apache/dubbo-go/protocol"
)
const (
//used in URL.
// nolint
FileDateFormat = "2006-01-02"
// nolint
MessageDateLayout = "2006-01-02 15:04:05"
// nolint
LogMaxBuffer = 5000
// nolint
LogFileMode = 0600
// those fields are the data collected by this filter
// nolint
Types = "types"
// nolint
Arguments = "arguments"
)
func init() {
extension.SetFilter(constant.ACCESS_LOG_KEY, GetAccessLogFilter)
}
/*
* AccessLogFilter
* Although the access log filter is a default filter,
* you should config "accesslog" in service's config to tell the filter where store the access log.
* for example:
* "UserProvider":
* registry: "hangzhouzk"
* protocol : "dubbo"
* interface : "com.ikurento.user.UserProvider"
* ... # other configuration
* accesslog: "/your/path/to/store/the/log/", # it should be the path of file.
*
* the value of "accesslog" can be "true" or "default" too.
* If the value is one of them, the access log will be record in log file which defined in log.yml
* AccessLogFilter is designed to be singleton
*/
type AccessLogFilter struct {
logChan chan AccessLogData
}
// Invoke will check whether user wants to use this filter.
// If we find the value of key constant.ACCESS_LOG_KEY, we will log the invocation info
func (ef *AccessLogFilter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result {
accessLog := invoker.GetUrl().GetParam(constant.ACCESS_LOG_KEY, "")
// the user do not
if len(accessLog) > 0 {
accessLogData := AccessLogData{data: ef.buildAccessLogData(invoker, invocation), accessLog: accessLog}
ef.logIntoChannel(accessLogData)
}
return invoker.Invoke(ctx, invocation)
}
// logIntoChannel won't block the invocation
func (ef *AccessLogFilter) logIntoChannel(accessLogData AccessLogData) {
select {
case ef.logChan <- accessLogData:
return
default:
logger.Warn("The channel is full and the access logIntoChannel data will be dropped")
return
}
}
// buildAccessLogData builds the access log data
func (ef *AccessLogFilter) buildAccessLogData(_ protocol.Invoker, invocation protocol.Invocation) map[string]string {
dataMap := make(map[string]string, 16)
attachments := invocation.Attachments()
itf := attachments[constant.INTERFACE_KEY]
if itf == nil || len(itf.(string)) == 0 {
itf = attachments[constant.PATH_KEY]
}
if itf != nil {
dataMap[constant.INTERFACE_KEY] = itf.(string)
}
if v, ok := attachments[constant.METHOD_KEY]; ok && v != nil {
dataMap[constant.METHOD_KEY] = v.(string)
}
if v, ok := attachments[constant.VERSION_KEY]; ok && v != nil {
dataMap[constant.VERSION_KEY] = v.(string)
}
if v, ok := attachments[constant.GROUP_KEY]; ok && v != nil {
dataMap[constant.GROUP_KEY] = v.(string)
}
if v, ok := attachments[constant.TIMESTAMP_KEY]; ok && v != nil {
dataMap[constant.TIMESTAMP_KEY] = v.(string)
}
if v, ok := attachments[constant.LOCAL_ADDR]; ok && v != nil {
dataMap[constant.LOCAL_ADDR] = v.(string)
}
if v, ok := attachments[constant.REMOTE_ADDR]; ok && v != nil {
dataMap[constant.REMOTE_ADDR] = v.(string)
}
if len(invocation.Arguments()) > 0 {
builder := strings.Builder{}
// todo(after the paramTypes were set to the invocation. we should change this implementation)
typeBuilder := strings.Builder{}
builder.WriteString(reflect.ValueOf(invocation.Arguments()[0]).String())
typeBuilder.WriteString(reflect.TypeOf(invocation.Arguments()[0]).Name())
for idx := 1; idx < len(invocation.Arguments()); idx++ {
arg := invocation.Arguments()[idx]
builder.WriteString(",")
builder.WriteString(reflect.ValueOf(arg).String())
typeBuilder.WriteString(",")
typeBuilder.WriteString(reflect.TypeOf(arg).Name())
}
dataMap[Arguments] = builder.String()
dataMap[Types] = typeBuilder.String()
}
return dataMap
}
// OnResponse do nothing
func (ef *AccessLogFilter) OnResponse(_ context.Context, result protocol.Result, _ protocol.Invoker, _ protocol.Invocation) protocol.Result {
return result
}
// writeLogToFile actually write the logs into file
func (ef *AccessLogFilter) writeLogToFile(data AccessLogData) {
accessLog := data.accessLog
if isDefault(accessLog) {
logger.Info(data.toLogMessage())
return
}
logFile, err := ef.openLogFile(accessLog)
if err != nil {
logger.Warnf("Can not open the access log file: %s, %v", accessLog, err)
return
}
logger.Debugf("Append log to %s", accessLog)
message := data.toLogMessage()
message = message + "\n"
_, err = logFile.WriteString(message)
if err != nil {
logger.Warnf("Can not write the log into access log file: %s, %v", accessLog, err)
}
}
// openLogFile will open the log file with append mode.
// If the file is not found, it will create the file.
// Actually, the accessLog is the filename
// You may find out that, once we want to write access log into log file,
// we open the file again and again.
// It needs to be optimized.
func (ef *AccessLogFilter) openLogFile(accessLog string) (*os.File, error) {
logFile, err := os.OpenFile(accessLog, os.O_CREATE|os.O_APPEND|os.O_RDWR, LogFileMode)
if err != nil {
logger.Warnf("Can not open the access log file: %s, %v", accessLog, err)
return nil, err
}
now := time.Now().Format(FileDateFormat)
fileInfo, err := logFile.Stat()
if err != nil {
logger.Warnf("Can not get the info of access log file: %s, %v", accessLog, err)
return nil, err
}
last := fileInfo.ModTime().Format(FileDateFormat)
// this is confused.
// for example, if the last = '2020-03-04'
// and today is '2020-03-05'
// we will create one new file to log access data
// By this way, we can split the access log based on days.
if now != last {
err = os.Rename(fileInfo.Name(), fileInfo.Name()+"."+now)
if err != nil {
logger.Warnf("Can not rename access log file: %s, %v", fileInfo.Name(), err)
return nil, err
}
logFile, err = os.OpenFile(accessLog, os.O_CREATE|os.O_APPEND|os.O_RDWR, LogFileMode)
}
return logFile, err
}
// isDefault check whether accessLog == true or accessLog == default
func isDefault(accessLog string) bool {
return strings.EqualFold("true", accessLog) || strings.EqualFold("default", accessLog)
}
// GetAccessLogFilter return the instance of AccessLogFilter
func GetAccessLogFilter() filter.Filter {
accessLogFilter := &AccessLogFilter{logChan: make(chan AccessLogData, LogMaxBuffer)}
go func() {
for accessLogData := range accessLogFilter.logChan {
accessLogFilter.writeLogToFile(accessLogData)
}
}()
return accessLogFilter
}
// AccessLogData defines the data that will be log into file
type AccessLogData struct {
accessLog string
data map[string]string
}
// toLogMessage convert the AccessLogData to String
func (ef *AccessLogData) toLogMessage() string {
builder := strings.Builder{}
builder.WriteString("[")
builder.WriteString(ef.data[constant.TIMESTAMP_KEY])
builder.WriteString("] ")
builder.WriteString(ef.data[constant.REMOTE_ADDR])
builder.WriteString(" -> ")
builder.WriteString(ef.data[constant.LOCAL_ADDR])
builder.WriteString(" - ")
if len(ef.data[constant.GROUP_KEY]) > 0 {
builder.WriteString(ef.data[constant.GROUP_KEY])
builder.WriteString("/")
}
builder.WriteString(ef.data[constant.INTERFACE_KEY])
if len(ef.data[constant.VERSION_KEY]) > 0 {
builder.WriteString(":")
builder.WriteString(ef.data[constant.VERSION_KEY])
}
builder.WriteString(" ")
builder.WriteString(ef.data[constant.METHOD_KEY])
builder.WriteString("(")
if len(ef.data[Types]) > 0 {
builder.WriteString(ef.data[Types])
}
builder.WriteString(") ")
if len(ef.data[Arguments]) > 0 {
builder.WriteString(ef.data[Arguments])
}
return builder.String()
}