From acf2ffb0c7a3fba8b02f16c186ba81cbb025323d Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Mon, 5 Feb 2024 17:38:04 -0500 Subject: [PATCH] feat(tracing): add project hash attr Hash the project config and add it as an attribute. This can be used to group multiple spans. Signed-off-by: Milas Bowman --- internal/tracing/attributes.go | 26 +++++++++++ internal/tracing/attributes_test.go | 67 +++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 internal/tracing/attributes_test.go diff --git a/internal/tracing/attributes.go b/internal/tracing/attributes.go index 1fd2dd7f9..a603ec910 100644 --- a/internal/tracing/attributes.go +++ b/internal/tracing/attributes.go @@ -17,6 +17,9 @@ package tracing import ( + "crypto/sha256" + "encoding/json" + "fmt" "strings" "time" @@ -72,6 +75,9 @@ func ProjectOptions(proj *types.Project) SpanOptions { attribute.StringSlice("project.extensions", keys(proj.Extensions)), attribute.StringSlice("project.includes", flattenIncludeReferences(proj.IncludeReferences)), } + if projHash, ok := projectHash(proj); ok { + attrs = append(attrs, attribute.String("project.hash", projHash)) + } return []trace.SpanStartEventOption{ trace.WithAttributes(attrs...), } @@ -158,3 +164,23 @@ func flattenIncludeReferences(includeRefs map[string][]types.IncludeConfig) []st } return ret.Elements() } + +// projectHash returns a checksum from the JSON encoding of the project. +func projectHash(p *types.Project) (string, bool) { + if p == nil { + return "", false + } + // disabled services aren't included in the output, so make a copy with + // all the services active for hashing + var err error + p, err = p.WithServicesEnabled(append(p.ServiceNames(), p.DisabledServiceNames()...)...) + if err != nil { + return "", false + } + projData, err := json.Marshal(p) + if err != nil { + return "", false + } + d := sha256.Sum256(projData) + return fmt.Sprintf("%x", d), true +} diff --git a/internal/tracing/attributes_test.go b/internal/tracing/attributes_test.go new file mode 100644 index 000000000..d4277a940 --- /dev/null +++ b/internal/tracing/attributes_test.go @@ -0,0 +1,67 @@ +/* + Copyright 2024 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tracing + +import ( + "testing" + + "github.com/compose-spec/compose-go/v2/types" + "github.com/stretchr/testify/require" +) + +func TestProjectHash(t *testing.T) { + projA := &types.Project{ + Name: "fake-proj", + WorkingDir: "/tmp", + Services: map[string]types.ServiceConfig{ + "foo": {Image: "fake-image"}, + }, + DisabledServices: map[string]types.ServiceConfig{ + "bar": {Image: "diff-image"}, + }, + } + projB := &types.Project{ + Name: "fake-proj", + WorkingDir: "/tmp", + Services: map[string]types.ServiceConfig{ + "foo": {Image: "fake-image"}, + "bar": {Image: "diff-image"}, + }, + } + projC := &types.Project{ + Name: "fake-proj", + WorkingDir: "/tmp", + Services: map[string]types.ServiceConfig{ + "foo": {Image: "fake-image"}, + "bar": {Image: "diff-image"}, + "baz": {Image: "yet-another-image"}, + }, + } + + hashA, ok := projectHash(projA) + require.True(t, ok) + require.NotEmpty(t, hashA) + hashB, ok := projectHash(projB) + require.True(t, ok) + require.NotEmpty(t, hashB) + require.Equal(t, hashA, hashB) + + hashC, ok := projectHash(projC) + require.True(t, ok) + require.NotEmpty(t, hashC) + require.NotEqual(t, hashC, hashA) +}