istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/telemetry_logging.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 model
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	accesslog "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
    22  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    23  	fileaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/file/v3"
    24  	grpcaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
    25  	otelaccesslog "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/open_telemetry/v3"
    26  	celformatter "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/cel/v3"
    27  	metadataformatter "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/metadata/v3"
    28  	reqwithoutquery "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/req_without_query/v3"
    29  	otlpcommon "go.opentelemetry.io/proto/otlp/common/v1"
    30  	"google.golang.org/protobuf/types/known/structpb"
    31  
    32  	meshconfig "istio.io/api/mesh/v1alpha1"
    33  	"istio.io/istio/pilot/pkg/features"
    34  	"istio.io/istio/pilot/pkg/util/protoconv"
    35  	"istio.io/istio/pkg/config/host"
    36  	"istio.io/istio/pkg/maps"
    37  	"istio.io/istio/pkg/slices"
    38  	"istio.io/istio/pkg/util/protomarshal"
    39  	"istio.io/istio/pkg/wellknown"
    40  )
    41  
    42  const (
    43  	// EnvoyTextLogFormat format for envoy text based access logs for Istio 1.9 onwards.
    44  	// This includes the additional new operator RESPONSE_CODE_DETAILS and CONNECTION_TERMINATION_DETAILS that tells
    45  	// the reason why Envoy rejects a request.
    46  	EnvoyTextLogFormat = "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% " +
    47  		"%PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% " +
    48  		"%RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS% " +
    49  		"\"%UPSTREAM_TRANSPORT_FAILURE_REASON%\" %BYTES_RECEIVED% %BYTES_SENT% " +
    50  		"%DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" " +
    51  		"\"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" " +
    52  		"%UPSTREAM_CLUSTER% %UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% " +
    53  		"%DOWNSTREAM_REMOTE_ADDRESS% %REQUESTED_SERVER_NAME% %ROUTE_NAME%\n"
    54  
    55  	HTTPEnvoyAccessLogFriendlyName = "http_envoy_accesslog"
    56  	TCPEnvoyAccessLogFriendlyName  = "tcp_envoy_accesslog"
    57  	OtelEnvoyAccessLogFriendlyName = "otel_envoy_accesslog"
    58  
    59  	TCPEnvoyALSName  = "envoy.tcp_grpc_access_log"
    60  	OtelEnvoyALSName = "envoy.access_loggers.open_telemetry"
    61  
    62  	reqWithoutQueryCommandOperator = "%REQ_WITHOUT_QUERY"
    63  	metadataCommandOperator        = "%METADATA"
    64  	celCommandOperator             = "%CEL"
    65  
    66  	DevStdout = "/dev/stdout"
    67  
    68  	builtinEnvoyAccessLogProvider = "envoy"
    69  )
    70  
    71  var (
    72  	// this is used for testing. it should not be changed in regular code.
    73  	clusterLookupFn = LookupCluster
    74  
    75  	// EnvoyJSONLogFormatIstio map of values for envoy json based access logs for Istio 1.9 onwards.
    76  	// This includes the additional log operator RESPONSE_CODE_DETAILS and CONNECTION_TERMINATION_DETAILS that tells
    77  	// the reason why Envoy rejects a request.
    78  	EnvoyJSONLogFormatIstio = &structpb.Struct{
    79  		Fields: map[string]*structpb.Value{
    80  			"start_time":                        {Kind: &structpb.Value_StringValue{StringValue: "%START_TIME%"}},
    81  			"route_name":                        {Kind: &structpb.Value_StringValue{StringValue: "%ROUTE_NAME%"}},
    82  			"method":                            {Kind: &structpb.Value_StringValue{StringValue: "%REQ(:METHOD)%"}},
    83  			"path":                              {Kind: &structpb.Value_StringValue{StringValue: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"}},
    84  			"protocol":                          {Kind: &structpb.Value_StringValue{StringValue: "%PROTOCOL%"}},
    85  			"response_code":                     {Kind: &structpb.Value_StringValue{StringValue: "%RESPONSE_CODE%"}},
    86  			"response_flags":                    {Kind: &structpb.Value_StringValue{StringValue: "%RESPONSE_FLAGS%"}},
    87  			"response_code_details":             {Kind: &structpb.Value_StringValue{StringValue: "%RESPONSE_CODE_DETAILS%"}},
    88  			"connection_termination_details":    {Kind: &structpb.Value_StringValue{StringValue: "%CONNECTION_TERMINATION_DETAILS%"}},
    89  			"bytes_received":                    {Kind: &structpb.Value_StringValue{StringValue: "%BYTES_RECEIVED%"}},
    90  			"bytes_sent":                        {Kind: &structpb.Value_StringValue{StringValue: "%BYTES_SENT%"}},
    91  			"duration":                          {Kind: &structpb.Value_StringValue{StringValue: "%DURATION%"}},
    92  			"upstream_service_time":             {Kind: &structpb.Value_StringValue{StringValue: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%"}},
    93  			"x_forwarded_for":                   {Kind: &structpb.Value_StringValue{StringValue: "%REQ(X-FORWARDED-FOR)%"}},
    94  			"user_agent":                        {Kind: &structpb.Value_StringValue{StringValue: "%REQ(USER-AGENT)%"}},
    95  			"request_id":                        {Kind: &structpb.Value_StringValue{StringValue: "%REQ(X-REQUEST-ID)%"}},
    96  			"authority":                         {Kind: &structpb.Value_StringValue{StringValue: "%REQ(:AUTHORITY)%"}},
    97  			"upstream_host":                     {Kind: &structpb.Value_StringValue{StringValue: "%UPSTREAM_HOST%"}},
    98  			"upstream_cluster":                  {Kind: &structpb.Value_StringValue{StringValue: "%UPSTREAM_CLUSTER%"}},
    99  			"upstream_local_address":            {Kind: &structpb.Value_StringValue{StringValue: "%UPSTREAM_LOCAL_ADDRESS%"}},
   100  			"downstream_local_address":          {Kind: &structpb.Value_StringValue{StringValue: "%DOWNSTREAM_LOCAL_ADDRESS%"}},
   101  			"downstream_remote_address":         {Kind: &structpb.Value_StringValue{StringValue: "%DOWNSTREAM_REMOTE_ADDRESS%"}},
   102  			"requested_server_name":             {Kind: &structpb.Value_StringValue{StringValue: "%REQUESTED_SERVER_NAME%"}},
   103  			"upstream_transport_failure_reason": {Kind: &structpb.Value_StringValue{StringValue: "%UPSTREAM_TRANSPORT_FAILURE_REASON%"}},
   104  		},
   105  	}
   106  
   107  	// State logged by the metadata exchange filter about the upstream and downstream service instances
   108  	// We need to propagate these as part of access log service stream
   109  	// Logging them by default on the console may be an issue as the base64 encoded string is bound to be a big one.
   110  	// But end users can certainly configure it on their own via the meshConfig using the %FILTER_STATE% macro.
   111  	envoyWasmStateToLog = []string{"wasm.upstream_peer", "wasm.upstream_peer_id", "wasm.downstream_peer", "wasm.downstream_peer_id"}
   112  
   113  	// reqWithoutQueryFormatter configures additional formatters needed for some of the format strings like "REQ_WITHOUT_QUERY"
   114  	reqWithoutQueryFormatter = &core.TypedExtensionConfig{
   115  		Name:        "envoy.formatter.req_without_query",
   116  		TypedConfig: protoconv.MessageToAny(&reqwithoutquery.ReqWithoutQuery{}),
   117  	}
   118  
   119  	// metadataFormatter configures additional formatters needed for some of the format strings like "METADATA"
   120  	// for more information, see https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/formatter/metadata/v3/metadata.proto
   121  	metadataFormatter = &core.TypedExtensionConfig{
   122  		Name:        "envoy.formatter.metadata",
   123  		TypedConfig: protoconv.MessageToAny(&metadataformatter.Metadata{}),
   124  	}
   125  
   126  	// celFormatter configures additional formatters needed for some of the format strings like "CEL"
   127  	// for more information, see https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/formatter/cel/v3/cel.proto
   128  	celFormatter = &core.TypedExtensionConfig{
   129  		Name:        "envoy.formatter.cel",
   130  		TypedConfig: protoconv.MessageToAny(&celformatter.Cel{}),
   131  	}
   132  )
   133  
   134  // configureFromProviderConfigHandled contains the number of providers we handle below.
   135  // This is to ensure this stays in sync as new handlers are added
   136  // STOP. DO NOT UPDATE THIS WITHOUT UPDATING telemetryAccessLog.
   137  const telemetryAccessLogHandled = 14
   138  
   139  func telemetryAccessLog(push *PushContext, fp *meshconfig.MeshConfig_ExtensionProvider) *accesslog.AccessLog {
   140  	var al *accesslog.AccessLog
   141  	switch prov := fp.Provider.(type) {
   142  	case *meshconfig.MeshConfig_ExtensionProvider_EnvoyFileAccessLog:
   143  		// For built-in provider, fallback to MeshConfig for formatting options when LogFormat unset.
   144  		if fp.Name == builtinEnvoyAccessLogProvider && prov.EnvoyFileAccessLog.LogFormat == nil {
   145  			al = FileAccessLogFromMeshConfig(prov.EnvoyFileAccessLog.Path, push.Mesh)
   146  		} else {
   147  			al = fileAccessLogFromTelemetry(prov.EnvoyFileAccessLog)
   148  		}
   149  	case *meshconfig.MeshConfig_ExtensionProvider_EnvoyHttpAls:
   150  		al = httpGrpcAccessLogFromTelemetry(push, prov.EnvoyHttpAls)
   151  	case *meshconfig.MeshConfig_ExtensionProvider_EnvoyTcpAls:
   152  		al = tcpGrpcAccessLogFromTelemetry(push, prov.EnvoyTcpAls)
   153  	case *meshconfig.MeshConfig_ExtensionProvider_EnvoyOtelAls:
   154  		al = openTelemetryLog(push, prov.EnvoyOtelAls)
   155  	case *meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzHttp,
   156  		*meshconfig.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc,
   157  		*meshconfig.MeshConfig_ExtensionProvider_Zipkin,
   158  		*meshconfig.MeshConfig_ExtensionProvider_Lightstep,
   159  		*meshconfig.MeshConfig_ExtensionProvider_Datadog,
   160  		*meshconfig.MeshConfig_ExtensionProvider_Skywalking,
   161  		*meshconfig.MeshConfig_ExtensionProvider_Opencensus,
   162  		*meshconfig.MeshConfig_ExtensionProvider_Opentelemetry,
   163  		*meshconfig.MeshConfig_ExtensionProvider_Prometheus,
   164  		*meshconfig.MeshConfig_ExtensionProvider_Stackdriver:
   165  		// No access logs supported for this provider
   166  		// Stackdriver is a special case as its handled in the Metrics logic, as it uses a shared filter
   167  		return nil
   168  	}
   169  
   170  	return al
   171  }
   172  
   173  func tcpGrpcAccessLogFromTelemetry(push *PushContext, prov *meshconfig.MeshConfig_ExtensionProvider_EnvoyTcpGrpcV3LogProvider) *accesslog.AccessLog {
   174  	logName := TCPEnvoyAccessLogFriendlyName
   175  	if prov != nil && prov.LogName != "" {
   176  		logName = prov.LogName
   177  	}
   178  
   179  	filterObjects := envoyWasmStateToLog
   180  	if len(prov.FilterStateObjectsToLog) != 0 {
   181  		filterObjects = prov.FilterStateObjectsToLog
   182  	}
   183  
   184  	hostname, cluster, err := clusterLookupFn(push, prov.Service, int(prov.Port))
   185  	if err != nil {
   186  		IncLookupClusterFailures("envoyTCPAls")
   187  		log.Errorf("could not find cluster for tcp grpc provider %q: %v", prov, err)
   188  		return nil
   189  	}
   190  
   191  	fl := &grpcaccesslog.TcpGrpcAccessLogConfig{
   192  		CommonConfig: &grpcaccesslog.CommonGrpcAccessLogConfig{
   193  			LogName: logName,
   194  			GrpcService: &core.GrpcService{
   195  				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
   196  					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
   197  						ClusterName: cluster,
   198  						Authority:   hostname,
   199  					},
   200  				},
   201  			},
   202  			TransportApiVersion:     core.ApiVersion_V3,
   203  			FilterStateObjectsToLog: filterObjects,
   204  		},
   205  	}
   206  
   207  	return &accesslog.AccessLog{
   208  		Name:       TCPEnvoyALSName,
   209  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   210  	}
   211  }
   212  
   213  func fileAccessLogFromTelemetry(prov *meshconfig.MeshConfig_ExtensionProvider_EnvoyFileAccessLogProvider) *accesslog.AccessLog {
   214  	p := prov.Path
   215  	if p == "" {
   216  		p = DevStdout
   217  	}
   218  	fl := &fileaccesslog.FileAccessLog{
   219  		Path: p,
   220  	}
   221  
   222  	var needsFormatter []*core.TypedExtensionConfig
   223  	if prov.LogFormat != nil {
   224  		switch logFormat := prov.LogFormat.LogFormat.(type) {
   225  		case *meshconfig.MeshConfig_ExtensionProvider_EnvoyFileAccessLogProvider_LogFormat_Text:
   226  			fl.AccessLogFormat, needsFormatter = buildFileAccessTextLogFormat(logFormat.Text)
   227  		case *meshconfig.MeshConfig_ExtensionProvider_EnvoyFileAccessLogProvider_LogFormat_Labels:
   228  			fl.AccessLogFormat, needsFormatter = buildFileAccessJSONLogFormat(logFormat)
   229  		}
   230  	} else {
   231  		fl.AccessLogFormat, needsFormatter = buildFileAccessTextLogFormat("")
   232  	}
   233  	if len(needsFormatter) != 0 {
   234  		fl.GetLogFormat().Formatters = needsFormatter
   235  	}
   236  
   237  	al := &accesslog.AccessLog{
   238  		Name:       wellknown.FileAccessLog,
   239  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   240  	}
   241  
   242  	return al
   243  }
   244  
   245  func buildFileAccessTextLogFormat(logFormatText string) (*fileaccesslog.FileAccessLog_LogFormat, []*core.TypedExtensionConfig) {
   246  	formatString := fileAccessLogFormat(logFormatText)
   247  	formatters := accessLogTextFormatters(formatString)
   248  
   249  	return &fileaccesslog.FileAccessLog_LogFormat{
   250  		LogFormat: &core.SubstitutionFormatString{
   251  			Format: &core.SubstitutionFormatString_TextFormatSource{
   252  				TextFormatSource: &core.DataSource{
   253  					Specifier: &core.DataSource_InlineString{
   254  						InlineString: formatString,
   255  					},
   256  				},
   257  			},
   258  		},
   259  	}, formatters
   260  }
   261  
   262  func buildFileAccessJSONLogFormat(
   263  	logFormat *meshconfig.MeshConfig_ExtensionProvider_EnvoyFileAccessLogProvider_LogFormat_Labels,
   264  ) (*fileaccesslog.FileAccessLog_LogFormat, []*core.TypedExtensionConfig) {
   265  	jsonLogStruct := EnvoyJSONLogFormatIstio
   266  	if logFormat.Labels != nil {
   267  		jsonLogStruct = logFormat.Labels
   268  	}
   269  
   270  	// allow default behavior when no labels supplied.
   271  	if len(jsonLogStruct.Fields) == 0 {
   272  		jsonLogStruct = EnvoyJSONLogFormatIstio
   273  	}
   274  
   275  	formatters := accessLogJSONFormatters(jsonLogStruct)
   276  	return &fileaccesslog.FileAccessLog_LogFormat{
   277  		LogFormat: &core.SubstitutionFormatString{
   278  			Format: &core.SubstitutionFormatString_JsonFormat{
   279  				JsonFormat: jsonLogStruct,
   280  			},
   281  			JsonFormatOptions: &core.JsonFormatOptions{SortProperties: true},
   282  		},
   283  	}, formatters
   284  }
   285  
   286  func accessLogJSONFormatters(jsonLogStruct *structpb.Struct) []*core.TypedExtensionConfig {
   287  	reqWithoutQuery, metadata, cel := false, false, false
   288  	for _, value := range jsonLogStruct.Fields {
   289  		if reqWithoutQuery && metadata {
   290  			break
   291  		}
   292  
   293  		if !reqWithoutQuery && strings.Contains(value.GetStringValue(), reqWithoutQueryCommandOperator) {
   294  			reqWithoutQuery = true
   295  		}
   296  		if !metadata && strings.Contains(value.GetStringValue(), metadataCommandOperator) {
   297  			metadata = true
   298  		}
   299  		if !cel && strings.Contains(value.GetStringValue(), celCommandOperator) {
   300  			cel = true
   301  		}
   302  	}
   303  
   304  	formatters := make([]*core.TypedExtensionConfig, 0, 2)
   305  	if reqWithoutQuery {
   306  		formatters = append(formatters, reqWithoutQueryFormatter)
   307  	}
   308  	if metadata {
   309  		formatters = append(formatters, metadataFormatter)
   310  	}
   311  	if cel {
   312  		formatters = append(formatters, celFormatter)
   313  	}
   314  
   315  	return formatters
   316  }
   317  
   318  func accessLogTextFormatters(text string) []*core.TypedExtensionConfig {
   319  	formatters := make([]*core.TypedExtensionConfig, 0, 2)
   320  	if strings.Contains(text, reqWithoutQueryCommandOperator) {
   321  		formatters = append(formatters, reqWithoutQueryFormatter)
   322  	}
   323  	if strings.Contains(text, metadataCommandOperator) {
   324  		formatters = append(formatters, metadataFormatter)
   325  	}
   326  	if strings.Contains(text, celCommandOperator) {
   327  		formatters = append(formatters, celFormatter)
   328  	}
   329  
   330  	return formatters
   331  }
   332  
   333  func httpGrpcAccessLogFromTelemetry(push *PushContext, prov *meshconfig.MeshConfig_ExtensionProvider_EnvoyHttpGrpcV3LogProvider) *accesslog.AccessLog {
   334  	logName := HTTPEnvoyAccessLogFriendlyName
   335  	if prov != nil && prov.LogName != "" {
   336  		logName = prov.LogName
   337  	}
   338  
   339  	filterObjects := envoyWasmStateToLog
   340  	if len(prov.FilterStateObjectsToLog) != 0 {
   341  		filterObjects = prov.FilterStateObjectsToLog
   342  	}
   343  
   344  	hostname, cluster, err := clusterLookupFn(push, prov.Service, int(prov.Port))
   345  	if err != nil {
   346  		IncLookupClusterFailures("envoyHTTPAls")
   347  		log.Errorf("could not find cluster for http grpc provider %q: %v", prov, err)
   348  		return nil
   349  	}
   350  
   351  	fl := &grpcaccesslog.HttpGrpcAccessLogConfig{
   352  		CommonConfig: &grpcaccesslog.CommonGrpcAccessLogConfig{
   353  			LogName: logName,
   354  			GrpcService: &core.GrpcService{
   355  				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
   356  					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
   357  						ClusterName: cluster,
   358  						Authority:   hostname,
   359  					},
   360  				},
   361  			},
   362  			TransportApiVersion:     core.ApiVersion_V3,
   363  			FilterStateObjectsToLog: filterObjects,
   364  		},
   365  		AdditionalRequestHeadersToLog:   prov.AdditionalRequestHeadersToLog,
   366  		AdditionalResponseHeadersToLog:  prov.AdditionalResponseHeadersToLog,
   367  		AdditionalResponseTrailersToLog: prov.AdditionalResponseTrailersToLog,
   368  	}
   369  
   370  	return &accesslog.AccessLog{
   371  		Name:       wellknown.HTTPGRPCAccessLog,
   372  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   373  	}
   374  }
   375  
   376  func fileAccessLogFormat(formatString string) string {
   377  	if formatString != "" {
   378  		// From the spec: "NOTE: Istio will insert a newline ('\n') on all formats (if missing)."
   379  		if !strings.HasSuffix(formatString, "\n") {
   380  			formatString += "\n"
   381  		}
   382  
   383  		return formatString
   384  	}
   385  
   386  	return EnvoyTextLogFormat
   387  }
   388  
   389  func FileAccessLogFromMeshConfig(path string, mesh *meshconfig.MeshConfig) *accesslog.AccessLog {
   390  	// We need to build access log. This is needed either on first access or when mesh config changes.
   391  	fl := &fileaccesslog.FileAccessLog{
   392  		Path: path,
   393  	}
   394  	var formatters []*core.TypedExtensionConfig
   395  	switch mesh.AccessLogEncoding {
   396  	case meshconfig.MeshConfig_TEXT:
   397  		formatString := fileAccessLogFormat(mesh.AccessLogFormat)
   398  		formatters = accessLogTextFormatters(formatString)
   399  		fl.AccessLogFormat = &fileaccesslog.FileAccessLog_LogFormat{
   400  			LogFormat: &core.SubstitutionFormatString{
   401  				Format: &core.SubstitutionFormatString_TextFormatSource{
   402  					TextFormatSource: &core.DataSource{
   403  						Specifier: &core.DataSource_InlineString{
   404  							InlineString: formatString,
   405  						},
   406  					},
   407  				},
   408  			},
   409  		}
   410  	case meshconfig.MeshConfig_JSON:
   411  		jsonLogStruct := EnvoyJSONLogFormatIstio
   412  		if len(mesh.AccessLogFormat) > 0 {
   413  			parsedJSONLogStruct := structpb.Struct{}
   414  			if err := protomarshal.UnmarshalAllowUnknown([]byte(mesh.AccessLogFormat), &parsedJSONLogStruct); err != nil {
   415  				log.Errorf("error parsing provided json log format, default log format will be used: %v", err)
   416  			} else {
   417  				jsonLogStruct = &parsedJSONLogStruct
   418  			}
   419  		}
   420  		formatters = accessLogJSONFormatters(jsonLogStruct)
   421  		fl.AccessLogFormat = &fileaccesslog.FileAccessLog_LogFormat{
   422  			LogFormat: &core.SubstitutionFormatString{
   423  				Format: &core.SubstitutionFormatString_JsonFormat{
   424  					JsonFormat: jsonLogStruct,
   425  				},
   426  				JsonFormatOptions: &core.JsonFormatOptions{SortProperties: true},
   427  			},
   428  		}
   429  	default:
   430  		log.Warnf("unsupported access log format %v", mesh.AccessLogEncoding)
   431  	}
   432  
   433  	if len(formatters) > 0 {
   434  		fl.GetLogFormat().Formatters = formatters
   435  	}
   436  	al := &accesslog.AccessLog{
   437  		Name:       wellknown.FileAccessLog,
   438  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(fl)},
   439  	}
   440  
   441  	return al
   442  }
   443  
   444  func openTelemetryLog(pushCtx *PushContext,
   445  	provider *meshconfig.MeshConfig_ExtensionProvider_EnvoyOpenTelemetryLogProvider,
   446  ) *accesslog.AccessLog {
   447  	hostname, cluster, err := clusterLookupFn(pushCtx, provider.Service, int(provider.Port))
   448  	if err != nil {
   449  		IncLookupClusterFailures("envoyOtelAls")
   450  		log.Errorf("could not find cluster for open telemetry provider %q: %v", provider, err)
   451  		return nil
   452  	}
   453  
   454  	logName := provider.LogName
   455  	if logName == "" {
   456  		logName = OtelEnvoyAccessLogFriendlyName
   457  	}
   458  
   459  	f := EnvoyTextLogFormat
   460  	if provider.LogFormat != nil && provider.LogFormat.Text != "" {
   461  		f = provider.LogFormat.Text
   462  	}
   463  
   464  	var labels *structpb.Struct
   465  	if provider.LogFormat != nil {
   466  		labels = provider.LogFormat.Labels
   467  	}
   468  
   469  	cfg := buildOpenTelemetryAccessLogConfig(logName, hostname, cluster, f, labels)
   470  
   471  	return &accesslog.AccessLog{
   472  		Name:       OtelEnvoyALSName,
   473  		ConfigType: &accesslog.AccessLog_TypedConfig{TypedConfig: protoconv.MessageToAny(cfg)},
   474  	}
   475  }
   476  
   477  func buildOpenTelemetryAccessLogConfig(logName, hostname, clusterName, format string, labels *structpb.Struct) *otelaccesslog.OpenTelemetryAccessLogConfig {
   478  	cfg := &otelaccesslog.OpenTelemetryAccessLogConfig{
   479  		CommonConfig: &grpcaccesslog.CommonGrpcAccessLogConfig{
   480  			LogName: logName,
   481  			GrpcService: &core.GrpcService{
   482  				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
   483  					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{
   484  						ClusterName: clusterName,
   485  						Authority:   hostname,
   486  					},
   487  				},
   488  			},
   489  			TransportApiVersion:     core.ApiVersion_V3,
   490  			FilterStateObjectsToLog: envoyWasmStateToLog,
   491  		},
   492  		DisableBuiltinLabels: !features.EnableOTELBuiltinResourceLabels,
   493  	}
   494  
   495  	if format != "" {
   496  		cfg.Body = &otlpcommon.AnyValue{
   497  			Value: &otlpcommon.AnyValue_StringValue{
   498  				StringValue: format,
   499  			},
   500  		}
   501  	}
   502  
   503  	if labels != nil && len(labels.Fields) != 0 {
   504  		cfg.Attributes = &otlpcommon.KeyValueList{
   505  			Values: ConvertStructToAttributeKeyValues(labels.Fields),
   506  		}
   507  	}
   508  
   509  	return cfg
   510  }
   511  
   512  func ConvertStructToAttributeKeyValues(labels map[string]*structpb.Value) []*otlpcommon.KeyValue {
   513  	if len(labels) == 0 {
   514  		return nil
   515  	}
   516  	attrList := make([]*otlpcommon.KeyValue, 0, len(labels))
   517  	// Sort keys to ensure stable XDS generation
   518  	for _, key := range slices.Sort(maps.Keys(labels)) {
   519  		value := labels[key]
   520  		kv := &otlpcommon.KeyValue{
   521  			Key:   key,
   522  			Value: &otlpcommon.AnyValue{Value: &otlpcommon.AnyValue_StringValue{StringValue: value.GetStringValue()}},
   523  		}
   524  		attrList = append(attrList, kv)
   525  	}
   526  	return attrList
   527  }
   528  
   529  func LookupCluster(push *PushContext, service string, port int) (hostname string, cluster string, err error) {
   530  	if service == "" {
   531  		err = fmt.Errorf("service must not be empty")
   532  		return
   533  	}
   534  
   535  	// TODO(yangminzhu): Verify the service and its cluster is supported, e.g. resolution type is not OriginalDst.
   536  	if parts := strings.Split(service, "/"); len(parts) == 2 {
   537  		namespace, name := parts[0], parts[1]
   538  		if svc := push.ServiceIndex.HostnameAndNamespace[host.Name(name)][namespace]; svc != nil {
   539  			hostname = string(svc.Hostname)
   540  			cluster = BuildSubsetKey(TrafficDirectionOutbound, "", svc.Hostname, port)
   541  			return
   542  		}
   543  	} else {
   544  		namespaceToServices := push.ServiceIndex.HostnameAndNamespace[host.Name(service)]
   545  		var namespaces []string
   546  		for k := range namespaceToServices {
   547  			namespaces = append(namespaces, k)
   548  		}
   549  		// If namespace is omitted, return successfully if there is only one such host name in the service index.
   550  		if len(namespaces) == 1 {
   551  			svc := namespaceToServices[namespaces[0]]
   552  			hostname = string(svc.Hostname)
   553  			cluster = BuildSubsetKey(TrafficDirectionOutbound, "", svc.Hostname, port)
   554  			return
   555  		} else if len(namespaces) > 1 {
   556  			err = fmt.Errorf("found %s in multiple namespaces %v, specify the namespace explicitly in "+
   557  				"the format of <Namespace>/<Hostname>", service, namespaces)
   558  			return
   559  		}
   560  	}
   561  
   562  	err = fmt.Errorf("could not find service %s in Istio service registry", service)
   563  	return
   564  }