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 }