docker_context.go 3.9 KB

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