mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved Zitadel did not provide easy correlation between errors, logs, traces and metrics. The configuration for those instrumentations was also not consistent, with some supporting different exporters then others. Implementation and parsing of config was also spaghettified over multiple packages, with awkward parsing and inconsistent naming of options. # How the Problems Are Solved All telemetry is now merged under the name "instrumentation". Why? 1. We thought it was a good idea in the past to call the milestone exporter `Telemtry` in the runtime config. Calling this `TelemetryV2` looks weird. 2. Not everything is a meter and not everything is sent (tele...). 3. It's also [defined](https://opentelemetry.io/docs/concepts/instrumentation/) as such by the OTEL documentation. ## New features - Adds structured, context based logging with trace-ID awareness - Static log fields are added to the context, such as service and request path - Static log fields are injected in each logline emitted by the application - Structured logs can also be send to an otel exporter - Structured logs can be printed to StdErr in text and JSON format - Error sinks make sure every error is logged at the correct level: - Warnings for client side errors (HTTP 400 range, Invalid request etc) - Error for server side errors (Internal server errors) - Metrics can now also be send to a OTEL collector. (previously they could only be scraped from `/debug/metrics` with prometheus) ## Exporters This change adds all the exporters supported by OTEL upstream and some google specific exporters for our cloud deployment. - StdOut / StdErr: all instrumentations - OTEL gRPC / HTTP: all instrumentations - Google: all instrumentations except logging - Prometheus (pull-based): only metrics The exception is profiling, which only supports the google exporting due to lack of support by OTEL upstream. ## Configuration and structure - All instrumentation is moved into the new `backend/v3/instrumentation` package. It reuses configuration types, so both code and runtime configuration are easier to understand. - The `internal/telemetry` packages are removed. - Instrumentation is started with a single function and a proper shutdown function is now provided. - Legacy configuration is still parsed from the runtime config, as long as the new configuration is disabled. This allows backporting this feature to v4 without breaking existing configurations. # Additional Changes - Devcontainer: set `$PATH` variable so installed go binaries can be run individually, without NX. - NX: install GCI tool to fix imports # Additional Context - Closes https://github.com/zitadel/zitadel/issues/8408 - Closes https://github.com/zitadel/zitadel/issues/6664 - Backport to v4
480 lines
10 KiB
Go
480 lines
10 KiB
Go
package actions
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/zitadel/zitadel/internal/logstore"
|
|
"github.com/zitadel/zitadel/internal/logstore/record"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func Test_isHostBlocked(t *testing.T) {
|
|
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
|
|
var denyList = []AddressChecker{
|
|
mustNewHostChecker(t, "192.168.5.0/24"),
|
|
mustNewHostChecker(t, "127.0.0.1"),
|
|
mustNewHostChecker(t, "test.com"),
|
|
}
|
|
type fields struct {
|
|
lookup func(host string) ([]net.IP, error)
|
|
}
|
|
type args struct {
|
|
address *url.URL
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want error
|
|
}{
|
|
{
|
|
name: "in range",
|
|
args: args{
|
|
address: mustNewURL(t, "https://192.168.5.4/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("192.168.5.0/24"),
|
|
},
|
|
{
|
|
name: "exact ip",
|
|
args: args{
|
|
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("127.0.0.1"),
|
|
},
|
|
{
|
|
name: "address match",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test.com:42/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("test.com"),
|
|
},
|
|
{
|
|
name: "address not match",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "looked up ip matches",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: NewAddressDeniedError("127.0.0.1"),
|
|
},
|
|
{
|
|
name: "looked up failure",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return nil, io.EOF
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: zerrors.ThrowInternal(io.EOF, "ACTIO-4m9s2", "lookup failed"),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
trans := &transport{
|
|
lookup: tt.fields.lookup,
|
|
}
|
|
got := trans.isHostBlocked(denyList, tt.args.address)
|
|
assert.ErrorIs(t, got, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustNewHostChecker(t *testing.T, ip string) AddressChecker {
|
|
t.Helper()
|
|
checker, err := NewHostChecker(ip)
|
|
if err != nil {
|
|
t.Errorf("unable to parse cidr of %q because: %v", ip, err)
|
|
t.FailNow()
|
|
}
|
|
return checker
|
|
}
|
|
|
|
func mustNewURL(t *testing.T, raw string) *url.URL {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
t.Errorf("unable to parse address of %q because: %v", raw, err)
|
|
t.FailNow()
|
|
}
|
|
return u
|
|
}
|
|
|
|
func TestHTTP_fetchConfigFromArg(t *testing.T) {
|
|
runtime := goja.New()
|
|
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
|
|
|
type args struct {
|
|
arg *goja.Object
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantConfig fetchConfig
|
|
wantErr func(error) bool
|
|
}{
|
|
{
|
|
name: "no fetch option provided",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
struct{}{},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "header set as string",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Headers map[string]string
|
|
}{
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer token",
|
|
},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Headers: http.Header{
|
|
"Authorization": {"Bearer token"},
|
|
},
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "header set as list",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Headers map[string][]any
|
|
}{
|
|
Headers: map[string][]any{
|
|
"Authorization": {"Bearer token"},
|
|
},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Headers: http.Header{
|
|
"Authorization": {"Bearer token"},
|
|
},
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "method set",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Method string
|
|
}{
|
|
Method: http.MethodPost,
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Method: http.MethodPost,
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "body set",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Body struct{ Id string }
|
|
}{
|
|
Body: struct{ Id string }{
|
|
Id: "asdf123",
|
|
},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Body: bytes.NewReader([]byte(`{"id":"asdf123"}`)),
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "invalid header",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
NotExists struct{}
|
|
}{
|
|
NotExists: struct{}{},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{},
|
|
wantErr: func(err error) bool {
|
|
return zerrors.IsErrorInvalidArgument(err)
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &HTTP{
|
|
runtime: runtime,
|
|
client: http.DefaultClient,
|
|
}
|
|
gotConfig := new(fetchConfig)
|
|
|
|
err := c.fetchConfigFromArg(tt.args.arg, gotConfig)
|
|
if !tt.wantErr(err) {
|
|
t.Errorf("HTTP.fetchConfigFromArg() error = %v", err)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(gotConfig.Headers, tt.wantConfig.Headers) {
|
|
t.Errorf("config.Headers got = %#v, want %#v", gotConfig.Headers, tt.wantConfig.Headers)
|
|
}
|
|
if gotConfig.Method != tt.wantConfig.Method {
|
|
t.Errorf("config.Method got = %#v, want %#v", gotConfig.Method, tt.wantConfig.Method)
|
|
}
|
|
|
|
if tt.wantConfig.Body == nil {
|
|
if gotConfig.Body != nil {
|
|
t.Errorf("didn't expect a body")
|
|
}
|
|
return
|
|
}
|
|
|
|
gotBody, _ := io.ReadAll(gotConfig.Body)
|
|
wantBody, _ := io.ReadAll(tt.wantConfig.Body)
|
|
|
|
if !reflect.DeepEqual(gotBody, wantBody) {
|
|
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTP_buildHTTPRequest(t *testing.T) {
|
|
runtime := goja.New()
|
|
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
|
|
|
type args struct {
|
|
args []goja.Value
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantReq *http.Request
|
|
shouldPanic bool
|
|
}{
|
|
{
|
|
name: "only url",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: defaultFetchConfig.Headers,
|
|
Body: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "no params",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(&struct{}{}),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: defaultFetchConfig.Headers,
|
|
Body: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "overwrite headers",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Headers map[string][]interface{}
|
|
}{
|
|
Headers: map[string][]interface{}{"Authorization": {"some token"}},
|
|
}),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: http.Header{
|
|
"Authorization": []string{"some token"},
|
|
},
|
|
Body: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "post with body",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Body struct{ MyData string }
|
|
}{
|
|
Body: struct{ MyData string }{MyData: "hello world"},
|
|
}),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: defaultFetchConfig.Headers,
|
|
Body: io.NopCloser(bytes.NewReader([]byte(`{"myData":"hello world"}`))),
|
|
},
|
|
},
|
|
{
|
|
name: "too many args",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue("http://my-url.ch"),
|
|
},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
{
|
|
name: "no args",
|
|
args: args{
|
|
args: []goja.Value{},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
{
|
|
name: "invalid config",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Invalid bool
|
|
}{
|
|
Invalid: true,
|
|
}),
|
|
},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
{
|
|
name: "invalid method",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Method string
|
|
}{
|
|
Method: " asdf asdf",
|
|
}),
|
|
},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
panicked := false
|
|
if tt.shouldPanic {
|
|
defer func() {
|
|
if panicked != tt.shouldPanic {
|
|
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
|
|
}
|
|
}()
|
|
defer func() {
|
|
recover()
|
|
panicked = true
|
|
}()
|
|
}
|
|
|
|
c := &HTTP{
|
|
runtime: runtime,
|
|
}
|
|
|
|
gotReq := c.buildHTTPRequest(context.Background(), tt.args.args)
|
|
|
|
if tt.shouldPanic {
|
|
return
|
|
}
|
|
|
|
if gotReq.URL.String() != tt.wantReq.URL.String() {
|
|
t.Errorf("url = %s, want %s", gotReq.URL, tt.wantReq.URL)
|
|
}
|
|
|
|
if !reflect.DeepEqual(gotReq.Header, tt.wantReq.Header) {
|
|
t.Errorf("headers = %v, want %v", gotReq.Header, tt.wantReq.Header)
|
|
}
|
|
|
|
if gotReq.Method != tt.wantReq.Method {
|
|
t.Errorf("method = %s, want %s", gotReq.Method, tt.wantReq.Method)
|
|
}
|
|
|
|
if tt.wantReq.Body == nil {
|
|
if gotReq.Body != nil {
|
|
t.Errorf("didn't expect a body")
|
|
}
|
|
return
|
|
}
|
|
|
|
gotBody, _ := io.ReadAll(gotReq.Body)
|
|
wantBody, _ := io.ReadAll(tt.wantReq.Body)
|
|
|
|
if !reflect.DeepEqual(gotBody, wantBody) {
|
|
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|