istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/networking/core/accesslog.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package core
    16  
    17  import (
    18  	"sync"
    19  
    20  	accesslog "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
    21  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    22  	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    23  	cel "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/filters/cel/v3"
    24  	grpcaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
    25  	hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    26  	tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
    27  
    28  	meshconfig "istio.io/api/mesh/v1alpha1"
    29  	"istio.io/istio/pilot/pkg/model"
    30  	"istio.io/istio/pilot/pkg/networking"
    31  	"istio.io/istio/pilot/pkg/util/protoconv"
    32  	"istio.io/istio/pkg/wellknown"
    33  )
    34  
    35  const (
    36  	// EnvoyServerName for istio's envoy
    37  	EnvoyServerName = "istio-envoy"
    38  
    39  	celFilter                          = "envoy.access_loggers.extension_filters.cel"
    40  	listenerEnvoyAccessLogFriendlyName = "listener_envoy_accesslog"
    41  
    42  	// EnvoyAccessLogCluster is the cluster name that has details for server implementing Envoy ALS.
    43  	// This cluster is created in bootstrap.
    44  	EnvoyAccessLogCluster = "envoy_accesslog_service"
    45  )
    46  
    47  var (
    48  	// State logged by the metadata exchange filter about the upstream and downstream service instances
    49  	// We need to propagate these as part of access log service stream
    50  	// Logging them by default on the console may be an issue as the base64 encoded string is bound to be a big one.
    51  	// But end users can certainly configure it on their own via the meshConfig using the %FILTER_STATE% macro.
    52  	envoyWasmStateToLog = []string{"wasm.upstream_peer", "wasm.upstream_peer_id", "wasm.downstream_peer", "wasm.downstream_peer_id"}
    53  
    54  	// accessLogBuilder is used to set accessLog to filters
    55  	accessLogBuilder = newAccessLogBuilder()
    56  )
    57  
    58  type AccessLogBuilder struct {
    59  	// tcpGrpcAccessLog is used when access log service is enabled in mesh config.
    60  	tcpGrpcAccessLog *accesslog.AccessLog
    61  	// httpGrpcAccessLog is used when access log service is enabled in mesh config.
    62  	httpGrpcAccessLog *accesslog.AccessLog
    63  	// tcpGrpcListenerAccessLog is used when access log service is enabled in mesh config.
    64  	tcpGrpcListenerAccessLog *accesslog.AccessLog
    65  
    66  	// file accessLog which is cached and reset on MeshConfig change.
    67  	mutex                 sync.RWMutex
    68  	fileAccesslog         *accesslog.AccessLog
    69  	listenerFileAccessLog *accesslog.AccessLog
    70  }
    71  
    72  func newAccessLogBuilder() *AccessLogBuilder {
    73  	return &AccessLogBuilder{
    74  		tcpGrpcAccessLog:         tcpGrpcAccessLog(false),
    75  		httpGrpcAccessLog:        httpGrpcAccessLog(),
    76  		tcpGrpcListenerAccessLog: tcpGrpcAccessLog(true),
    77  	}
    78  }
    79  
    80  func (b *AccessLogBuilder) setTCPAccessLog(push *model.PushContext, proxy *model.Proxy, tcp *tcp.TcpProxy, class networking.ListenerClass, svc *model.Service) {
    81  	mesh := push.Mesh
    82  	cfgs := push.Telemetry.AccessLogging(push, proxy, class, svc)
    83  
    84  	if len(cfgs) == 0 {
    85  		// No Telemetry API configured, fall back to legacy mesh config setting
    86  		if mesh.AccessLogFile != "" {
    87  			tcp.AccessLog = append(tcp.AccessLog, b.buildFileAccessLog(mesh))
    88  		}
    89  
    90  		if mesh.EnableEnvoyAccessLogService {
    91  			// Setting it to TCP as the low level one.
    92  			tcp.AccessLog = append(tcp.AccessLog, b.tcpGrpcAccessLog)
    93  		}
    94  		return
    95  	}
    96  
    97  	if al := buildAccessLogFromTelemetry(cfgs, false); len(al) != 0 {
    98  		tcp.AccessLog = append(tcp.AccessLog, al...)
    99  	}
   100  }
   101  
   102  func buildAccessLogFromTelemetry(cfgs []model.LoggingConfig, forListener bool) []*accesslog.AccessLog {
   103  	als := make([]*accesslog.AccessLog, 0, len(cfgs))
   104  	for _, c := range cfgs {
   105  		if c.Disabled {
   106  			continue
   107  		}
   108  		filters := make([]*accesslog.AccessLogFilter, 0, 2)
   109  		if forListener {
   110  			filters = append(filters, addAccessLogFilter())
   111  		}
   112  
   113  		if telFilter := buildAccessLogFilterFromTelemetry(c); telFilter != nil {
   114  			filters = append(filters, telFilter)
   115  		}
   116  
   117  		al := &accesslog.AccessLog{
   118  			Name:       c.AccessLog.Name,
   119  			ConfigType: c.AccessLog.ConfigType,
   120  			Filter:     buildAccessLogFilter(filters...),
   121  		}
   122  
   123  		als = append(als, al)
   124  	}
   125  	return als
   126  }
   127  
   128  func buildAccessLogFilterFromTelemetry(spec model.LoggingConfig) *accesslog.AccessLogFilter {
   129  	if spec.Filter == nil {
   130  		return nil
   131  	}
   132  
   133  	fl := &cel.ExpressionFilter{
   134  		Expression: spec.Filter.Expression,
   135  	}
   136  
   137  	return &accesslog.AccessLogFilter{
   138  		FilterSpecifier: &accesslog.AccessLogFilter_ExtensionFilter{
   139  			ExtensionFilter: &accesslog.ExtensionFilter{
   140  				Name:       celFilter,
   141  				ConfigType: &accesslog.ExtensionFilter_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   142  			},
   143  		},
   144  	}
   145  }
   146  
   147  func (b *AccessLogBuilder) setHTTPAccessLog(push *model.PushContext, proxy *model.Proxy,
   148  	connectionManager *hcm.HttpConnectionManager, class networking.ListenerClass, svc *model.Service,
   149  ) {
   150  	mesh := push.Mesh
   151  	cfgs := push.Telemetry.AccessLogging(push, proxy, class, svc)
   152  	if len(cfgs) == 0 {
   153  		// No Telemetry API configured, fall back to legacy mesh config setting
   154  		if mesh.AccessLogFile != "" {
   155  			connectionManager.AccessLog = append(connectionManager.AccessLog, b.buildFileAccessLog(mesh))
   156  		}
   157  
   158  		if mesh.EnableEnvoyAccessLogService {
   159  			connectionManager.AccessLog = append(connectionManager.AccessLog, b.httpGrpcAccessLog)
   160  		}
   161  		return
   162  	}
   163  
   164  	if al := buildAccessLogFromTelemetry(cfgs, false); len(al) != 0 {
   165  		connectionManager.AccessLog = append(connectionManager.AccessLog, al...)
   166  	}
   167  }
   168  
   169  func (b *AccessLogBuilder) setListenerAccessLog(push *model.PushContext, proxy *model.Proxy,
   170  	listener *listener.Listener, class networking.ListenerClass,
   171  ) {
   172  	mesh := push.Mesh
   173  	if mesh.DisableEnvoyListenerLog {
   174  		return
   175  	}
   176  
   177  	cfgs := push.Telemetry.AccessLogging(push, proxy, class, nil)
   178  
   179  	if len(cfgs) == 0 {
   180  		// No Telemetry API configured, fall back to legacy mesh config setting
   181  		if mesh.AccessLogFile != "" {
   182  			listener.AccessLog = append(listener.AccessLog, b.buildListenerFileAccessLog(mesh))
   183  		}
   184  
   185  		if mesh.EnableEnvoyAccessLogService {
   186  			// Setting it to TCP as the low level one.
   187  			listener.AccessLog = append(listener.AccessLog, b.tcpGrpcListenerAccessLog)
   188  		}
   189  
   190  		return
   191  	}
   192  
   193  	if al := buildAccessLogFromTelemetry(cfgs, true); len(al) != 0 {
   194  		listener.AccessLog = append(listener.AccessLog, al...)
   195  	}
   196  }
   197  
   198  func (b *AccessLogBuilder) buildFileAccessLog(mesh *meshconfig.MeshConfig) *accesslog.AccessLog {
   199  	if cal := b.cachedFileAccessLog(); cal != nil {
   200  		return cal
   201  	}
   202  
   203  	// We need to build access log. This is needed either on first access or when mesh config changes.
   204  	al := model.FileAccessLogFromMeshConfig(mesh.AccessLogFile, mesh)
   205  
   206  	b.mutex.Lock()
   207  	defer b.mutex.Unlock()
   208  	b.fileAccesslog = al
   209  
   210  	return al
   211  }
   212  
   213  func addAccessLogFilter() *accesslog.AccessLogFilter {
   214  	return &accesslog.AccessLogFilter{
   215  		FilterSpecifier: &accesslog.AccessLogFilter_ResponseFlagFilter{
   216  			ResponseFlagFilter: &accesslog.ResponseFlagFilter{Flags: []string{"NR"}},
   217  		},
   218  	}
   219  }
   220  
   221  func buildAccessLogFilter(f ...*accesslog.AccessLogFilter) *accesslog.AccessLogFilter {
   222  	if len(f) == 0 {
   223  		return nil
   224  	}
   225  
   226  	if len(f) == 1 {
   227  		return f[0]
   228  	}
   229  
   230  	return &accesslog.AccessLogFilter{
   231  		FilterSpecifier: &accesslog.AccessLogFilter_AndFilter{
   232  			AndFilter: &accesslog.AndFilter{
   233  				Filters: f,
   234  			},
   235  		},
   236  	}
   237  }
   238  
   239  func (b *AccessLogBuilder) buildListenerFileAccessLog(mesh *meshconfig.MeshConfig) *accesslog.AccessLog {
   240  	if cal := b.cachedListenerFileAccessLog(); cal != nil {
   241  		return cal
   242  	}
   243  
   244  	// We need to build access log. This is needed either on first access or when mesh config changes.
   245  	lal := model.FileAccessLogFromMeshConfig(mesh.AccessLogFile, mesh)
   246  	// We add ResponseFlagFilter here, as we want to get listener access logs only on scenarios where we might
   247  	// not get filter Access Logs like in cases like NR to upstream.
   248  	lal.Filter = addAccessLogFilter()
   249  
   250  	b.mutex.Lock()
   251  	defer b.mutex.Unlock()
   252  	b.listenerFileAccessLog = lal
   253  
   254  	return lal
   255  }
   256  
   257  func (b *AccessLogBuilder) cachedFileAccessLog() *accesslog.AccessLog {
   258  	b.mutex.RLock()
   259  	defer b.mutex.RUnlock()
   260  	return b.fileAccesslog
   261  }
   262  
   263  func (b *AccessLogBuilder) cachedListenerFileAccessLog() *accesslog.AccessLog {
   264  	b.mutex.RLock()
   265  	defer b.mutex.RUnlock()
   266  	return b.listenerFileAccessLog
   267  }
   268  
   269  func tcpGrpcAccessLog(isListener bool) *accesslog.AccessLog {
   270  	accessLogFriendlyName := model.TCPEnvoyAccessLogFriendlyName
   271  	if isListener {
   272  		accessLogFriendlyName = listenerEnvoyAccessLogFriendlyName
   273  	}
   274  	fl := &grpcaccesslog.TcpGrpcAccessLogConfig{
   275  		CommonConfig: &grpcaccesslog.CommonGrpcAccessLogConfig{
   276  			LogName: accessLogFriendlyName,
   277  			GrpcService: &core.GrpcService{
   278  				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
   279  					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
   280  						ClusterName: EnvoyAccessLogCluster,
   281  					},
   282  				},
   283  			},
   284  			TransportApiVersion:     core.ApiVersion_V3,
   285  			FilterStateObjectsToLog: envoyWasmStateToLog,
   286  		},
   287  	}
   288  
   289  	var filter *accesslog.AccessLogFilter
   290  	if isListener {
   291  		filter = addAccessLogFilter()
   292  	}
   293  	return &accesslog.AccessLog{
   294  		Name:       model.TCPEnvoyALSName,
   295  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   296  		Filter:     filter,
   297  	}
   298  }
   299  
   300  func httpGrpcAccessLog() *accesslog.AccessLog {
   301  	fl := &grpcaccesslog.HttpGrpcAccessLogConfig{
   302  		CommonConfig: &grpcaccesslog.CommonGrpcAccessLogConfig{
   303  			LogName: model.HTTPEnvoyAccessLogFriendlyName,
   304  			GrpcService: &core.GrpcService{
   305  				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
   306  					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
   307  						ClusterName: EnvoyAccessLogCluster,
   308  					},
   309  				},
   310  			},
   311  			TransportApiVersion:     core.ApiVersion_V3,
   312  			FilterStateObjectsToLog: envoyWasmStateToLog,
   313  		},
   314  	}
   315  
   316  	return &accesslog.AccessLog{
   317  		Name:       wellknown.HTTPGRPCAccessLog,
   318  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   319  	}
   320  }
   321  
   322  func (b *AccessLogBuilder) reset() {
   323  	b.mutex.Lock()
   324  	b.fileAccesslog = nil
   325  	b.listenerFileAccessLog = nil
   326  	b.mutex.Unlock()
   327  }