diff --git a/.task/checksum/generate b/.task/checksum/generate
index e9c2351..543160d 100644
--- a/.task/checksum/generate
+++ b/.task/checksum/generate
@@ -1 +1 @@
-8d0ebfae489db7ae534fee52ed6ceb3f
+c2a78909343b26a169423b19ada40fad
diff --git a/cmd/background/main.go b/cmd/background/main.go
index f9cc98e..ba65c8f 100644
--- a/cmd/background/main.go
+++ b/cmd/background/main.go
@@ -29,7 +29,13 @@ func main() {
}
}
+const savingOrganizationIDInternalInsteadOfExternal = false
+
func app(ctx context.Context) error {
+ if !savingOrganizationIDInternalInsteadOfExternal {
+ panic("fix saving ogranization id as external id instead of internal")
+ }
+
var cfgpath string
if len(os.Args) > 1 {
cfgpath = os.Args[1]
diff --git a/cmd/kuriweb/http.go b/cmd/kuriweb/http.go
index faf2f90..9182c38 100644
--- a/cmd/kuriweb/http.go
+++ b/cmd/kuriweb/http.go
@@ -107,11 +107,21 @@ func middlewareCustomWriterInjector() mux.MiddlewareFunc {
}
}
+type attributeStringKey string
+
+func (k attributeStringKey) Value(value string) attribute.KeyValue {
+ return attribute.String(string(k), value)
+}
+
func middlewareTrace() mux.MiddlewareFunc {
- reqidAttr := attribute.Key("http.request_id")
- statusAttr := attribute.Key("http.status_code")
- payloadAttr := attribute.Key("http.payload_size")
- pathAttr := attribute.Key("http.template_path")
+ methodAttr := attributeStringKey("http.request.method")
+ reqidAttr := attributeStringKey("http.request_id")
+ routeAttr := attributeStringKey("http.route")
+ queryAttr := attributeStringKey("url.query")
+ pathAttr := attributeStringKey("http.path")
+ uaAttr := attributeStringKey("user_agent.original")
+ statusAttr := attribute.Key("http.response.status_code")
+ payloadAttr := attribute.Key("http.response_content_length")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -120,14 +130,18 @@ func middlewareTrace() mux.MiddlewareFunc {
var span trace.Span
route := mux.CurrentRoute(r)
- hname := route.GetName()
hpath, _ := route.GetPathTemplate()
ctx, span = webtracer.Start(
- ctx, "http."+hname,
+ ctx, r.Method+" "+hpath,
trace.WithAttributes(
- reqidAttr.String(reqid),
- pathAttr.String(hpath),
+ methodAttr.Value(r.Method),
+ reqidAttr.Value(reqid),
+ routeAttr.Value(hpath),
+ pathAttr.Value(r.URL.Path),
+ queryAttr.Value(r.URL.RawQuery),
+ uaAttr.Value(r.UserAgent()),
),
+ trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
diff --git a/cmd/kuriweb/trace.go b/cmd/kuriweb/trace.go
index b9202de..7e1fde3 100644
--- a/cmd/kuriweb/trace.go
+++ b/cmd/kuriweb/trace.go
@@ -8,18 +8,19 @@ import (
"time"
"git.loyso.art/frx/kurious/internal/common/config"
- "git.loyso.art/frx/kurious/pkg/xdefault"
"github.com/gorilla/mux"
- "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
+ semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
var webtracer = otel.Tracer("kuriweb.http")
@@ -46,19 +47,26 @@ func setupOtelSDK(ctx context.Context, cfg config.Trace) (shutdown shutdownFunc,
prop := newPropagator()
otel.SetTextMapPropagator(prop)
- tracerProvider, err := newTraceProvider(ctx, cfg.Endpoint, cfg.LicenseKey)
+ tracerProvider, err := newCommonTraceProvider(ctx, TraceProviderParams{
+ Endpoint: cfg.Endpoint,
+ Type: cfg.Type,
+ AuthHeader: cfg.APIHeader,
+ APIKey: cfg.APIKey,
+ })
if err != nil {
return nil, handleError(err)
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
- meterProvider, err := newMeterProvider()
- if err != nil {
- return nil, handleError(err)
+ if cfg.ShowMetrics {
+ meterProvider, err := newMeterProvider()
+ if err != nil {
+ return nil, handleError(err)
+ }
+ shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
+ otel.SetMeterProvider(meterProvider)
}
- shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
- otel.SetMeterProvider(meterProvider)
return shutdown, nil
}
@@ -70,46 +78,75 @@ func newPropagator() propagation.TextMapPropagator {
)
}
-const defaultNewRelicEndpoint = "otlp.eu01.nr-data.net:443"
+type TraceProviderParams struct {
+ Type config.TraceClientType
+ Endpoint string
+ APIKey string
+ AuthHeader string
+}
-func newTraceProvider(ctx context.Context, endpoint, licensekey string) (traceProvider *trace.TracerProvider, err error) {
- opts := make([]trace.TracerProviderOption, 0, 2)
+func newCommonTraceProvider(ctx context.Context, params TraceProviderParams) (tp *trace.TracerProvider, err error) {
+ r, err := resource.New(
+ ctx,
+ resource.WithDetectors(
+ resource.StringDetector(semconv.SchemaURL, semconv.ServiceNameKey, func() (string, error) {
+ return "bigstats:kuriweb", nil
+ }),
+ ),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("making new resource: %w", err)
+ }
+
+ r, err = resource.Merge(resource.Default(), r)
+ if err != nil {
+ return nil, fmt.Errorf("merging resources: %w", err)
+ }
+
+ opts := make([]trace.TracerProviderOption, 0, 4)
opts = append(
opts,
trace.WithSampler(trace.AlwaysSample()),
- trace.WithResource(resource.Default()),
+ trace.WithResource(r),
)
- if licensekey != "" {
- endpoint = xdefault.WithFallback(endpoint, defaultNewRelicEndpoint)
- client, err := otlptracegrpc.New(
- ctx,
- otlptracegrpc.WithEndpoint(endpoint),
- otlptracegrpc.WithHeaders(map[string]string{
- "api-key": licensekey,
- }),
- )
- if err != nil {
- return nil, fmt.Errorf("making grpc client: %w", err)
+ if params.Type != config.TraceClientTypeUnset {
+ var spanExporter trace.SpanExporter
+ var headers map[string]string
+
+ if params.AuthHeader != "" {
+ headers = make(map[string]string, 1)
+ headers[params.AuthHeader] = params.APIKey
}
- opts = append(opts, trace.WithBatcher(client, trace.WithBatchTimeout(time.Second*10)))
- } else {
- traceExporter, err := stdouttrace.New(
- stdouttrace.WithPrettyPrint())
+ switch params.Type {
+ case config.TraceClientTypeGRPC:
+ spanExporter, err = otlptracegrpc.New(
+ ctx,
+ otlptracegrpc.WithEndpointURL(params.Endpoint),
+ otlptracegrpc.WithHeaders(headers),
+ )
+ case config.TraceClientTypeHTTP:
+ httpClient := otlptracehttp.NewClient(
+ otlptracehttp.WithEndpointURL(params.Endpoint),
+ otlptracehttp.WithHeaders(headers),
+ )
+ spanExporter, err = otlptrace.New(ctx, httpClient)
+ case config.TraceClientTypeStdout:
+ spanExporter, err = stdouttrace.New(stdouttrace.WithPrettyPrint())
+ default:
+ return nil, fmt.Errorf("unsupported provider type")
+ }
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("making trace exporter: %w", err)
}
- opts = append(
- opts,
- trace.WithBatcher(traceExporter, trace.WithBatchTimeout(time.Second*5)),
- )
+ opts = append(opts, trace.WithBatcher(spanExporter))
}
- traceProvider = trace.NewTracerProvider(opts...)
+ tp = trace.NewTracerProvider(opts...)
- return traceProvider, nil
+ return tp, nil
}
func newMeterProvider() (*metric.MeterProvider, error) {
@@ -127,6 +164,6 @@ func newMeterProvider() (*metric.MeterProvider, error) {
}
func muxHandleFunc(router *mux.Router, name, path string, hf http.HandlerFunc) *mux.Route {
- h := otelhttp.WithRouteTag(path, hf)
- return router.Handle(path, h).Name(name)
+ // h := otelhttp.WithRouteTag(path, hf)
+ return router.Handle(path, hf).Name(name)
}
diff --git a/go.mod b/go.mod
index f650aa3..6ea7f8a 100644
--- a/go.mod
+++ b/go.mod
@@ -12,15 +12,16 @@ require (
github.com/teris-io/cli v1.0.1
github.com/ydb-platform/ydb-go-sdk/v3 v3.54.2
github.com/ydb-platform/ydb-go-yc v0.12.1
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
- go.opentelemetry.io/otel v1.24.0
+ go.opentelemetry.io/otel v1.25.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0
- go.opentelemetry.io/otel/sdk v1.24.0
+ go.opentelemetry.io/otel/sdk v1.25.0
go.opentelemetry.io/otel/sdk/metric v1.24.0
- go.opentelemetry.io/otel/trace v1.24.0
- golang.org/x/net v0.22.0
+ go.opentelemetry.io/otel/trace v1.25.0
+ golang.org/x/net v0.24.0
golang.org/x/sync v0.6.0
golang.org/x/time v0.5.0
modernc.org/sqlite v1.29.3
@@ -30,11 +31,9 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
- github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
- github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -47,14 +46,13 @@ require (
github.com/yandex-cloud/go-genproto v0.0.0-20231120081503-a21e9fe75162 // indirect
github.com/ydb-platform/ydb-go-genproto v0.0.0-20231012155159-f85a672542fd // indirect
github.com/ydb-platform/ydb-go-yc-metadata v0.6.1 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
- go.opentelemetry.io/otel/metric v1.24.0 // indirect
- go.opentelemetry.io/proto/otlp v1.1.0 // indirect
- golang.org/x/sys v0.18.0 // indirect
+ go.opentelemetry.io/otel/metric v1.25.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.2.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
- google.golang.org/grpc v1.62.1 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
+ google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
diff --git a/go.sum b/go.sum
index 173981b..f30c88e 100644
--- a/go.sum
+++ b/go.sum
@@ -575,8 +575,6 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -641,8 +639,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
-github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -846,31 +842,31 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
-go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
-go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
+go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
+go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0 h1:JYE2HM7pZbOt5Jhk8ndWZTUWYOVift2cHjXVMkPdmdc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0/go.mod h1:yMb/8c6hVsnma0RpsBMNo0fEiQKeclawtgaIaOp2MLY=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
-go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
-go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
-go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
-go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
+go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
+go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
+go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
+go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0=
-go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
-go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
+go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
-go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
-go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
+go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
+go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -1000,8 +996,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1126,8 +1122,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -1430,10 +1426,10 @@ google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA=
google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
-google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
-google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU=
+google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1474,8 +1470,8 @@ google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsA
google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
-google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
-google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
+google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
diff --git a/htmlexamples/courses.html b/htmlexamples/courses.html
index ae1dfdb..258b6f4 100644
--- a/htmlexamples/courses.html
+++ b/htmlexamples/courses.html
@@ -1,142 +1,277 @@
+
+ Test page
+
+
+
+
+
+
-
- Test page
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
- Languages
- A languages category provides all courses to help learn language
+
+
+
+
+
+
+ Filter categories
+
+
+
+
+
+
+
+
-
-