mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved We run multiple deployments (regions) under a single GCP project. Within these deployments we scale containers. Currently all metrics target the same time series in GCP, regardless or region or container. All metics ended up in `generic_node` and Cloud Monitoring rejects the duplicate and out-of-order points. This can be fixed by setting vendor specific attributes such as `faas.name` (K_SERVICE), `faas.version` (K_REVISION), `faas.instance` (instance id from the metadata server), and `cloud.region` (metadata instance/region). # How the Problems Are Solved Use `go.opentelemetry.io/contrib/detectors/gcp` to auto-detect values as mentioned above. A new `Instrumentation.Detectors` config entry is added, which accepts and array of enum for detecotrs. At the moment only Google is accepted, other entries from [contrib](https://github.com/open-telemetry/opentelemetry-go-contrib/tree/main/detectors) can be added if deemed usefull. I've considered using `autodetect` contrib package, identifiers and constructor, but this pulls in the full AWS SDK, increasing zitadel's binary size by 50MiB. By default detection is switched off, as it relies on unauthenticated link-local calls that are insecure anywhere outside of the infrastructure it targets. It also prevents breaking existing deployments that rely on detection being off already. (May break existing trace and logging pipelines). # Additional Changes - none # Additional Context - (internal only): https://github.com/caos/infra/pull/1813 --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]>
202 lines
5.0 KiB
Go
202 lines
5.0 KiB
Go
package instrumentation
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
|
)
|
|
|
|
func TestExporterType_isNone(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
e ExporterType
|
|
want bool
|
|
}{
|
|
{
|
|
name: "unspecified is none",
|
|
e: ExporterTypeUnspecified,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "none is none",
|
|
e: ExporterTypeNone,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "stdout is not none",
|
|
e: ExporterTypeStdOut,
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.e.isNone()
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_newResource(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serviceName string
|
|
env map[string]string
|
|
want map[attribute.Key]string
|
|
}{
|
|
{
|
|
name: "service name from config",
|
|
serviceName: "zitadel",
|
|
want: map[attribute.Key]string{
|
|
semconv.ServiceNameKey: "zitadel",
|
|
},
|
|
},
|
|
{
|
|
name: "OTEL_SERVICE_NAME overrides config",
|
|
serviceName: "zitadel",
|
|
env: map[string]string{"OTEL_SERVICE_NAME": "from-env"},
|
|
want: map[attribute.Key]string{
|
|
semconv.ServiceNameKey: "from-env",
|
|
},
|
|
},
|
|
{
|
|
name: "config service name wins over OTEL_RESOURCE_ATTRIBUTES",
|
|
serviceName: "zitadel",
|
|
env: map[string]string{"OTEL_RESOURCE_ATTRIBUTES": "service.name=from-attrs,deployment.environment=qa"},
|
|
want: map[attribute.Key]string{
|
|
semconv.ServiceNameKey: "zitadel",
|
|
semconv.DeploymentEnvironmentKey: "qa",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
for k, v := range tt.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
got, err := newResource(t.Context(), tt.serviceName, nil)
|
|
require.NoError(t, err)
|
|
|
|
attributes := make(map[attribute.Key]string)
|
|
for _, kv := range got.Attributes() {
|
|
attributes[kv.Key] = kv.Value.String()
|
|
}
|
|
for key, value := range tt.want {
|
|
assert.Equal(t, value, attributes[key], "attribute %q", key)
|
|
}
|
|
// The telemetry SDK detector always contributes, proving the
|
|
// detector chain ran even when nothing else is detectable.
|
|
assert.NotEmpty(t, attributes[semconv.TelemetrySDKNameKey])
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_newResource_errors(t *testing.T) {
|
|
// Resource construction is part of startup: anything that cannot be
|
|
// resolved is returned rather than dropped, so a mistyped environment
|
|
// fails loudly instead of silently losing attributes.
|
|
tests := []struct {
|
|
name string
|
|
resourceAttrs string
|
|
detectors []DetectorType
|
|
}{
|
|
{
|
|
name: "attribute pair without a value",
|
|
resourceAttrs: "deployment.environment",
|
|
},
|
|
{
|
|
name: "trailing comma",
|
|
resourceAttrs: "deployment.environment=prod,",
|
|
},
|
|
{
|
|
name: "unsupported detector",
|
|
detectors: []DetectorType{DetectorType(99)},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.resourceAttrs != "" {
|
|
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", tt.resourceAttrs)
|
|
}
|
|
_, err := newResource(t.Context(), "zitadel", tt.detectors)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_newDetectors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
types []DetectorType
|
|
wantLen int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "nil is off",
|
|
types: nil,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "empty is off",
|
|
types: []DetectorType{},
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "unspecified is rejected",
|
|
types: []DetectorType{DetectorTypeUnspecified},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "a valid entry does not excuse an unspecified one",
|
|
types: []DetectorType{DetectorTypeGoogle, DetectorTypeUnspecified},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "google",
|
|
types: []DetectorType{DetectorTypeGoogle},
|
|
wantLen: 1,
|
|
},
|
|
{
|
|
name: "unknown type errors",
|
|
types: []DetectorType{DetectorType(99)},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := newDetectors(tt.types)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
assert.Len(t, got, tt.wantLen)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDetectorType_text(t *testing.T) {
|
|
// The config decoder resolves detectors through encoding.TextUnmarshaler,
|
|
// so the YAML and env var spellings have to round-trip.
|
|
for _, spelling := range []string{"google", "Google"} {
|
|
var typ DetectorType
|
|
require.NoError(t, typ.UnmarshalText([]byte(spelling)))
|
|
assert.Equal(t, DetectorTypeGoogle, typ)
|
|
}
|
|
|
|
// The empty string still parses, because the zero value is a member of the
|
|
// enum by project convention. It is newDetectors that rejects it as a list
|
|
// entry, so an empty string in the config fails startup rather than being
|
|
// silently skipped.
|
|
var unspecified DetectorType
|
|
require.NoError(t, unspecified.UnmarshalText([]byte("")))
|
|
assert.Equal(t, DetectorTypeUnspecified, unspecified)
|
|
_, err := newDetectors([]DetectorType{unspecified})
|
|
assert.Error(t, err)
|
|
|
|
var unknown DetectorType
|
|
assert.Error(t, unknown.UnmarshalText([]byte("aws")))
|
|
}
|