dubbo.apache.org/dubbo-go/v3@v3.1.1/filter/accesslog/filter.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one or more
     3   * contributor license agreements.  See the NOTICE file distributed with
     4   * this work for additional information regarding copyright ownership.
     5   * The ASF licenses this file to You under the Apache License, Version 2.0
     6   * (the "License"); you may not use this file except in compliance with
     7   * the License.  You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  // Package accesslog providers logging filter.
    19  package accesslog
    20  
    21  import (
    22  	"context"
    23  	"os"
    24  	"reflect"
    25  	"strings"
    26  	"sync"
    27  	"time"
    28  )
    29  
    30  import (
    31  	"github.com/dubbogo/gost/log/logger"
    32  )
    33  
    34  import (
    35  	"dubbo.apache.org/dubbo-go/v3/common/constant"
    36  	"dubbo.apache.org/dubbo-go/v3/common/extension"
    37  	"dubbo.apache.org/dubbo-go/v3/filter"
    38  	"dubbo.apache.org/dubbo-go/v3/protocol"
    39  )
    40  
    41  const (
    42  	// used in URL.
    43  	// nolint
    44  	FileDateFormat = "2006-01-02"
    45  	// nolint
    46  	MessageDateLayout = "2006-01-02 15:04:05"
    47  	// nolint
    48  	LogMaxBuffer = 5000
    49  	// nolint
    50  	LogFileMode = 0o600
    51  
    52  	// those fields are the data collected by this filter
    53  
    54  	// nolint
    55  	Types = "types"
    56  	// nolint
    57  	Arguments = "arguments"
    58  )
    59  
    60  var (
    61  	once            sync.Once
    62  	accessLogFilter *Filter
    63  )
    64  
    65  func init() {
    66  	extension.SetFilter(constant.AccessLogFilterKey, newFilter)
    67  }
    68  
    69  // Filter for Access Log
    70  /**
    71   * Although the access log filter is a default filter,
    72   * you should config "accesslog" in service's config to tell the filter where store the access log.
    73   * for example:
    74   * "UserProvider":
    75   *   registry: "hangzhouzk"
    76   *   protocol : "dubbo"
    77   *   interface : "com.ikurento.user.UserProvider"
    78   *   ... # other configuration
    79   *   accesslog: "/your/path/to/store/the/log/", # it should be the path of file.
    80   *
    81   * the value of "accesslog" can be "true" or "default" too.
    82   * If the value is one of them, the access log will be record in log file which defined in log.yml
    83   * AccessLogFilter is designed to be singleton
    84   */
    85  type Filter struct {
    86  	logChan chan Data
    87  }
    88  
    89  func newFilter() filter.Filter {
    90  	if accessLogFilter == nil {
    91  		once.Do(func() {
    92  			accessLogFilter = &Filter{logChan: make(chan Data, LogMaxBuffer)}
    93  			go func() {
    94  				for accessLogData := range accessLogFilter.logChan {
    95  					accessLogFilter.writeLogToFile(accessLogData)
    96  				}
    97  			}()
    98  		})
    99  	}
   100  	return accessLogFilter
   101  }
   102  
   103  // Invoke will check whether user wants to use this filter.
   104  // If we find the value of key constant.AccessLogFilterKey, we will log the invocation info
   105  func (f *Filter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result {
   106  	accessLog := invoker.GetURL().GetParam(constant.AccessLogFilterKey, "")
   107  
   108  	// the user do not
   109  	if len(accessLog) > 0 {
   110  		accessLogData := Data{data: f.buildAccessLogData(invoker, invocation), accessLog: accessLog}
   111  		f.logIntoChannel(accessLogData)
   112  	}
   113  	return invoker.Invoke(ctx, invocation)
   114  }
   115  
   116  // logIntoChannel won't block the invocation
   117  func (f *Filter) logIntoChannel(accessLogData Data) {
   118  	select {
   119  	case f.logChan <- accessLogData:
   120  		return
   121  	default:
   122  		logger.Warn("The channel is full and the access logIntoChannel data will be dropped")
   123  		return
   124  	}
   125  }
   126  
   127  // buildAccessLogData builds the access log data
   128  func (f *Filter) buildAccessLogData(_ protocol.Invoker, invocation protocol.Invocation) map[string]string {
   129  	dataMap := make(map[string]string, 16)
   130  	attachments := invocation.Attachments()
   131  	itf := attachments[constant.InterfaceKey]
   132  	if itf == nil || len(itf.(string)) == 0 {
   133  		itf = attachments[constant.PathKey]
   134  	}
   135  	if itf != nil {
   136  		dataMap[constant.InterfaceKey] = itf.(string)
   137  	}
   138  	if v, ok := attachments[constant.MethodKey]; ok && v != nil {
   139  		dataMap[constant.MethodKey] = v.(string)
   140  	}
   141  	if v, ok := attachments[constant.VersionKey]; ok && v != nil {
   142  		dataMap[constant.VersionKey] = v.(string)
   143  	}
   144  	if v, ok := attachments[constant.GroupKey]; ok && v != nil {
   145  		dataMap[constant.GroupKey] = v.(string)
   146  	}
   147  	if v, ok := attachments[constant.TimestampKey]; ok && v != nil {
   148  		dataMap[constant.TimestampKey] = v.(string)
   149  	}
   150  	if v, ok := attachments[constant.LocalAddr]; ok && v != nil {
   151  		dataMap[constant.LocalAddr] = v.(string)
   152  	}
   153  	if v, ok := attachments[constant.RemoteAddr]; ok && v != nil {
   154  		dataMap[constant.RemoteAddr] = v.(string)
   155  	}
   156  
   157  	if len(invocation.Arguments()) > 0 {
   158  		builder := strings.Builder{}
   159  		// todo(after the paramTypes were set to the invocation. we should change this implementation)
   160  		typeBuilder := strings.Builder{}
   161  
   162  		builder.WriteString(reflect.ValueOf(invocation.Arguments()[0]).String())
   163  		typeBuilder.WriteString(reflect.TypeOf(invocation.Arguments()[0]).Name())
   164  		for idx := 1; idx < len(invocation.Arguments()); idx++ {
   165  			arg := invocation.Arguments()[idx]
   166  			builder.WriteString(",")
   167  			builder.WriteString(reflect.ValueOf(arg).String())
   168  
   169  			typeBuilder.WriteString(",")
   170  			typeBuilder.WriteString(reflect.TypeOf(arg).Name())
   171  		}
   172  		dataMap[Arguments] = builder.String()
   173  		dataMap[Types] = typeBuilder.String()
   174  	}
   175  
   176  	return dataMap
   177  }
   178  
   179  // OnResponse do nothing
   180  func (f *Filter) OnResponse(_ context.Context, result protocol.Result, _ protocol.Invoker, _ protocol.Invocation) protocol.Result {
   181  	return result
   182  }
   183  
   184  // writeLogToFile actually write the logs into file
   185  func (f *Filter) writeLogToFile(data Data) {
   186  	accessLog := data.accessLog
   187  	if isDefault(accessLog) {
   188  		logger.Info(data.toLogMessage())
   189  		return
   190  	}
   191  
   192  	logFile, err := f.openLogFile(accessLog)
   193  	if err != nil {
   194  		logger.Warnf("Can not open the access log file: %s, %v", accessLog, err)
   195  		return
   196  	}
   197  	logger.Debugf("Append log to %s", accessLog)
   198  	message := data.toLogMessage()
   199  	message = message + "\n"
   200  	_, err = logFile.WriteString(message)
   201  	if err != nil {
   202  		logger.Warnf("Can not write the log into access log file: %s, %v", accessLog, err)
   203  	}
   204  }
   205  
   206  // openLogFile will open the log file with append mode.
   207  // If the file is not found, it will create the file.
   208  // Actually, the accessLog is the filename
   209  // You may find out that, once we want to write access log into log file,
   210  // we open the file again and again.
   211  // It needs to be optimized.
   212  func (f *Filter) openLogFile(accessLog string) (*os.File, error) {
   213  	logFile, err := os.OpenFile(accessLog, os.O_CREATE|os.O_APPEND|os.O_RDWR, LogFileMode)
   214  	if err != nil {
   215  		logger.Warnf("Can not open the access log file: %s, %v", accessLog, err)
   216  		return nil, err
   217  	}
   218  	now := time.Now().Format(FileDateFormat)
   219  	fileInfo, err := logFile.Stat()
   220  	if err != nil {
   221  		logger.Warnf("Can not get the info of access log file: %s, %v", accessLog, err)
   222  		return nil, err
   223  	}
   224  	last := fileInfo.ModTime().Format(FileDateFormat)
   225  
   226  	// this is confused.
   227  	// for example, if the last = '2020-03-04'
   228  	// and today is '2020-03-05'
   229  	// we will create one new file to log access data
   230  	// By this way, we can split the access log based on days.
   231  	if now != last {
   232  		err = os.Rename(fileInfo.Name(), fileInfo.Name()+"."+now)
   233  		if err != nil {
   234  			logger.Warnf("Can not rename access log file: %s, %v", fileInfo.Name(), err)
   235  			return nil, err
   236  		}
   237  		logFile, err = os.OpenFile(accessLog, os.O_CREATE|os.O_APPEND|os.O_RDWR, LogFileMode)
   238  	}
   239  	return logFile, err
   240  }
   241  
   242  // isDefault check whether accessLog == true or accessLog == default
   243  func isDefault(accessLog string) bool {
   244  	return strings.EqualFold("true", accessLog) || strings.EqualFold("default", accessLog)
   245  }
   246  
   247  // Data defines the data that will be log into file
   248  type Data struct {
   249  	accessLog string
   250  	data      map[string]string
   251  }
   252  
   253  // toLogMessage convert the Data to String
   254  func (d *Data) toLogMessage() string {
   255  	builder := strings.Builder{}
   256  	builder.WriteString("[")
   257  	builder.WriteString(d.data[constant.TimestampKey])
   258  	builder.WriteString("] ")
   259  	builder.WriteString(d.data[constant.RemoteAddr])
   260  	builder.WriteString(" -> ")
   261  	builder.WriteString(d.data[constant.LocalAddr])
   262  	builder.WriteString(" - ")
   263  	if len(d.data[constant.GroupKey]) > 0 {
   264  		builder.WriteString(d.data[constant.GroupKey])
   265  		builder.WriteString("/")
   266  	}
   267  
   268  	builder.WriteString(d.data[constant.InterfaceKey])
   269  
   270  	if len(d.data[constant.VersionKey]) > 0 {
   271  		builder.WriteString(":")
   272  		builder.WriteString(d.data[constant.VersionKey])
   273  	}
   274  
   275  	builder.WriteString(" ")
   276  	builder.WriteString(d.data[constant.MethodKey])
   277  	builder.WriteString("(")
   278  	if len(d.data[Types]) > 0 {
   279  		builder.WriteString(d.data[Types])
   280  	}
   281  	builder.WriteString(") ")
   282  
   283  	if len(d.data[Arguments]) > 0 {
   284  		builder.WriteString(d.data[Arguments])
   285  	}
   286  	return builder.String()
   287  }