docker_context.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /*
  2. Copyright 2023 Docker Compose CLI authors
  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. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package tracing
  14. import (
  15. "fmt"
  16. "os"
  17. "github.com/docker/cli/cli/command"
  18. "github.com/docker/cli/cli/context/store"
  19. "github.com/docker/compose/v2/internal/memnet"
  20. "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
  21. "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
  22. "google.golang.org/grpc"
  23. "google.golang.org/grpc/credentials/insecure"
  24. )
  25. const otelConfigFieldName = "otel"
  26. // traceClientFromDockerContext creates a gRPC OTLP client based on metadata
  27. // from the active Docker CLI context.
  28. func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptrace.Client, error) {
  29. // attempt to extract an OTEL config from the Docker context to enable
  30. // automatic integration with Docker Desktop;
  31. cfg, err := ConfigFromDockerContext(dockerCli.ContextStore(), dockerCli.CurrentContext())
  32. if err != nil {
  33. return nil, fmt.Errorf("loading otel config from docker context metadata: %w", err)
  34. }
  35. if cfg.Endpoint == "" {
  36. return nil, nil
  37. }
  38. // HACK: unfortunately _all_ public OTEL initialization functions
  39. // implicitly read from the OS env, so temporarily unset them all and
  40. // restore afterwards
  41. defer func() {
  42. for k, v := range otelEnv {
  43. if err := os.Setenv(k, v); err != nil {
  44. panic(fmt.Errorf("restoring env for %q: %w", k, err))
  45. }
  46. }
  47. }()
  48. for k := range otelEnv {
  49. if err := os.Unsetenv(k); err != nil {
  50. return nil, fmt.Errorf("stashing env for %q: %w", k, err)
  51. }
  52. }
  53. conn, err := grpc.NewClient(cfg.Endpoint,
  54. grpc.WithContextDialer(memnet.DialEndpoint),
  55. // this dial is restricted to using a local Unix socket / named pipe,
  56. // so there is no need for TLS
  57. grpc.WithTransportCredentials(insecure.NewCredentials()),
  58. )
  59. if err != nil {
  60. return nil, fmt.Errorf("initializing otel connection from docker context metadata: %w", err)
  61. }
  62. client := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn))
  63. return client, nil
  64. }
  65. // ConfigFromDockerContext inspects extra metadata included as part of the
  66. // specified Docker context to try and extract a valid OTLP client configuration.
  67. func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) {
  68. meta, err := st.GetMetadata(name)
  69. if err != nil {
  70. return OTLPConfig{}, err
  71. }
  72. var otelCfg interface{}
  73. switch m := meta.Metadata.(type) {
  74. case command.DockerContext:
  75. otelCfg = m.AdditionalFields[otelConfigFieldName]
  76. case map[string]interface{}:
  77. otelCfg = m[otelConfigFieldName]
  78. }
  79. if otelCfg == nil {
  80. return OTLPConfig{}, nil
  81. }
  82. otelMap, ok := otelCfg.(map[string]interface{})
  83. if !ok {
  84. return OTLPConfig{}, fmt.Errorf(
  85. "unexpected type for field %q: %T (expected: %T)",
  86. otelConfigFieldName,
  87. otelCfg,
  88. otelMap,
  89. )
  90. }
  91. // keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
  92. cfg := OTLPConfig{
  93. Endpoint: valueOrDefault[string](otelMap, "OTEL_EXPORTER_OTLP_ENDPOINT"),
  94. }
  95. return cfg, nil
  96. }
  97. // valueOrDefault returns the type-cast value at the specified key in the map
  98. // if present and the correct type; otherwise, it returns the default value for
  99. // T.
  100. func valueOrDefault[T any](m map[string]interface{}, key string) T {
  101. if v, ok := m[key].(T); ok {
  102. return v
  103. }
  104. return *new(T)
  105. }