feat: otel telemetry collection and dashboard (#6796)

Co-authored-by: Kamil Kisiela <kamil.kisiela@gmail.com>
Co-authored-by: Dotan Simha <dotansimha@gmail.com>
Co-authored-by: Denis Badurina <denis@denelop.com>
This commit is contained in:
Laurin Quast 2025-10-10 14:06:02 +02:00 committed by GitHub
parent 03f7066449
commit 4f70fc9555
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 13335 additions and 243 deletions

17
configs/gateway.config.ts Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig } from '@graphql-hive/gateway';
import { hiveTracingSetup } from '@graphql-hive/plugin-opentelemetry/setup';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; // install
hiveTracingSetup({
contextManager: new AsyncLocalStorageContextManager(),
target: process.env.HIVE_TRACING_TARGET!,
accessToken: process.env.HIVE_TRACING_ACCESS_TOKEN!,
// optional, for self-hosting
endpoint: process.env.HIVE_TRACING_ENDPOINT!,
});
export const gatewayConfig = defineConfig({
openTelemetry: {
traces: true,
},
});

View file

@ -14,6 +14,7 @@ import { configureGithubApp } from './services/github';
import { deployGraphQL } from './services/graphql'; import { deployGraphQL } from './services/graphql';
import { deployKafka } from './services/kafka'; import { deployKafka } from './services/kafka';
import { deployObservability } from './services/observability'; import { deployObservability } from './services/observability';
import { deployOTELCollector } from './services/otel-collector';
import { deploySchemaPolicy } from './services/policy'; import { deploySchemaPolicy } from './services/policy';
import { deployPostgres } from './services/postgres'; import { deployPostgres } from './services/postgres';
import { deployProxy } from './services/proxy'; import { deployProxy } from './services/proxy';
@ -278,6 +279,15 @@ if (hiveAppPersistedDocumentsAbsolutePath && RUN_PUBLISH_COMMANDS) {
}); });
} }
const otelCollector = deployOTELCollector({
environment,
graphql,
dbMigrations,
clickhouse,
image: docker.factory.getImageId('otel-collector', imagesTag),
docker,
});
const app = deployApp({ const app = deployApp({
environment, environment,
graphql, graphql,
@ -306,6 +316,7 @@ const proxy = deployProxy({
usage, usage,
environment, environment,
publicGraphQLAPIGateway, publicGraphQLAPIGateway,
otelCollector,
}); });
deployCloudFlareSecurityTransform({ deployCloudFlareSecurityTransform({
@ -332,4 +343,5 @@ export const schemaApiServiceId = schema.service.id;
export const webhooksApiServiceId = webhooks.service.id; export const webhooksApiServiceId = webhooks.service.id;
export const appId = app.deployment.id; export const appId = app.deployment.id;
export const otelCollectorId = otelCollector.deployment.id;
export const publicIp = proxy.get()!.status.loadBalancer.ingress[0].ip; export const publicIp = proxy.get()!.status.loadBalancer.ingress[0].ip;

View file

@ -79,6 +79,11 @@ export function prepareEnvironment(input: {
cpuLimit: isProduction ? '512m' : '150m', cpuLimit: isProduction ? '512m' : '150m',
memoryLimit: isProduction ? '1000Mi' : '300Mi', memoryLimit: isProduction ? '1000Mi' : '300Mi',
}, },
tracingCollector: {
cpuLimit: isProduction ? '1000m' : '100m',
memoryLimit: isProduction ? '2000Mi' : '200Mi',
maxReplicas: isProduction || isStaging ? 3 : 1,
},
}, },
}; };
} }

View file

@ -0,0 +1,60 @@
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceDeployment } from '../utils/service-deployment';
import { Clickhouse } from './clickhouse';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Environment } from './environment';
import { GraphQL } from './graphql';
export type OTELCollector = ReturnType<typeof deployOTELCollector>;
export function deployOTELCollector(args: {
image: string;
environment: Environment;
docker: Docker;
clickhouse: Clickhouse;
dbMigrations: DbMigrations;
graphql: GraphQL;
}) {
return new ServiceDeployment(
'otel-collector',
{
image: args.image,
imagePullSecret: args.docker.secret,
env: {
...args.environment.envVars,
HIVE_OTEL_AUTH_ENDPOINT: serviceLocalEndpoint(args.graphql.service).apply(
value => value + '/otel-auth',
),
},
/**
* We are using the healthcheck extension.
* https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/healthcheckextension
*/
probePort: 13133,
readinessProbe: '/',
livenessProbe: '/',
startupProbe: '/',
exposesMetrics: true,
replicas: args.environment.podsConfig.tracingCollector.maxReplicas,
pdb: true,
availabilityOnEveryNode: true,
port: 4318,
memoryLimit: args.environment.podsConfig.tracingCollector.memoryLimit,
autoScaling: {
maxReplicas: args.environment.podsConfig.tracingCollector.maxReplicas,
cpu: {
limit: args.environment.podsConfig.tracingCollector.cpuLimit,
cpuAverageToScale: 80,
},
},
},
[args.clickhouse.deployment, args.clickhouse.service, args.dbMigrations],
)
.withSecret('CLICKHOUSE_HOST', args.clickhouse.secret, 'host')
.withSecret('CLICKHOUSE_PORT', args.clickhouse.secret, 'port')
.withSecret('CLICKHOUSE_USERNAME', args.clickhouse.secret, 'username')
.withSecret('CLICKHOUSE_PASSWORD', args.clickhouse.secret, 'password')
.withSecret('CLICKHOUSE_PROTOCOL', args.clickhouse.secret, 'protocol')
.deploy();
}

View file

@ -5,6 +5,7 @@ import { App } from './app';
import { Environment } from './environment'; import { Environment } from './environment';
import { GraphQL } from './graphql'; import { GraphQL } from './graphql';
import { Observability } from './observability'; import { Observability } from './observability';
import { OTELCollector } from './otel-collector';
import { type PublicGraphQLAPIGateway } from './public-graphql-api-gateway'; import { type PublicGraphQLAPIGateway } from './public-graphql-api-gateway';
import { Usage } from './usage'; import { Usage } from './usage';
@ -15,6 +16,7 @@ export function deployProxy({
environment, environment,
observability, observability,
publicGraphQLAPIGateway, publicGraphQLAPIGateway,
otelCollector,
}: { }: {
observability: Observability; observability: Observability;
environment: Environment; environment: Environment;
@ -22,6 +24,7 @@ export function deployProxy({
app: App; app: App;
usage: Usage; usage: Usage;
publicGraphQLAPIGateway: PublicGraphQLAPIGateway; publicGraphQLAPIGateway: PublicGraphQLAPIGateway;
otelCollector: OTELCollector;
}) { }) {
const { tlsIssueName } = new CertManager().deployCertManagerAndIssuer(); const { tlsIssueName } = new CertManager().deployCertManagerAndIssuer();
const commonConfig = new pulumi.Config('common'); const commonConfig = new pulumi.Config('common');
@ -113,5 +116,13 @@ export function deployProxy({
requestTimeout: '60s', requestTimeout: '60s',
retriable: true, retriable: true,
}, },
{
name: 'otel-traces',
path: '/otel/v1/traces',
customRewrite: '/v1/traces',
service: otelCollector.service,
requestTimeout: '60s',
retriable: true,
},
]); ]);
} }

View file

@ -40,6 +40,8 @@ export class ServiceDeployment {
args?: kx.types.Container['args']; args?: kx.types.Container['args'];
image: string; image: string;
port?: number; port?: number;
/** Port to use for liveness, startup and readiness probes. */
probePort?: number;
serviceAccountName?: pulumi.Output<string>; serviceAccountName?: pulumi.Output<string>;
livenessProbe?: string | ProbeConfig; livenessProbe?: string | ProbeConfig;
readinessProbe?: string | ProbeConfig; readinessProbe?: string | ProbeConfig;
@ -107,6 +109,7 @@ export class ServiceDeployment {
createPod(asJob: boolean) { createPod(asJob: boolean) {
const port = this.options.port || 3000; const port = this.options.port || 3000;
const probePort = this.options.probePort ?? port;
const additionalEnv: any[] = normalizeEnv(this.options.env); const additionalEnv: any[] = normalizeEnv(this.options.env);
const secretsEnv: any[] = normalizeEnvSecrets(this.envSecrets); const secretsEnv: any[] = normalizeEnvSecrets(this.envSecrets);
@ -125,14 +128,14 @@ export class ServiceDeployment {
timeoutSeconds: 5, timeoutSeconds: 5,
httpGet: { httpGet: {
path: this.options.livenessProbe, path: this.options.livenessProbe,
port, port: probePort,
}, },
} }
: { : {
...this.options.livenessProbe, ...this.options.livenessProbe,
httpGet: { httpGet: {
path: this.options.livenessProbe.endpoint, path: this.options.livenessProbe.endpoint,
port, port: probePort,
}, },
}; };
} }
@ -147,14 +150,14 @@ export class ServiceDeployment {
timeoutSeconds: 5, timeoutSeconds: 5,
httpGet: { httpGet: {
path: this.options.readinessProbe, path: this.options.readinessProbe,
port, port: probePort,
}, },
} }
: { : {
...this.options.readinessProbe, ...this.options.readinessProbe,
httpGet: { httpGet: {
path: this.options.readinessProbe.endpoint, path: this.options.readinessProbe.endpoint,
port, port: probePort,
}, },
}; };
} }
@ -169,14 +172,14 @@ export class ServiceDeployment {
timeoutSeconds: 10, timeoutSeconds: 10,
httpGet: { httpGet: {
path: this.options.startupProbe, path: this.options.startupProbe,
port, port: probePort,
}, },
} }
: { : {
...this.options.startupProbe, ...this.options.startupProbe,
httpGet: { httpGet: {
path: this.options.startupProbe.endpoint, path: this.options.startupProbe.endpoint,
port, port: probePort,
}, },
}; };
} }

View file

@ -0,0 +1,30 @@
dist:
version: 0.122.0
name: otelcol-custom
description: Custom OTel Collector distribution
output_path: ./otelcol-custom
receivers:
- gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.122.0
processors:
- gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.122.0
- gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.122.0
- gomod:
github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor
v0.122.0
- gomod:
github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.122.0
exporters:
- gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.122.0
- gomod:
github.com/open-telemetry/opentelemetry-collector-contrib/exporter/clickhouseexporter v0.122.0
extensions:
- gomod:
github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckextension
v0.122.0
- gomod: github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth v0.0.0
path: ./extension-hiveauth
name: hiveauthextension # when using local extensions, package name is required, otherwise you get "missing import path"

View file

@ -0,0 +1,79 @@
extensions:
hiveauth:
endpoint: ${HIVE_OTEL_AUTH_ENDPOINT}
health_check:
endpoint: '0.0.0.0:13133'
receivers:
otlp:
protocols:
grpc:
include_metadata: true
endpoint: '0.0.0.0:4317'
auth:
authenticator: hiveauth
http:
cors:
allowed_origins: ['*']
allowed_headers: ['*']
include_metadata: true
endpoint: '0.0.0.0:4318'
auth:
authenticator: hiveauth
processors:
batch:
timeout: 5s
send_batch_size: 10000
attributes:
actions:
- key: hive.target_id
from_context: auth.targetId
action: insert
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
exporters:
debug:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
clickhouse:
endpoint: ${CLICKHOUSE_PROTOCOL}://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}?dial_timeout=10s&compress=lz4&async_insert=1
database: default
async_insert: true
username: ${CLICKHOUSE_USERNAME}
password: ${CLICKHOUSE_PASSWORD}
create_schema: false
ttl: 720h
compress: lz4
logs_table_name: otel_logs
traces_table_name: otel_traces
metrics_table_name: otel_metrics
timeout: 5s
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
service:
extensions:
- hiveauth
- health_check
telemetry:
logs:
level: INFO
encoding: json
output_paths: ['stdout']
error_output_paths: ['stderr']
metrics:
address: '0.0.0.0:10254'
pipelines:
traces:
receivers: [otlp]
processors:
- memory_limiter
- attributes
- batch
exporters:
- clickhouse
# - debug

View file

@ -0,0 +1,28 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package hiveauthextension // import "github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth"
import (
"errors"
"time"
)
type Config struct {
// Endpoint is the address of the authentication server
Endpoint string `mapstructure:"endpoint"`
// Timeout is the timeout for the HTTP request to the auth service
Timeout time.Duration `mapstructure:"timeout"`
}
func (cfg *Config) Validate() error {
if cfg.Endpoint == "" {
return errors.New("missing endpoint")
}
if cfg.Timeout <= 0 {
return errors.New("timeout must be a positive value")
}
return nil
}

View file

@ -0,0 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
//go:generate mdatagen metadata.yaml
// Package hiveauthextension accepts HTTP requests and forwards them to an external authentication service.
package hiveauthextension // import "github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth"

View file

@ -0,0 +1,257 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package hiveauthextension
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/patrickmn/go-cache"
"go.opentelemetry.io/collector/client"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/extension/extensionauth"
"go.uber.org/zap"
"golang.org/x/sync/singleflight"
)
var _ extensionauth.Server = (*hiveAuthExtension)(nil)
var _ client.AuthData = (*authData)(nil)
type authData struct {
targetId string
}
func (a *authData) GetAttribute(name string) any {
switch name {
case "targetId":
return a.targetId
default:
return nil
}
}
func (*authData) GetAttributeNames() []string {
return []string{"targetId"}
}
type hiveAuthExtension struct {
logger *zap.Logger
config *Config
client *http.Client
group singleflight.Group
cache *cache.Cache
}
func (h *hiveAuthExtension) Start(_ context.Context, _ component.Host) error {
h.logger.Info("Starting hive auth extension", zap.String("endpoint", h.config.Endpoint), zap.Duration("timeout", h.config.Timeout))
return nil
}
func (h *hiveAuthExtension) Shutdown(_ context.Context) error {
h.logger.Info("Shutting down hive auth extension")
return nil
}
type AuthStatusError struct {
Code int
Msg string
}
func (e *AuthStatusError) Error() string {
return fmt.Sprintf("authentication failed: status %d, %s", e.Code, e.Msg)
}
func getHeader(h map[string][]string, headerKey string, metadataKey string) string {
headerValues, ok := h[headerKey]
if !ok {
headerValues, ok = h[metadataKey]
}
if !ok {
for k, v := range h {
if strings.EqualFold(k, metadataKey) {
headerValues = v
break
}
}
}
if len(headerValues) == 0 {
return ""
}
return headerValues[0]
}
func getAuthHeader(h map[string][]string) string {
const (
canonicalHeaderKey = "Authorization"
metadataKey = "authorization"
)
return getHeader(h, canonicalHeaderKey, metadataKey)
}
func getTargetRefHeader(h map[string][]string) string {
const (
canonicalHeaderKey = "X-Hive-Target-Ref"
metadataKey = "x-hive-target-ref"
)
return getHeader(h, canonicalHeaderKey, metadataKey)
}
type authResult struct {
err error
targetId string
}
func (h *hiveAuthExtension) doAuthRequest(ctx context.Context, auth string, targetRef string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.config.Endpoint, nil)
if err != nil {
h.logger.Error("failed to create auth request", zap.Error(err))
return "", err
}
req.Header.Set("Authorization", auth)
req.Header.Set("X-Hive-Target-Ref", targetRef)
// Retry parameters.
const maxRetries = 3
const retryDelay = 100 * time.Millisecond
var lastStatus int
for attempt := 0; attempt < maxRetries; attempt++ {
resp, err := h.client.Do(req)
if err != nil {
h.logger.Error("error calling authentication service", zap.Error(err))
return "", err
}
lastStatus = resp.StatusCode
// Success.
if resp.StatusCode == http.StatusOK {
var result struct {
TargetId string `json:"targetId"`
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return "", err
}
if err := json.Unmarshal(body, &result); err != nil {
return "", err
}
h.logger.Debug("authentication succeeded", zap.String("targetId", result.TargetId))
return result.TargetId, nil
}
// For 5XX responses, retry.
if resp.StatusCode >= 500 && resp.StatusCode < 600 {
h.logger.Warn("received 5xx response, retrying",
zap.Int("attempt", attempt+1),
zap.String("status", resp.Status))
resp.Body.Close()
select {
case <-time.After(retryDelay * time.Duration(attempt + 1)):
// Continue to next attempt.
case <-ctx.Done():
return "", ctx.Err()
}
continue
}
// For non-retryable errors.
errMsg := fmt.Sprintf("authentication failed: received status %s", resp.Status)
h.logger.Warn(errMsg)
resp.Body.Close()
return "", &AuthStatusError{
Code: resp.StatusCode,
Msg: "non-retryable error",
}
}
return "", &AuthStatusError{
Code: lastStatus,
Msg: "authentication failed after retries",
}
}
func (h *hiveAuthExtension) Authenticate(ctx context.Context, headers map[string][]string) (context.Context, error) {
auth := getAuthHeader(headers)
targetRef := getTargetRefHeader(headers)
if auth == "" {
return ctx, errors.New("No auth provided")
}
if targetRef == "" {
return ctx, errors.New("No target ref provided")
}
cacheKey := fmt.Sprintf("%s|%s", auth, targetRef)
if cached, found := h.cache.Get(cacheKey); found {
res := cached.(authResult)
if res.err == nil {
cl := client.FromContext(ctx)
cl.Auth = &authData{targetId: res.targetId}
return client.NewContext(ctx, cl), nil
}
return ctx, res.err
}
// Deduplicate concurrent calls.
targetId, err, _ := h.group.Do(cacheKey, func() (any, error) {
return h.doAuthRequest(ctx, auth, targetRef)
})
var ttl time.Duration
if err == nil {
ttl = 30 * time.Second
} else {
ttl = 10 * time.Second
}
h.cache.Set(cacheKey, authResult{err: err, targetId: targetId.(string)}, ttl)
if err == nil {
cl := client.FromContext(ctx)
cl.Auth = &authData{targetId: targetId.(string)}
return client.NewContext(ctx, cl), nil
}
return ctx, err
}
func newHiveAuthExtension(
logger *zap.Logger,
cfg component.Config,
) (extensionauth.Server, error) {
c, ok := cfg.(*Config)
if !ok {
return nil, errors.New("invalid configuration")
}
if err := c.Validate(); err != nil {
return nil, err
}
return &hiveAuthExtension{
logger: logger,
config: c,
client: &http.Client{
Timeout: c.Timeout,
},
cache: cache.New(30*time.Second, time.Minute),
}, nil
}

View file

@ -0,0 +1,35 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package hiveauthextension // import "github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth"
import (
"context"
"time"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/extension"
"github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth/internal/metadata"
)
// NewFactory creates a factory for the static bearer token Authenticator extension.
func NewFactory() extension.Factory {
return extension.NewFactory(
metadata.Type,
createDefaultConfig,
createExtension,
metadata.ExtensionStability,
)
}
func createDefaultConfig() component.Config {
return &Config{
Endpoint: "http://localhost:3000/",
Timeout: 5 * time.Second,
}
}
func createExtension(_ context.Context, params extension.Settings, cfg component.Config) (extension.Extension, error) {
return newHiveAuthExtension(params.Logger, cfg.(*Config))
}

View file

@ -0,0 +1,52 @@
// Code generated by mdatagen. DO NOT EDIT.
package hiveauthextension
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/confmap/confmaptest"
"go.opentelemetry.io/collector/extension/extensiontest"
)
var typ = component.MustNewType("hiveauth")
func TestComponentFactoryType(t *testing.T) {
require.Equal(t, typ, NewFactory().Type())
}
func TestComponentConfigStruct(t *testing.T) {
require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig()))
}
func TestComponentLifecycle(t *testing.T) {
factory := NewFactory()
cm, err := confmaptest.LoadConf("metadata.yaml")
require.NoError(t, err)
cfg := factory.CreateDefaultConfig()
sub, err := cm.Sub("tests::config")
require.NoError(t, err)
require.NoError(t, sub.Unmarshal(&cfg))
t.Run("shutdown", func(t *testing.T) {
e, err := factory.Create(context.Background(), extensiontest.NewNopSettings(typ), cfg)
require.NoError(t, err)
err = e.Shutdown(context.Background())
require.NoError(t, err)
})
t.Run("lifecycle", func(t *testing.T) {
firstExt, err := factory.Create(context.Background(), extensiontest.NewNopSettings(typ), cfg)
require.NoError(t, err)
require.NoError(t, firstExt.Start(context.Background(), componenttest.NewNopHost()))
require.NoError(t, firstExt.Shutdown(context.Background()))
secondExt, err := factory.Create(context.Background(), extensiontest.NewNopSettings(typ), cfg)
require.NoError(t, err)
require.NoError(t, secondExt.Start(context.Background(), componenttest.NewNopHost()))
require.NoError(t, secondExt.Shutdown(context.Background()))
})
}

View file

@ -0,0 +1,13 @@
// Code generated by mdatagen. DO NOT EDIT.
package hiveauthextension
import (
"testing"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}

View file

@ -0,0 +1,52 @@
module github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth
go 1.23.0
require (
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.10.0
go.opentelemetry.io/collector/client v1.28.0
go.opentelemetry.io/collector/component v1.28.0
go.opentelemetry.io/collector/component/componenttest v0.122.0
go.opentelemetry.io/collector/confmap v1.28.0
go.opentelemetry.io/collector/extension v1.28.0
go.opentelemetry.io/collector/extension/extensionauth v0.122.0
go.opentelemetry.io/collector/extension/extensiontest v0.122.0
go.uber.org/goleak v1.3.0
go.uber.org/zap v1.27.0
golang.org/x/sync v0.12.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
github.com/knadh/koanf/v2 v2.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/featuregate v1.28.0 // indirect
go.opentelemetry.io/collector/pdata v1.28.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace github.com/open-telemetry/opentelemetry-collector-contrib/internal/common => ../../internal/common

View file

@ -0,0 +1,133 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/collector/client v1.28.0 h1:QKewiYc5Fc87pViqt8Lav/lQAybMUO4hf1ubV0gqRtg=
go.opentelemetry.io/collector/client v1.28.0/go.mod h1:mopBD0EZwShVUMjet1ElpzIvOlmVxpJ1r7b7XSr9npg=
go.opentelemetry.io/collector/component v1.28.0 h1:SQAGxxuyZ+d5tOsuEka8m9oE+wAroaYQpJ8NTIbl6Lk=
go.opentelemetry.io/collector/component v1.28.0/go.mod h1:te8gbcKU6Mgu7ewo/2VYDSbCkLrhOYYy2llayXCF0bI=
go.opentelemetry.io/collector/component/componenttest v0.122.0 h1:TxMm4nXB9iByQhDP0QFZwYxG+BFXEB6qUUwVh5YYW7g=
go.opentelemetry.io/collector/component/componenttest v0.122.0/go.mod h1:zzRftQeGgVPxKzXkJEx3ghC4U3hgiDRuuNljsq3cLPI=
go.opentelemetry.io/collector/confmap v1.28.0 h1:pUQh4eOW0YQ1GFWTDP5pw/ZMQuppkz6oSoDDloAH/Sc=
go.opentelemetry.io/collector/confmap v1.28.0/go.mod h1:k/3fo+2RE6m+OKlJzx78Q8hstABYwYgvXO3u9zyTeHI=
go.opentelemetry.io/collector/consumer v1.28.0 h1:3JzDm7EFAF9ws4O3vVou5n8egdGZtrRN3xVw6AjNtqE=
go.opentelemetry.io/collector/consumer v1.28.0/go.mod h1:Ge1HGm5aRYTW+SXggM+US9phb/BQR2of4FZ8r/3OH3Y=
go.opentelemetry.io/collector/extension v1.28.0 h1:E3j6/EtcahF2bX9DvRduLQ6tD7SuZdXM9DzAi7NSAeY=
go.opentelemetry.io/collector/extension v1.28.0/go.mod h1:3MW9IGCNNgjG/ngkALVH5epwbCwYuoZMTbh4523aYv0=
go.opentelemetry.io/collector/extension/extensionauth v0.122.0 h1:ypFO+JFrUsxWb2llD50Thyikrigzvac0cJeNh8nRT8M=
go.opentelemetry.io/collector/extension/extensionauth v0.122.0/go.mod h1:EsGPuDcbOxwku0ebMmtVOm+j8FCdH6yd7lQ6JT/1TXc=
go.opentelemetry.io/collector/extension/extensiontest v0.122.0 h1:daeCPXhb4HveyeYyX6G0IqjGuvWJprZeiq7pZiSdC+M=
go.opentelemetry.io/collector/extension/extensiontest v0.122.0/go.mod h1:JXSONLbyuX+uOy1gcQ3Jcp/48pfkh0RiZPy7XkyCBdU=
go.opentelemetry.io/collector/featuregate v1.28.0 h1:nkaMw0HyOSxojLwlezF2O/xJ9T/Jo1a0iEetesT9lr0=
go.opentelemetry.io/collector/featuregate v1.28.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc=
go.opentelemetry.io/collector/pdata v1.28.0 h1:xSZyvTOOc2Wmz4PoxrVqeQfodLgs9k7gowLAnzZN0eU=
go.opentelemetry.io/collector/pdata v1.28.0/go.mod h1:asKE8MD/4SOKz1mCrGdAz4VO2U2HUNg8A6094uK7pq0=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,16 @@
// Code generated by mdatagen. DO NOT EDIT.
package metadata
import (
"go.opentelemetry.io/collector/component"
)
var (
Type = component.MustNewType("hiveauth")
ScopeName = "github.com/graphql-hive/console/docker/configs/otel-collector/extension-hiveauth"
)
const (
ExtensionStability = component.StabilityLevelBeta
)

View file

@ -0,0 +1,12 @@
type: hiveauth
status:
class: extension
stability:
beta: [extension]
distributions: [contrib, k8s]
codeowners:
active: [kamilkisiela]
tests:
config:

View file

@ -422,3 +422,21 @@ services:
LOG_LEVEL: '${LOG_LEVEL:-debug}' LOG_LEVEL: '${LOG_LEVEL:-debug}'
SENTRY: '${SENTRY:-0}' SENTRY: '${SENTRY:-0}'
SENTRY_DSN: '${SENTRY_DSN:-}' SENTRY_DSN: '${SENTRY_DSN:-}'
otel-collector:
depends_on:
clickhouse:
condition: service_healthy
image: '${DOCKER_REGISTRY}otel-collector${DOCKER_TAG}'
environment:
HIVE_OTEL_AUTH_ENDPOINT: 'http://server:3001/otel-auth'
CLICKHOUSE_PROTOCOL: 'http'
CLICKHOUSE_HOST: clickhouse
CLICKHOUSE_PORT: '8123'
CLICKHOUSE_USERNAME: '${CLICKHOUSE_USER}'
CLICKHOUSE_PASSWORD: '${CLICKHOUSE_PASSWORD}'
ports:
- '4317:4317'
- '4318:4318'
networks:
- 'stack'

View file

@ -16,6 +16,7 @@ services:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_DB: registry POSTGRES_DB: registry
PGDATA: /var/lib/postgresql/data PGDATA: /var/lib/postgresql/data
HIVE_OTEL_AUTH_ENDPOINT: 'http://host.docker.internal:3001/otel-auth'
volumes: volumes:
- ./.hive-dev/postgresql/db:/var/lib/postgresql/data - ./.hive-dev/postgresql/db:/var/lib/postgresql/data
ports: ports:
@ -176,5 +177,28 @@ services:
networks: networks:
- 'stack' - 'stack'
otel-collector:
depends_on:
clickhouse:
condition: service_healthy
build:
context: ./configs/otel-collector
dockerfile: ./../../otel-collector.dockerfile
environment:
HIVE_OTEL_AUTH_ENDPOINT: 'http://host.docker.internal:3001/otel-auth'
CLICKHOUSE_PROTOCOL: 'http'
CLICKHOUSE_HOST: clickhouse
CLICKHOUSE_PORT: 8123
CLICKHOUSE_USERNAME: test
CLICKHOUSE_PASSWORD: test
volumes:
- ./configs/otel-collector/builder-config.yaml:/builder-config.yaml
- ./configs/otel-collector/config.yaml:/etc/otel-config.yaml
ports:
- '4317:4317'
- '4318:4318'
networks:
- 'stack'
networks: networks:
stack: {} stack: {}

View file

@ -82,6 +82,13 @@ target "router-base" {
} }
} }
target "otel-collector-base" {
dockerfile = "${PWD}/docker/otel-collector.dockerfile"
args = {
RELEASE = "${RELEASE}"
}
}
target "cli-base" { target "cli-base" {
dockerfile = "${PWD}/docker/cli.dockerfile" dockerfile = "${PWD}/docker/cli.dockerfile"
args = { args = {
@ -370,6 +377,21 @@ target "apollo-router" {
] ]
} }
target "otel-collector" {
inherits = ["otel-collector-base", get_target()]
context = "${PWD}/docker/configs/otel-collector"
args = {
IMAGE_TITLE = "graphql-hive/otel-collector"
IMAGE_DESCRIPTION = "OTEL Collector for GraphQL Hive."
}
tags = [
local_image_tag("otel-collector"),
stable_image_tag("otel-collector"),
image_tag("otel-collector", COMMIT_SHA),
image_tag("otel-collector", BRANCH_NAME)
]
}
target "cli" { target "cli" {
inherits = ["cli-base", get_target()] inherits = ["cli-base", get_target()]
context = "${PWD}/packages/libraries/cli" context = "${PWD}/packages/libraries/cli"
@ -400,7 +422,8 @@ group "build" {
"server", "server",
"commerce", "commerce",
"composition-federation-2", "composition-federation-2",
"app" "app",
"otel-collector"
] ]
} }
@ -416,7 +439,8 @@ group "integration-tests" {
"usage", "usage",
"webhooks", "webhooks",
"server", "server",
"composition-federation-2" "composition-federation-2",
"otel-collector"
] ]
} }

View file

@ -0,0 +1,34 @@
FROM scratch AS config
COPY builder-config.yaml .
COPY extension-hiveauth/ ./extension-hiveauth/
FROM golang:1.23.7-bookworm AS builder
ARG OTEL_VERSION=0.122.0
WORKDIR /build
RUN go install go.opentelemetry.io/collector/cmd/builder@v${OTEL_VERSION}
# Copy the manifest file and other necessary files
COPY --from=config builder-config.yaml .
COPY --from=config extension-hiveauth/ ./extension-hiveauth/
# Build the custom collector
RUN CGO_ENABLED=0 builder --config=/build/builder-config.yaml
# Stage 2: Final Image
FROM alpine:3.14
WORKDIR /app
# Copy the generated collector binary from the builder stage
COPY --from=builder /build/otelcol-custom .
COPY config.yaml /etc/otel-config.yaml
# Expose necessary ports
EXPOSE 4317/tcp 4318/tcp 13133/tcp
# Set the default command
CMD ["./otelcol-custom", "--config=/etc/otel-config.yaml"]

View file

@ -0,0 +1,27 @@
# OTEL Trace Stress Test
The purpose of this script is to see how our infrastructure responds to high loads and cardinality.
The affected components are:
- otel trace collector
- clickhouse (cloud)
## Running the script
The following environment variabels are required
```
OTEL_ENDPOINT
HIVE_ORGANIZATION_ACCESS_TOKEN
HIVE_TARGET_REF
```
**Example:**
```sh
OTEL_ENDPOINT=https://api.hiveready.dev/otel/v1/traces \
HIVE_ORGANIZATION_ACCESS_TOKEN="<REPLACE ME>" \
HIVE_TARGET_REF="the-guild/hive/dev" \
k6 run load-tests/otel-traces/test.ts
```

View file

@ -0,0 +1,7 @@
{
"name": "loade-test-otel-traces",
"private": true,
"devDependencies": {
"@types/k6": "1.2.0"
}
}

View file

@ -0,0 +1,259 @@
import './bru.ts';
import { expect } from 'https://jslib.k6.io/k6-testing/0.5.0/index.js';
import { randomIntBetween, randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import * as immer from 'https://unpkg.com/immer@10.1.3/dist/immer.mjs';
import { check } from 'k6';
import http from 'k6/http';
// prettier-ignore
globalThis.process = { env: {} };
// Cardinality Variables Start
const countUniqueErrorCodes = 1_000;
const countUniqueClients = 1_000;
const appVersionsPerClient = 1_000;
// Cardinality Variables End
//
export const options = {
scenarios: {
constant_rps: {
executor: 'constant-arrival-rate',
rate: __ENV.REQUESTS_PER_SECOND ?? 10, // requests per second
timeUnit: '1s', // 50 requests per 1 second
duration: __ENV.DURATION, // how long to run
preAllocatedVUs: 10, // number of VUs to pre-allocate
maxVUs: 50, // max number of VUs
},
},
};
const otelEndpointUrl = __ENV.OTEL_ENDPOINT || 'http://localhost:4318/v1/traces';
console.log(
`Endpoint: ${otelEndpointUrl}. (Overwrite using the OTEL_ENDPOINT environment variable)`,
);
const HIVE_ORGANIZATION_ACCESS_TOKEN = __ENV.HIVE_ORGANIZATION_ACCESS_TOKEN;
if (!HIVE_ORGANIZATION_ACCESS_TOKEN) {
throw new Error('Environment variable HIVE_ORGANIZATION_ACCESS_TOKEN is missing.');
}
const HIVE_TARGET_REF = __ENV.HIVE_TARGET_REF;
if (!HIVE_TARGET_REF) {
throw new Error('Environment variable HIVE_TARGET_REF is missing.');
}
// A helper to generate a random 16-byte trace/span ID in hex
function randomId(bytes: number = 32): string {
let traceId = '';
for (let i = 0; i < bytes; i++) {
// generate random nibble (015)
const nibble = Math.floor(Math.random() * 16);
traceId += nibble.toString(16);
}
// ensure not all zero (very unlikely)
if (/^0+$/.test(traceId)) {
return randomId(bytes);
}
return traceId;
}
function toTimeUnixNano(date = new Date()) {
const milliseconds = date.getTime(); // ms since epoch
const nanoseconds = BigInt(milliseconds) * 1_000_000n; // ns = ms × 1_000_000
return nanoseconds;
}
function getRandomIndex(length: number) {
return Math.floor(Math.random() * length);
}
function randomArrayItem<T>(arr: Array<T>) {
return arr[getRandomIndex(arr.length)];
}
const clientNames = new Array(countUniqueClients)
.fill(null)
.map(() => randomString(randomIntBetween(5, 30)));
const appVersions = new Map<string, Array<string>>();
for (const name of clientNames) {
const versions = new Array<string>();
for (let i = 0; i <= appVersionsPerClient; i++) {
versions.push(randomString(20));
}
appVersions.set(name, versions);
}
function generateRandomClient() {
const name = randomArrayItem(clientNames);
const version = randomArrayItem(appVersions.get(name)!);
return {
name,
version,
};
}
const errorCodes = new Array(countUniqueErrorCodes)
.fill(null)
.map(() => randomString(randomIntBetween(3, 30)));
function getRandomErrorCodes() {
if (randomIntBetween(0, 10) > 3) {
return [];
}
if (randomIntBetween(0, 10) > 3) {
return new Array(randomIntBetween(1, 10)).fill(null).map(() => randomArrayItem(errorCodes));
}
return [randomArrayItem(errorCodes)];
}
// graphql query document size
// operation name length
//
const references: Array<Reference> = [
open('./../../scripts/seed-traces/sample-introspection.json'),
open('./../../scripts/seed-traces/sample-my-profile.json'),
open('./../../scripts/seed-traces/sample-products-overview.json'),
open('./../../scripts/seed-traces/sample-user-review.json'),
open('./../../scripts/seed-traces/sample-user-review-error-missing-variables.json'),
open('./../../scripts/seed-traces/sample-user-review-not-found.json'),
].map(res => JSON.parse(res));
function mutate(currentTime: Date, reference: Reference) {
const newTraceId = randomId();
const newSpanIds = new Map<string, string>();
function getNewSpanId(spanId: string) {
let newSpanId = newSpanIds.get(spanId);
if (!newSpanId) {
newSpanId = randomId(16);
newSpanIds.set(spanId, newSpanId);
}
return newSpanId;
}
let rootTrace:
| Reference[number]['resourceSpans'][number]['scopeSpans'][number]['spans'][number]
| null = null;
for (const payload of reference) {
for (const resourceSpan of payload.resourceSpans) {
for (const scopeSpan of resourceSpan.scopeSpans) {
for (const span of scopeSpan.spans) {
if (span.parentSpanId === undefined) {
rootTrace = span;
const client = generateRandomClient();
rootTrace.attributes.push(
{
key: 'hive.client.name',
value: { stringValue: client.name },
},
{
key: 'hive.client.version',
value: { stringValue: client.version },
},
// TODO: actually calculate this based on the operation.
{
key: 'hive.graphql.operation.hash',
value: { stringValue: randomString(20) },
},
);
const errors = getRandomErrorCodes();
if (errors.length) {
rootTrace.attributes.push(
{
key: 'hive.graphql.error.codes',
value: {
arrayValue: {
values: errors.map(code => ({ stringValue: code })),
},
},
},
{
key: 'hive.graphql.error.count',
value: { intValue: errors.length },
},
);
}
break;
}
}
}
}
}
if (!rootTrace) {
throw new Error('Parent Span must always be the first span in the file.');
}
const startTime = BigInt(rootTrace.startTimeUnixNano);
const currentTimeB = toTimeUnixNano(currentTime);
for (const payload of reference) {
for (const resourceSpans of payload.resourceSpans) {
for (const scopeSpan of resourceSpans.scopeSpans) {
for (const span of scopeSpan.spans) {
if (span.parentSpanId) {
span.parentSpanId = getNewSpanId(span.parentSpanId);
}
span.spanId = getNewSpanId(span.spanId);
span.traceId = newTraceId;
const spanStartTime = BigInt(span.startTimeUnixNano);
const spanEndTime = BigInt(span.endTimeUnixNano);
const spanDuration = spanEndTime - spanStartTime;
const spanOffset = spanStartTime - startTime;
const newStartTime = currentTimeB + spanOffset;
span.startTimeUnixNano = newStartTime.toString();
span.endTimeUnixNano = (newStartTime + spanDuration).toString();
if (span.events.length) {
for (const event of span.events) {
const spanStartTime = BigInt(event.timeUnixNano);
const spanOffset = spanStartTime - startTime;
const newStartTime = currentTimeB + spanOffset;
event.timeUnixNano = newStartTime.toString();
}
}
}
}
}
}
}
function createTrace(date: Date, reference: Reference) {
return immer.produce(reference, draft => mutate(date, draft));
}
export default function () {
const data = new Array(50).fill(null).flatMap(() => {
const reference = randomArrayItem(references);
const tracePayloads = createTrace(new Date(), reference);
return tracePayloads.flatMap(payload => payload.resourceSpans);
});
const response = http.post(otelEndpointUrl, JSON.stringify({ resourceSpans: data }), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${HIVE_ORGANIZATION_ACCESS_TOKEN}`,
'x-hive-target-ref': HIVE_TARGET_REF,
},
});
check(response, {
'is status 200': r => r.status === 200,
});
}

View file

@ -29,7 +29,7 @@
"docker:override-up": "docker compose -f ./docker/docker-compose.override.yml up -d --remove-orphans", "docker:override-up": "docker compose -f ./docker/docker-compose.override.yml up -d --remove-orphans",
"env:sync": "tsx scripts/sync-env-files.ts", "env:sync": "tsx scripts/sync-env-files.ts",
"generate": "pnpm --filter @hive/storage db:generate && pnpm graphql:generate", "generate": "pnpm --filter @hive/storage db:generate && pnpm graphql:generate",
"graphql:generate": "graphql-codegen --config codegen.mts", "graphql:generate": "VERBOSE=1 graphql-codegen --config codegen.mts",
"graphql:generate:watch": "pnpm graphql:generate --watch", "graphql:generate:watch": "pnpm graphql:generate --watch",
"integration:prepare": "cd integration-tests && ./local.sh", "integration:prepare": "cd integration-tests && ./local.sh",
"lint": "eslint --cache --ignore-path .gitignore \"{packages,cypress}/**/*.{ts,tsx,graphql}\"", "lint": "eslint --cache --ignore-path .gitignore \"{packages,cypress}/**/*.{ts,tsx,graphql}\"",

View file

@ -0,0 +1,10 @@
extend type Query {
topProducts(first: Int = 5): [Product]
}
type Product @key(fields: "upc") {
upc: String!
name: String
price: Int
weight: Int
}

View file

@ -0,0 +1,17 @@
type Review @key(fields: "id") {
id: ID!
body: String
author: User @provides(fields: "username")
product: Product
}
extend type User @key(fields: "id") {
id: ID! @external
username: String @external
reviews: [Review]
}
extend type Product @key(fields: "upc") {
upc: String! @external
reviews: [Review]
}

View file

@ -0,0 +1,11 @@
extend type Query {
me: User
user(id: ID!): User
users: [User]
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}

View file

@ -0,0 +1,316 @@
import type { Action } from '../clickhouse';
export const action: Action = async exec => {
// Base tables as created by otel-exporter clickhouse
await exec(`
CREATE TABLE IF NOT EXISTS "otel_traces" (
"Timestamp" DateTime64(9, 'UTC') CODEC(Delta(8), ZSTD(1))
, "TraceId" String CODEC(ZSTD(1))
, "SpanId" String CODEC(ZSTD(1))
, "ParentSpanId" String CODEC(ZSTD(1))
, "TraceState" String CODEC(ZSTD(1))
, "SpanName" String CODEC(ZSTD(1))
, "SpanKind" LowCardinality(String) CODEC(ZSTD(1))
, "ServiceName" LowCardinality(String) CODEC(ZSTD(1))
, "ResourceAttributes" Map(LowCardinality(String), String) CODEC(ZSTD(1))
, "ScopeName" String CODEC(ZSTD(1))
, "ScopeVersion" String CODEC(ZSTD(1))
, "SpanAttributes" Map(LowCardinality(String), String) CODEC(ZSTD(1))
, "Duration" UInt64 CODEC(ZSTD(1))
, "StatusCode" LowCardinality(String) CODEC(ZSTD(1))
, "StatusMessage" String CODEC(ZSTD(1))
, "Events.Timestamp" Array(DateTime64(9, 'UTC')) CODEC(ZSTD(1))
, "Events.Name" Array(LowCardinality(String)) CODEC(ZSTD(1))
, "Events.Attributes" Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1))
, "Links.TraceId" Array(String) CODEC(ZSTD(1))
, "Links.SpanId" Array(String) CODEC(ZSTD(1))
, "Links.TraceState" Array(String) CODEC(ZSTD(1))
, "Links.Attributes" Array(Map(LowCardinality(String), String)) CODEC(ZSTD(1))
, INDEX "idx_trace_id" "TraceId" TYPE bloom_filter(0.001) GRANULARITY 1
, INDEX "idx_res_attr_key" mapKeys("ResourceAttributes") TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_res_attr_value" mapValues("ResourceAttributes") TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_span_attr_key" mapKeys("SpanAttributes") TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_span_attr_value" mapValues("SpanAttributes") TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_duration" Duration TYPE minmax GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate("Timestamp")
ORDER BY (
"ServiceName"
, "SpanName"
, toDateTime("Timestamp")
)
TTL toDate("Timestamp") + toIntervalDay(365)
SETTINGS
index_granularity = 8192
, ttl_only_drop_parts = 1
`);
await exec(`
CREATE TABLE IF NOT EXISTS "otel_traces_trace_id_ts" (
"TraceId" String CODEC(ZSTD(1))
, "Start" DateTime CODEC(Delta(4), ZSTD(1))
, "End" DateTime CODEC(Delta(4), ZSTD(1))
, INDEX "idx_trace_id" "TraceId" TYPE bloom_filter(0.01) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate(Start)
ORDER BY (
"TraceId"
, "Start"
)
TTL toDate("Start") + toIntervalDay(365)
SETTINGS
index_granularity = 8192
, ttl_only_drop_parts = 1
`);
await exec(`
CREATE MATERIALIZED VIEW IF NOT EXISTS "otel_traces_trace_id_ts_mv" TO "otel_traces_trace_id_ts" (
"TraceId" String
, "Start" DateTime64(9)
, "End" DateTime64(9)
)
AS (
SELECT
"TraceId"
, min("Timestamp") AS "Start"
, max("Timestamp") AS "End"
FROM
"otel_traces"
WHERE
"TraceId" != ''
GROUP BY
"TraceId"
)
`);
// Table and MV for trace overview and filtering
await exec(`
CREATE TABLE IF NOT EXISTS "otel_traces_normalized" (
"target_id" LowCardinality(String) CODEC(ZSTD(1))
, "trace_id" String CODEC(ZSTD(1))
, "span_id" String CODEC(ZSTD(1))
, "timestamp" DateTime('UTC') CODEC(DoubleDelta, LZ4)
, "duration" UInt64 CODEC(T64, ZSTD(1))
, "http_status_code" String CODEC(ZSTD(1))
, "http_method" String CODEC(ZSTD(1))
, "http_host" String CODEC(ZSTD(1))
, "http_route" String CODEC(ZSTD(1))
, "http_url" String CODEC(ZSTD(1))
, "client_name" String Codec(ZSTD(1))
, "client_version" String Codec(ZSTD(1))
, "graphql_operation_name" String CODEC(ZSTD(1))
, "graphql_operation_type" LowCardinality(String) CODEC(ZSTD(1))
, "graphql_operation_document" String CODEC(ZSTD(1))
, "graphql_operation_hash" String CODEC(ZSTD(1))
, "graphql_error_count" UInt32 CODEC(T64, ZSTD(1))
, "graphql_error_codes" Array(LowCardinality(String)) CODEC(ZSTD(1))
, "subgraph_names" Array(LowCardinality(String)) CODEC(ZSTD(1))
, INDEX "idx_duration" "duration" TYPE minmax GRANULARITY 1
, INDEX "idx_http_status_code" "http_status_code" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_http_method" "http_method" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_http_host" "http_host" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_http_route" "http_route" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_http_url" "http_url" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_client_name" "client_name" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_client_version" "client_version" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_graphql_operation_name" "graphql_operation_name" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_graphql_operation_type" "graphql_operation_type" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_graphql_error_codes" "graphql_error_codes" TYPE bloom_filter(0.01) GRANULARITY 1
, INDEX "idx_subgraph_names" "subgraph_names" TYPE bloom_filter(0.01) GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toDate("timestamp")
ORDER BY ("target_id", "timestamp")
TTL toDateTime(timestamp) + toIntervalDay(365)
SETTINGS
index_granularity = 8192
, ttl_only_drop_parts = 1
`);
// You might be wondering why we parse the data in such a weird way.
// This was the smartest I way I came up that did not require introducing additional span attribute validation logic on the gateway.
// We want to avoid inserts failing due to a column type-mismatch at any chance, since we are doing batch inserts and one fault record
// could prevent all other inserts within the same batch from happening.
//
// The idea here is to attempt verifying that the input is "array"-like and if so parse it as safe as possible.
// If the input is not "array"-like we just insert an empty array and move on.
//
// Later on, we could think about actually rejecting incorrect span values on the otel-collector business logic.
//
// ```
// if(
// JSONType(toString("SpanAttributes"['hive.graphql.error.codes'])) = 'Array',
// arrayFilter(
// -- Filter out empty values
// x -> notEquals(x, ''),
// arrayMap(
// -- If the user provided something non-stringy, this returns '', which is fine imho
// x -> JSONExtractString(x),
// JSONExtractArrayRaw(toString("SpanAttributes"['hive.graphql.error.codes']))
// )
// ),
// []
// )
// ```
await exec(`
CREATE MATERIALIZED VIEW IF NOT EXISTS "otel_traces_normalized_mv" TO "otel_traces_normalized" (
"target_id" LowCardinality(String)
, "trace_id" String
, "span_id" String
, "timestamp" DateTime('UTC')
, "duration" UInt64
, "http_status_code" String
, "http_host" String
, "http_method" String
, "http_route" String
, "http_url" String
, "client_name" String
, "client_version" String
, "graphql_operation_name" String
, "graphql_operation_type" LowCardinality(String)
, "graphql_operation_document" String
, "graphql_operation_hash" String
, "graphql_error_count" UInt32
, "graphql_error_codes" Array(LowCardinality(String))
, "subgraph_names" Array(String)
)
AS (
SELECT
toLowCardinality("SpanAttributes"['hive.target_id']) AS "target_id"
, "TraceId" as "trace_id"
, "SpanId" AS "span_id"
, toDateTime("Timestamp", 'UTC') AS "timestamp"
, "Duration" AS "duration"
, "SpanAttributes"['http.status_code'] AS "http_status_code"
, "SpanAttributes"['http.host'] AS "http_host"
, "SpanAttributes"['http.method'] AS "http_method"
, "SpanAttributes"['http.route'] AS "http_route"
, "SpanAttributes"['http.url'] AS "http_url"
, "SpanAttributes"['hive.client.name'] AS "client_name"
, "SpanAttributes"['hive.client.version'] AS "client_version"
, "SpanAttributes"['graphql.operation.name'] AS "graphql_operation_name"
, toLowCardinality("SpanAttributes"['graphql.operation.type']) AS "graphql_operation_type"
, "SpanAttributes"['graphql.document'] AS "graphql_operation_document"
, "SpanAttributes"['hive.graphql.operation.hash'] AS "graphql_operation_hash"
, toInt64OrZero("SpanAttributes"['hive.graphql.error.count']) AS "graphql_error_count"
, if(
JSONType(toString("SpanAttributes"['hive.graphql.error.codes'])) = 'Array',
arrayFilter(
x -> notEquals(x, ''),
arrayMap(
x -> JSONExtractString(x),
JSONExtractArrayRaw(toString("SpanAttributes"['hive.graphql.error.codes']))
)
),
[]
) AS "graphql_error_codes"
, if(
JSONType(toString("SpanAttributes"['hive.gateway.operation.subgraph.names'])) = 'Array',
arrayFilter(
x -> notEquals(x, ''),
arrayMap(
x -> JSONExtractString(x),
JSONExtractArrayRaw(toString("SpanAttributes"['hive.gateway.operation.subgraph.names']))
)
),
[]
) AS "subgraph_names"
FROM
"otel_traces"
WHERE
empty("ParentSpanId")
AND notEmpty("SpanAttributes"['hive.graphql'])
)
`);
// These can be used for dedicated subgraph views
await exec(`
CREATE TABLE IF NOT EXISTS "otel_subgraph_spans" (
"target_id" LowCardinality(String) CODEC(ZSTD(1))
, "subgraph_name" String CODEC(ZSTD(1))
, "trace_id" String CODEC(ZSTD(1))
, "span_id" String CODEC(ZSTD(1))
, "timestamp" DateTime('UTC') CODEC(DoubleDelta, LZ4)
, "duration" UInt64 CODEC(T64, ZSTD(1))
, "http_status_code" String CODEC(ZSTD(1))
, "http_method" String CODEC(ZSTD(1))
, "http_host" String CODEC(ZSTD(1))
, "http_route" String CODEC(ZSTD(1))
, "http_url" String CODEC(ZSTD(1))
, "graphql_operation_name" String CODEC(ZSTD(1))
, "graphql_operation_type" LowCardinality(String) CODEC(ZSTD(1))
, "graphql_operation_document" String CODEC(ZSTD(1))
, "graphql_error_count" UInt32 CODEC(T64, ZSTD(1))
, "graphql_error_codes" Array(LowCardinality(String)) CODEC(ZSTD(1))
)
ENGINE = MergeTree
PARTITION BY toDate("timestamp")
ORDER BY ("target_id", "subgraph_name", "timestamp")
TTL toDateTime(timestamp) + toIntervalDay(365)
SETTINGS
index_granularity = 8192
, ttl_only_drop_parts = 1
`);
await exec(`
CREATE MATERIALIZED VIEW IF NOT EXISTS "otel_subgraph_spans_mv" TO "otel_subgraph_spans" (
"target_id" LowCardinality(String)
, "subgraph_name" Array(String)
, "trace_id" String
, "span_id" String
, "timestamp" DateTime('UTC')
, "duration" UInt64
, "http_status_code" String
, "http_host" String
, "http_method" String
, "http_route" String
, "http_url" String
, "client_name" String
, "client_version" String
, "graphql_operation_name" String
, "graphql_operation_type" LowCardinality(String)
, "graphql_operation_document" String
, "graphql_error_count" UInt32
, "graphql_error_codes" Array(LowCardinality(String))
)
AS (
SELECT
toLowCardinality("SpanAttributes"['hive.target_id']) AS "target_id"
, "SpanAttributes"['hive.graphql.subgraph.name'] AS "subgraph_name"
, "TraceId" as "trace_id"
, "SpanId" AS "span_id"
, toDateTime("Timestamp", 'UTC') AS "timestamp"
, "Duration" AS "duration"
, "SpanAttributes"['http.status_code'] AS "http_status_code"
, "SpanAttributes"['http.host'] AS "http_host"
, "SpanAttributes"['http.method'] AS "http_method"
, "SpanAttributes"['http.route'] AS "http_route"
, "SpanAttributes"['http.url'] AS "http_url"
, "SpanAttributes"['hive.client.name'] AS "client_name"
, "SpanAttributes"['hive.client.version'] AS "client_version"
, "SpanAttributes"['graphql.operation.name'] AS "graphql_operation_name"
, toLowCardinality("SpanAttributes"['graphql.operation.type']) AS "graphql_operation_type"
, "SpanAttributes"['graphql.document'] AS "graphql_operation_document"
, toInt64OrZero("SpanAttributes"['hive.graphql.error.count']) AS "graphql_error_count"
, if(
JSONType(toString("SpanAttributes"['hive.graphql.error.codes'])) = 'Array',
arrayFilter(
x -> notEquals(x, ''),
arrayMap(
x -> JSONExtractString(x),
JSONExtractArrayRaw(toString("SpanAttributes"['hive.graphql.error.codes']))
)
),
[]
) AS "graphql_error_codes"
FROM
"otel_traces"
WHERE
notEmpty("SpanAttributes"['hive.graphql.subgraph.name'])
)
`);
};

View file

@ -175,6 +175,7 @@ export async function migrateClickHouse(
import('./clickhouse-actions/012-coordinates-typename-index'), import('./clickhouse-actions/012-coordinates-typename-index'),
import('./clickhouse-actions/013-apply-ttl'), import('./clickhouse-actions/013-apply-ttl'),
import('./clickhouse-actions/014-audit-logs-access-token'), import('./clickhouse-actions/014-audit-logs-access-token'),
import('./clickhouse-actions/015-otel-trace'),
]); ]);
async function actionRunner(action: Action, index: number) { async function actionRunner(action: Action, index: number) {

View file

@ -412,6 +412,7 @@ const permissionsByLevel = {
z.literal('laboratory:modifyPreflightScript'), z.literal('laboratory:modifyPreflightScript'),
z.literal('schema:compose'), z.literal('schema:compose'),
z.literal('usage:report'), z.literal('usage:report'),
z.literal('traces:report'),
], ],
service: [ service: [
z.literal('schemaCheck:create'), z.literal('schemaCheck:create'),

View file

@ -2,6 +2,7 @@ import { createModule } from 'graphql-modules';
import { ClickHouse } from './providers/clickhouse-client'; import { ClickHouse } from './providers/clickhouse-client';
import { OperationsManager } from './providers/operations-manager'; import { OperationsManager } from './providers/operations-manager';
import { OperationsReader } from './providers/operations-reader'; import { OperationsReader } from './providers/operations-reader';
import { Traces } from './providers/traces';
import { resolvers } from './resolvers.generated'; import { resolvers } from './resolvers.generated';
import typeDefs from './module.graphql'; import typeDefs from './module.graphql';
@ -10,5 +11,5 @@ export const operationsModule = createModule({
dirname: __dirname, dirname: __dirname,
typeDefs, typeDefs,
resolvers, resolvers,
providers: [OperationsManager, OperationsReader, ClickHouse], providers: [OperationsManager, OperationsReader, ClickHouse, Traces],
}); });

View file

@ -1,5 +1,8 @@
import type { ClientStatsValues, OperationStatsValues, PageInfo } from '../../__generated__/types'; import type { ClientStatsValues, OperationStatsValues, PageInfo } from '../../__generated__/types';
import type { DateRange } from '../../shared/entities'; import type { DateRange } from '../../shared/entities';
import type { Span, Trace, TraceBreakdownLoader } from './providers/traces';
// import { SqlValue } from './providers/sql';
type Connection<TNode> = { type Connection<TNode> = {
pageInfo: PageInfo; pageInfo: PageInfo;
@ -41,3 +44,8 @@ export interface DurationValuesMapper {
p95: number | null; p95: number | null;
p99: number | null; p99: number | null;
} }
export type TracesFilterOptionsMapper = TraceBreakdownLoader;
export type TraceMapper = Trace;
export type SpanMapper = Span;

View file

@ -259,6 +259,217 @@ export default gql`
body: String! @tag(name: "public") body: String! @tag(name: "public")
} }
type TraceConnection {
edges: [TraceEdge!]!
pageInfo: PageInfo!
}
type TraceEdge {
node: Trace!
cursor: String!
}
type Trace {
id: ID!
timestamp: DateTime!
operationName: String
operationType: GraphQLOperationType
"""
The Hash of the GraphQL operation.
"""
operationHash: ID
"""
Total duration of the trace.
"""
duration: SafeInt!
"""
The subgraphs called within the trace.
"""
subgraphs: [String!]
"""
Wether the trace is successful.
A trace is a success if no GraphQL errors occured and the HTTP status code is in the 2XX to 3XX range.
"""
success: Boolean!
"""
The client name.
Usually this is the 'x-graphql-client-name' header sent to the gateway.
"""
clientName: String
"""
The client version.
Usually this is the 'x-graphql-client-version' header sent to the gateway.
"""
clientVersion: String
httpStatusCode: String
httpMethod: String
httpHost: String
httpRoute: String
httpUrl: String
spans: [Span!]!
}
type SpanEvent {
date: DateTime64!
name: String!
attributes: JSONObject!
}
type Span {
id: ID!
traceId: ID!
parentId: ID
name: String!
startTime: DateTime64!
duration: SafeInt!
endTime: DateTime64!
resourceAttributes: JSONObject!
spanAttributes: JSONObject!
events: [SpanEvent!]!
}
input DurationInput {
min: SafeInt
max: SafeInt
}
input TracesFilterInput {
"""
Time range filter for the traces.
"""
period: DateRangeInput
"""
Duration filter for the traces.
"""
duration: DurationInput
"""
Filter based on trace ID.
"""
traceIds: [ID!]
"""
Filter based on whether the operation is a success.
A operation is successful if no GraphQL error has occured and the result is within the 2XX or 3XX range.
"""
success: [Boolean!]
"""
Filter based on GraphQL error codes (error.extensions.code).
"""
errorCodes: [String!]
"""
Filter based on the operation name.
"""
operationNames: [String!]
"""
Filter based on the operation type.
A value of 'null' value indicates a unknown operation type.
"""
operationTypes: [GraphQLOperationType]
"""
Filter based on the client name.
"""
clientNames: [String!]
"""
Filter based on the HTTP status code of the request.
"""
httpStatusCodes: [String!]
"""
Filter based on the HTTP method of the request.
"""
httpMethods: [String!]
"""
Filter based on the HTTP host of the request.
"""
httpHosts: [String!]
"""
Filter based on the HTTP route of the request.
"""
httpRoutes: [String!]
"""
Filter based on the HTTP URL of the request.
"""
httpUrls: [String!]
"""
Filter based on called subgraphs.
"""
subgraphNames: [String!]
}
type TracesFilterOptions {
success: [FilterBooleanOption!]!
"""
Filter based on GraphQL error code.
"""
errorCode(top: Int): [FilterStringOption!]!
operationType: [FilterStringOption!]!
operationName(top: Int): [FilterStringOption!]!
clientName(top: Int): [FilterStringOption!]!
httpStatusCode(top: Int): [FilterStringOption!]!
httpMethod(top: Int): [FilterStringOption!]!
httpHost(top: Int): [FilterStringOption!]!
httpRoute(top: Int): [FilterStringOption!]!
httpUrl(top: Int): [FilterStringOption!]!
subgraphs(top: Int): [FilterStringOption!]!
}
type FilterStringOption {
value: String!
count: Int!
}
type FilterBooleanOption {
value: Boolean!
count: Int!
}
type FilterIntOption {
value: Int!
count: Int!
}
enum SortDirectionType {
ASC
DESC
}
enum TracesSortType {
DURATION
TIMESTAMP
}
input TracesSortInput {
sort: TracesSortType!
direction: SortDirectionType!
}
type TraceStatusBreakdownBucket {
"""
The time bucket for the data
"""
timeBucketStart: DateTime!
"""
The end of the time bucket for the data
"""
timeBucketEnd: DateTime!
"""
Total amount of ok traces in the bucket.
"""
okCountTotal: SafeInt!
"""
Total amount of error traces in the bucket.
"""
errorCountTotal: SafeInt!
"""
Total amount of ok traces in the bucket based on the filter.
"""
okCountFiltered: SafeInt!
"""
Total mount of error traces in the bucket based on the filter.
"""
errorCountFiltered: SafeInt!
}
extend type Target { extend type Target {
requestsOverTime(resolution: Int!, period: DateRangeInput!): [RequestsOverTime!]! requestsOverTime(resolution: Int!, period: DateRangeInput!): [RequestsOverTime!]!
totalRequests(period: DateRangeInput!): SafeInt! totalRequests(period: DateRangeInput!): SafeInt!
@ -266,6 +477,20 @@ export default gql`
Retrieve an operation via it's hash. Retrieve an operation via it's hash.
""" """
operation(hash: ID! @tag(name: "public")): Operation @tag(name: "public") operation(hash: ID! @tag(name: "public")): Operation @tag(name: "public")
"""
Whether the viewer can access OTEL traces
"""
viewerCanAccessTraces: Boolean!
traces(
first: Int
after: String
filter: TracesFilterInput
sort: TracesSortInput
): TraceConnection!
tracesFilterOptions(filter: TracesFilterInput): TracesFilterOptions!
tracesStatusBreakdown(filter: TracesFilterInput): [TraceStatusBreakdownBucket!]!
trace(traceId: ID!): Trace
} }
extend type Project { extend type Project {

View file

@ -120,6 +120,23 @@ export class ClickHouse {
}, },
retry: { retry: {
calculateDelay: info => { calculateDelay: info => {
if (
info.error.response?.body &&
typeof info.error.response.body === 'object' &&
'exception' in info.error.response.body &&
typeof info.error.response.body.exception === 'string'
) {
this.logger.error(info.error.response.body.exception);
// In case of development errors we don't need to retry
// https://github.com/ClickHouse/ClickHouse/blob/eb33caaa13355761e4ceaba4a41b8801161ce327/src/Common/ErrorCodes.cpp#L55
// //https://github.com/ClickHouse/ClickHouse/blob/eb33caaa13355761e4ceaba4a41b8801161ce327/src/Common/ErrorCodes.cpp#L68C7-L68C9
if (
info.error.response.body.exception.startsWith('Code: 47') ||
info.error.response.body.exception.startsWith('Code: 62')
) {
return 0;
}
}
span.setAttribute('retry.count', info.attemptCount); span.setAttribute('retry.count', info.attemptCount);
if (info.attemptCount >= 6) { if (info.attemptCount >= 6) {

View file

@ -19,7 +19,7 @@ const CoordinateClientNamesGroupModel = z.array(
}), }),
); );
function formatDate(date: Date): string { export function formatDate(date: Date): string {
return format(addMinutes(date, date.getTimezoneOffset()), 'yyyy-MM-dd HH:mm:ss'); return format(addMinutes(date, date.getTimezoneOffset()), 'yyyy-MM-dd HH:mm:ss');
} }

View file

@ -0,0 +1,832 @@
import DataLoader from 'dataloader';
import stableJSONStringify from 'fast-json-stable-stringify';
import { Injectable } from 'graphql-modules';
import { z } from 'zod';
import { subDays } from '@/lib/date-time';
import * as GraphQLSchema from '../../../__generated__/types';
import { HiveError } from '../../../shared/errors';
import { batch, parseDateRangeInput } from '../../../shared/helpers';
import { Logger } from '../../shared/providers/logger';
import { Storage } from '../../shared/providers/storage';
import { ClickHouse, sql } from './clickhouse-client';
import { formatDate } from './operations-reader';
import { SqlValue } from './sql';
@Injectable({
global: true,
})
export class Traces {
constructor(
private clickHouse: ClickHouse,
private logger: Logger,
private storage: Storage,
) {}
async viewerCanAccessTraces(organizationId: string) {
const organization = await this.storage.getOrganization({ organizationId });
return organization.featureFlags.otelTracing;
}
private async _guardViewerCanAccessTraces(organizationId: string) {
if (await this.viewerCanAccessTraces(organizationId)) {
return;
}
throw new HiveError("You don't have acces to this feature.");
}
private _findTraceByTraceId = batch<string, Trace | null>(async (traceIds: Array<string>) => {
this.logger.debug('looking up traces by id (traceIds=%o)', traceIds);
const result = await this.clickHouse.query<unknown>({
query: sql`
SELECT
${traceFields}
FROM
"otel_traces_normalized"
WHERE
"trace_id" IN (${sql.array(traceIds, 'String')})
LIMIT 1 BY "trace_id"
`,
timeout: 10_000,
queryId: 'Traces.findTraceByTraceId',
});
this.logger.debug('found %d traces', result.data.length);
const lookupMap = new Map</* traceId */ string, Trace>();
const traces = TraceListModel.parse(result.data);
for (const trace of traces) {
lookupMap.set(trace.traceId, trace);
}
return traceIds.map(traceId => Promise.resolve(lookupMap.get(traceId) ?? null));
});
/**
* Find a specific trace by it's id.
* Uses batching under the hood.
*/
async findTraceById(
organizationId: string,
targetId: string,
traceId: string,
): Promise<Trace | null> {
await this._guardViewerCanAccessTraces(organizationId);
this.logger.debug('find trace by id (targetId=%s, traceId=%s)', targetId, traceId);
const trace = await this._findTraceByTraceId(traceId);
if (!trace) {
this.logger.debug('could not find trace by id (targetId=%s, traceId=%s)', targetId, traceId);
return null;
}
if (trace.targetId !== targetId) {
this.logger.debug(
'resolved trace target id does not match (targetId=%s, traceId=%s)',
targetId,
traceId,
);
return null;
}
this.logger.debug('trace found (targetId=%s, traceId=%s)', targetId, traceId);
return trace;
}
async findSpansForTraceId(traceId: string, targetId: string): Promise<Array<Span>> {
this.logger.debug('find spans for trace (traceId=%s)', traceId);
const result = await this.clickHouse.query<unknown>({
query: sql`
SELECT
${spanFields}
FROM
"otel_traces"
WHERE
"TraceId" = ${traceId}
AND "SpanAttributes"['hive.target_id'] = ${targetId}
`,
timeout: 10_000,
queryId: 'Traces.findSpansForTraceId',
});
return SpanListModel.parse(result.data);
}
async findTracesForTargetId(
organizationId: string,
targetId: string,
first: number | null,
filter: TraceFilter,
sort: GraphQLSchema.TracesSortInput | null,
cursorStr: string | null,
) {
function createCursor(trace: Trace) {
return Buffer.from(
JSON.stringify({
timestamp: trace.timestamp,
traceId: trace.traceId,
duration: sort?.sort === 'DURATION' ? trace.duration : undefined,
} satisfies z.TypeOf<typeof PaginatedTraceCursorModel>),
).toString('base64');
}
function parseCursor(cursor: string) {
const data = PaginatedTraceCursorModel.parse(
JSON.parse(Buffer.from(cursor, 'base64').toString('utf8')),
);
if (sort?.sort === 'DURATION' && !data.duration) {
throw new HiveError('Invalid cursor provided.');
}
return data;
}
await this._guardViewerCanAccessTraces(organizationId);
const limit = first ?? 50;
const cursor = cursorStr ? parseCursor(cursorStr) : null;
// By default we order by timestamp DESC
// In case a custom sort is provided, we order by duration asc/desc or timestamp asc
const orderByFragment = sql`
${sort?.sort === 'DURATION' ? sql`"duration" ${sort.direction === 'ASC' ? sql`ASC` : sql`DESC`},` : sql``}
"timestamp" ${sort?.sort === 'TIMESTAMP' && sort?.direction === 'ASC' ? sql`ASC` : sql`DESC`}
, "trace_id" DESC
`;
let paginationSQLFragmentPart = sql``;
if (cursor) {
if (sort?.sort === 'DURATION') {
const operator = sort.direction === 'ASC' ? sql`>` : sql`<`;
const durationStr = String(cursor.duration);
paginationSQLFragmentPart = sql`
AND (
"duration" ${operator} ${durationStr}
OR (
"duration" = ${durationStr}
AND "timestamp" < ${cursor.timestamp}
)
OR (
"duration" = ${durationStr}
AND "timestamp" = ${cursor.timestamp}
AND "trace_id" < ${cursor.traceId}
)
)
`;
} /* TIMESTAMP */ else {
const operator = sort?.direction === 'ASC' ? sql`>` : sql`<`;
paginationSQLFragmentPart = sql`
AND (
(
"timestamp" = ${cursor.timestamp}
AND "trace_id" < ${cursor.traceId}
)
OR "timestamp" ${operator} ${cursor.timestamp}
)
`;
}
}
const sqlConditions = buildTraceFilterSQLConditions(filter, false);
const filterSQLFragment = sqlConditions.length
? sql`AND ${sql.join(sqlConditions, ' AND ')}`
: sql``;
const tracesQuery = await this.clickHouse.query<unknown>({
query: sql`
SELECT
${traceFields}
FROM
"otel_traces_normalized"
WHERE
target_id = ${targetId}
${paginationSQLFragmentPart}
${filterSQLFragment}
ORDER BY
${orderByFragment}
LIMIT ${sql.raw(String(limit + 1))}
`,
queryId: 'traces',
timeout: 10_000,
});
let traces = TraceListModel.parse(tracesQuery.data);
const hasNext = traces.length > limit;
traces = traces.slice(0, limit);
return {
edges: traces.map(trace => ({
node: trace,
cursor: createCursor(trace),
})),
pageInfo: {
hasNextPage: hasNext,
hasPreviousPage: false,
endCursor: traces.length ? createCursor(traces[traces.length - 1]) : '',
startCursor: traces.length ? createCursor(traces[0]) : '',
},
};
}
async getTraceStatusBreakdownForTargetId(
organizationId: string,
targetId: string,
filter: TraceFilter,
) {
await this._guardViewerCanAccessTraces(organizationId);
const sqlConditions = buildTraceFilterSQLConditions(filter, true);
const filterSQLFragment = sqlConditions.length
? sql`AND ${sql.join(sqlConditions, ' AND ')}`
: sql``;
const endDate = filter.period?.to ?? new Date();
const startDate = filter.period?.from ?? subDays(endDate, 14);
const d = getBucketUnitAndCountNew(startDate, endDate);
const [countStr, unit] = d.candidate.name.split(' ');
const bucketStepFunctionName = {
MINUTE: 'addMinutes',
HOUR: 'addHours',
DAY: 'addDays',
WEEK: 'addWeeks',
MONTH: 'addMonths',
};
const addIntervalFn = sql.raw(
bucketStepFunctionName[unit as keyof typeof bucketStepFunctionName],
);
const result = await this.clickHouse.query<unknown>({
query: sql`
WITH "time_bucket_list" AS (
SELECT
${addIntervalFn}(
toStartOfInterval(
toDateTime(${formatDate(startDate)}, 'UTC')
, INTERVAL ${sql.raw(d.candidate.name)}
)
, ("number" + 1) * ${sql.raw(countStr)}
) AS "time_bucket"
FROM
"system"."numbers"
WHERE "system"."numbers"."number" < ${String(d.buckets)}
)
SELECT
replaceOne(concat(toDateTime64("time_bucket_list"."time_bucket", 9, 'UTC'), 'Z'), ' ', 'T') AS "timeBucketStart"
, replaceOne(concat(
toDateTime64(
"time_bucket_list"."time_bucket"
+ INTERVAL ${sql.raw(d.candidate.name)}
- INTERVAL 1 SECOND
, 9
, 'UTC'
) , 'Z') , ' ' , 'T'
) AS "timeBucketEnd"
, coalesce("t"."ok_count_total", 0) as "okCountTotal"
, coalesce("t"."error_count_total", 0) as "errorCountTotal"
, coalesce("t"."ok_count_filtered", 0) as "okCountFiltered"
, coalesce("t"."error_count_filtered", 0) as "errorCountFiltered"
FROM
"time_bucket_list"
LEFT JOIN
(
SELECT
toStartOfInterval("timestamp", INTERVAL ${sql.raw(d.candidate.name)}) AS "time_bucket_start"
, sumIf(1, "graphql_error_count" = 0) AS "ok_count_total"
, sumIf(1, "graphql_error_count" != 0) AS "error_count_total"
, sumIf(1, "graphql_error_count" = 0 ${filterSQLFragment}) AS "ok_count_filtered"
, sumIf(1, "graphql_error_count" != 0 ${filterSQLFragment}) AS "error_count_filtered"
FROM
"otel_traces_normalized"
WHERE
"target_id" = ${targetId}
AND "otel_traces_normalized"."timestamp" >= toDateTime(${formatDate(startDate)}, 'UTC')
AND "otel_traces_normalized"."timestamp" <= toDateTime(${formatDate(endDate)}, 'UTC')
GROUP BY
"time_bucket_start"
) AS "t"
ON "t"."time_bucket_start" = "time_bucket_list"."time_bucket"
`,
queryId: `trace_status_breakdown_for_target_id_`,
timeout: 10_000,
});
return TraceStatusBreakdownBucketList.parse(result.data);
}
async getTraceFilterOptions(targetId: string, filter: GraphQLSchema.TracesFilterInput | null) {
return new TraceBreakdownLoader(this.logger, this.clickHouse, targetId, filter);
}
}
export class TraceBreakdownLoader {
private logger: Logger;
private conditions: SqlValue[];
private loader = new DataLoader<
{
key: string;
columnExpression: string;
limit: number | null;
arrayJoinColumn: string | null;
},
{ value: string; count: number }[],
string
>(
async inputs => {
const statements: SqlValue[] = [];
for (const { key, columnExpression, limit, arrayJoinColumn } of inputs) {
statements.push(sql`
SELECT
${key} AS "key",
toString(${sql.raw(columnExpression)}) AS "value",
count(*) AS "count"
FROM "otel_traces_normalized"
${sql.raw(arrayJoinColumn ? `ARRAY JOIN ${arrayJoinColumn} AS "value"` : '')}
WHERE ${sql.join(this.conditions, ' AND ')}
GROUP BY value
ORDER BY count DESC
${sql.raw(limit ? `LIMIT ${limit}` : '')}
`);
}
const results = await this.clickhouse.query<{
key: string;
value: string;
count: number;
}>({
query: sql`
${sql.join(statements, ' UNION ALL ')}
`,
queryId: 'traces_filter_options',
timeout: 10_000,
});
const rowsGroupedByKey = results.data.reduce(
(acc, row) => {
if (!acc[row.key]) {
acc[row.key] = [];
}
acc[row.key].push({ value: row.value, count: row.count });
return acc;
},
{} as Record<string, { value: string; count: number }[]>,
);
return inputs.map(input => rowsGroupedByKey[input.key] ?? []);
},
{
cacheKeyFn: stableJSONStringify,
},
);
constructor(
logger: Logger,
private clickhouse: ClickHouse,
targetId: string,
filter: GraphQLSchema.TracesFilterInput | null,
) {
this.logger = logger.child({ source: 'TraceBreakdownLoader' });
this.conditions = [sql`target_id = ${targetId}`];
if (filter?.traceIds?.length) {
this.conditions.push(sql`"trace_id" IN (${sql.array(filter.traceIds, 'String')})`);
}
if (filter?.success?.length) {
const hasSuccess = filter.success.includes(true);
const hasError = filter.success.includes(false);
if (hasSuccess && !hasError) {
this.conditions.push(
sql`"graphql_error_count" = 0`,
sql`substring("http_status_code", 1, 1) IN (${sql.array(['2', '3'], 'String')})`,
);
} else if (hasError && !hasSuccess) {
this.conditions.push(
sql`"graphql_error_count" > 0`,
sql`substring("http_status_code", 1, 1) NOT IN (${sql.array(['2', '3'], 'String')})`,
);
}
}
if (filter?.operationNames?.length) {
this.conditions.push(
sql`"graphql_operation_name" IN (${sql.array(filter.operationNames, 'String')})`,
);
}
if (filter?.operationTypes?.length) {
this.conditions.push(
sql`"graphql_operation_type" IN (${sql.array(
filter.operationTypes.map(value => (value == null ? '' : value.toLowerCase())),
'String',
)})`,
);
}
if (filter?.clientNames?.length) {
this.conditions.push(sql`"client_name" IN (${sql.array(filter.clientNames, 'String')})`);
}
if (filter?.subgraphNames?.length) {
this.conditions.push(
sql`hasAny("subgraph_names", (${sql.array(filter.subgraphNames.flat(), 'String')}))`,
);
}
if (filter?.httpStatusCodes?.length) {
this.conditions.push(
sql`"http_status_code" IN (${sql.array(filter.httpStatusCodes.map(String), 'UInt16')})`,
);
}
if (filter?.httpMethods?.length) {
this.conditions.push(sql`"http_method" IN (${sql.array(filter.httpMethods, 'String')})`);
}
if (filter?.httpHosts?.length) {
this.conditions.push(sql`"http_host" IN (${sql.array(filter.httpHosts, 'String')})`);
}
if (filter?.httpRoutes?.length) {
this.conditions.push(sql`"http_route" IN (${sql.array(filter.httpRoutes, 'String')})`);
}
if (filter?.httpUrls?.length) {
this.conditions.push(sql`"http_url" IN (${sql.array(filter.httpUrls, 'String')})`);
}
}
httpHost(top: number | null) {
return this.loader.load({
key: 'http_host',
columnExpression: 'http_host',
limit: top ?? 5,
arrayJoinColumn: null,
});
}
httpMethod(top: number | null) {
return this.loader.load({
key: 'http_method',
columnExpression: 'http_method',
limit: top ?? 5,
arrayJoinColumn: null,
});
}
httpRoute(top: number | null) {
return this.loader.load({
key: 'http_route',
columnExpression: 'http_route',
limit: top ?? 5,
arrayJoinColumn: null,
});
}
httpStatusCode(top: number | null) {
return this.loader.load({
key: 'http_status_code',
columnExpression: 'http_status_code',
limit: top ?? 5,
arrayJoinColumn: null,
});
}
httpUrl(top: number | null) {
return this.loader.load({
key: 'http_url',
columnExpression: 'http_url',
limit: top ?? 5,
arrayJoinColumn: null,
});
}
operationName(top: number | null) {
return this.loader.load({
key: 'graphql_operation_name',
columnExpression: 'graphql_operation_name',
limit: top ?? 5,
arrayJoinColumn: null,
});
}
operationType() {
return this.loader.load({
key: 'graphql_operation_type',
columnExpression: 'graphql_operation_type',
limit: null,
arrayJoinColumn: null,
});
}
subgraphs(top: number | null) {
return this.loader.load({
key: 'subgraph_names',
columnExpression: 'value',
limit: top ?? 5,
arrayJoinColumn: 'subgraph_names',
});
}
success() {
return this.loader
.load({
key: 'success',
columnExpression:
'if((toUInt16OrZero(http_status_code) >= 200 AND toUInt16OrZero(http_status_code) < 300), true, false) AND "graphql_error_count" = 0',
limit: null,
arrayJoinColumn: null,
})
.then(data =>
data.map(({ value, count }) => ({
value: value === 'true' ? true : false,
count,
})),
);
}
errorCode(top: number | null) {
return this.loader.load({
key: 'errorCode',
columnExpression: 'value',
limit: top ?? 10,
arrayJoinColumn: 'graphql_error_codes',
});
}
clientName(top: number | null) {
return this.loader.load({
key: 'client_name',
columnExpression: 'client_name',
limit: top ?? 10,
arrayJoinColumn: null,
});
}
}
const traceFields = sql`
"target_id" AS "targetId"
, "trace_id" AS "traceId"
, "span_id" AS "spanId"
, replaceOne(concat(toDateTime64("timestamp", 9, 'UTC'), 'Z'), ' ', 'T') AS "timestamp"
, "http_status_code" AS "httpStatusCode"
, "http_method" AS "httpMethod"
, "http_host" AS "httpHost"
, "http_route" AS "httpRoute"
, "http_url" AS "httpUrl"
, "duration"
, "graphql_operation_name" AS "graphqlOperationName"
, "graphql_operation_document" AS "graphqlOperationDocument"
, "graphql_operation_hash" AS "graphqlOperationHash"
, "client_name" AS "clientName"
, "client_version" AS "clientVersion"
, upper("graphql_operation_type") AS "graphqlOperationType"
, "graphql_error_count" AS "graphqlErrorCount"
, "graphql_error_codes" AS "graphqlErrorCodes"
, "subgraph_names" AS "subgraphNames"
`;
const TraceModel = z.object({
traceId: z.string(),
targetId: z.string().uuid(),
spanId: z.string(),
timestamp: z.string(),
httpStatusCode: z.string(),
httpMethod: z.string(),
httpHost: z.string(),
httpRoute: z.string(),
httpUrl: z.string(),
duration: z.string().transform(value => parseFloat(value)),
graphqlOperationName: z
.string()
.nullable()
.transform(value => value || null),
graphqlOperationType: z
.union([z.literal('QUERY'), z.literal('MUTATION'), z.literal('SUBSCRIPTION'), z.literal('')])
.transform(value => value || null),
graphqlOperationHash: z.string().nullable(),
clientName: z.string().nullable(),
clientVersion: z.string().nullable(),
graphqlErrorCodes: z.array(z.string()).nullable(),
graphqlErrorCount: z.number(),
subgraphNames: z.array(z.string()).transform(s => (s.length === 1 && s.at(0) === '' ? [] : s)),
});
export type Trace = z.TypeOf<typeof TraceModel>;
const TraceListModel = z.array(TraceModel);
type TraceFilter = {
period: null | GraphQLSchema.DateRangeInput;
duration: {
min: number | null;
max: number | null;
} | null;
traceIds: ReadonlyArray<string> | null;
success: ReadonlyArray<boolean> | null;
errorCodes: ReadonlyArray<string> | null;
operationNames: ReadonlyArray<string> | null;
operationTypes: ReadonlyArray<string | null> | null;
clientNames: ReadonlyArray<string> | null;
subgraphNames: ReadonlyArray<string> | null;
httpStatusCodes: ReadonlyArray<string> | null;
httpMethods: ReadonlyArray<string> | null;
httpHosts: ReadonlyArray<string> | null;
httpRoutes: ReadonlyArray<string> | null;
httpUrls: ReadonlyArray<string> | null;
};
function buildTraceFilterSQLConditions(filter: TraceFilter, skipPeriod: boolean) {
const ANDs: SqlValue[] = [];
if (filter?.period && !skipPeriod) {
const period = parseDateRangeInput(filter.period);
ANDs.push(
sql`"otel_traces_normalized"."timestamp" >= toDateTime(${formatDate(period.from)}, 'UTC')`,
sql`"otel_traces_normalized"."timestamp" <= toDateTime(${formatDate(period.to)}, 'UTC')`,
);
}
if (filter?.duration?.min) {
ANDs.push(sql`"duration" >= ${String(filter.duration.min * 1_000 * 1_000)}`);
}
if (filter?.duration?.max) {
ANDs.push(sql`"duration" <= ${String(filter.duration.max * 1_000 * 1_000)}`);
}
if (filter?.traceIds?.length) {
ANDs.push(sql`"trace_id" IN (${sql.array(filter.traceIds, 'String')})`);
}
// Success based on GraphQL terms
if (filter?.success?.length) {
const hasSuccess = filter.success.includes(true);
const hasError = filter.success.includes(false);
if (hasSuccess && !hasError) {
ANDs.push(
sql`"graphql_error_count" = 0`,
sql`substring("http_status_code", 1, 1) IN (${sql.array(['2', '3'], 'String')})`,
);
} else if (hasError && !hasSuccess) {
ANDs.push(
sql`"graphql_error_count" > 0`,
sql`substring("http_status_code", 1, 1) NOT IN (${sql.array(['2', '3'], 'String')})`,
);
}
}
if (filter?.errorCodes?.length) {
ANDs.push(sql`hasAny("graphql_error_codes", (${sql.array(filter.errorCodes, 'String')}))`);
}
if (filter?.operationNames?.length) {
ANDs.push(sql`"graphql_operation_name" IN (${sql.array(filter.operationNames, 'String')})`);
}
if (filter?.operationTypes?.length) {
ANDs.push(
sql`"graphql_operation_type" IN (${sql.array(
filter.operationTypes.map(value => (value == null ? '' : value.toLowerCase())),
'String',
)})`,
);
}
if (filter?.clientNames?.length) {
ANDs.push(sql`"client_name" IN (${sql.array(filter.clientNames, 'String')})`);
}
if (filter?.subgraphNames?.length) {
ANDs.push(sql`hasAny("subgraph_names", (${sql.array(filter.subgraphNames, 'String')}))`);
}
if (filter?.httpStatusCodes?.length) {
ANDs.push(sql`"http_status_code" IN (${sql.array(filter.httpStatusCodes, 'String')})`);
}
if (filter?.httpMethods?.length) {
ANDs.push(sql`"http_method" IN (${sql.array(filter.httpMethods, 'String')})`);
}
if (filter?.httpHosts?.length) {
ANDs.push(sql`"http_host" IN (${sql.array(filter.httpHosts, 'String')})`);
}
if (filter?.httpRoutes?.length) {
ANDs.push(sql`"http_route" IN (${sql.array(filter.httpRoutes, 'String')})`);
}
if (filter?.httpUrls?.length) {
ANDs.push(sql`"http_url" IN (${sql.array(filter.httpUrls, 'String')})`);
}
return ANDs;
}
const IntFromString = z.string().transform(value => parseInt(value, 10));
const TraceStatusBreakdownBucket = z.object({
timeBucketStart: z.string(),
timeBucketEnd: z.string(),
okCountTotal: IntFromString,
errorCountTotal: IntFromString,
okCountFiltered: IntFromString,
errorCountFiltered: IntFromString,
});
const TraceStatusBreakdownBucketList = z.array(TraceStatusBreakdownBucket);
const spanFields = sql`
"TraceId" AS "traceId"
, "SpanId" AS "spanId"
, "SpanName" AS "spanName"
, "ResourceAttributes" AS "resourceAttributes"
, "SpanAttributes" AS "spanAttributes"
, replaceOne(concat(toDateTime64("Timestamp", 9, 'UTC'), 'Z'), ' ', 'T') AS "startDate"
, replaceOne(concat(toDateTime64(addNanoseconds("Timestamp", "Duration"), 9, 'UTC'), 'Z'), ' ', 'T') AS "endDate"
, "Duration" AS "duration"
, "ParentSpanId" AS "parentSpanId"
, arrayMap(x -> replaceOne(concat(toDateTime64(x, 9, 'UTC'), 'Z'), ' ', 'T'),"Events.Timestamp") AS "eventsTimestamps"
, "Events.Name" AS "eventsName"
, "Events.Attributes" AS "eventsAttributes"
`;
const SpanModel = z
.object({
traceId: z.string(),
spanId: z.string(),
spanName: z.string(),
resourceAttributes: z.record(z.string(), z.unknown()),
spanAttributes: z.record(z.string(), z.unknown()),
startDate: z.string(),
endDate: z.string(),
duration: z.string().transform(value => parseFloat(value)),
parentSpanId: z.string().transform(value => value || null),
eventsTimestamps: z.array(z.string()),
eventsName: z.array(z.string()),
eventsAttributes: z.array(z.record(z.string(), z.string())),
})
.transform(({ eventsTimestamps, eventsName, eventsAttributes, ...span }) => ({
...span,
events: eventsTimestamps.map((date, index) => ({
date,
name: eventsName[index],
attributes: eventsAttributes[index],
})),
}));
const SpanListModel = z.array(SpanModel);
export type Span = z.TypeOf<typeof SpanModel>;
type BucketCandidate = {
name: string;
seconds: number;
};
const bucketCanditates: Array<BucketCandidate> = [
{
name: '1 MINUTE',
seconds: 60,
},
{
name: '5 MINUTE',
seconds: 60 * 5,
},
{ name: '1 HOUR', seconds: 60 * 60 },
{ name: '4 HOUR', seconds: 60 * 60 * 4 },
{ name: '6 HOUR', seconds: 60 * 60 * 6 },
{ name: '1 DAY', seconds: 60 * 60 * 24 },
{ name: '1 WEEK', seconds: 60 * 60 * 24 * 7 },
{ name: '1 MONTH', seconds: 60 * 60 * 24 * 30 },
];
function getBucketUnitAndCountNew(startDate: Date, endDate: Date, targetBuckets: number = 50) {
const diffSeconds = Math.floor((endDate.getTime() - startDate.getTime()) / 1000);
let best = {
candidate: bucketCanditates[0],
buckets: Math.floor(diffSeconds / bucketCanditates[0].seconds),
};
let bestDiff = Number.POSITIVE_INFINITY;
for (const candidate of bucketCanditates) {
const buckets = Math.floor(diffSeconds / candidate.seconds);
const diff = Math.abs(buckets - targetBuckets);
if (diff < bestDiff) {
bestDiff = diff;
best = {
candidate,
buckets,
};
}
}
return best;
}
/**
* All sortable fields (duration, timestamp), must be part of the cursor
*/
const PaginatedTraceCursorModel = z.object({
traceId: z.string(),
timestamp: z.string(),
duration: z.number().optional(),
});

View file

@ -0,0 +1,28 @@
import type { SpanResolvers } from './../../../__generated__/types';
/*
* Note: This object type is generated because "SpanMapper" is declared. This is to ensure runtime safety.
*
* When a mapper is used, it is possible to hit runtime errors in some scenarios:
* - given a field name, the schema type's field type does not match mapper's field type
* - or a schema type's field does not exist in the mapper's fields
*
* If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config.
*/
export const Span: SpanResolvers = {
id(span) {
return span.spanId;
},
name(span) {
return span.spanName;
},
parentId(span) {
return span.parentSpanId;
},
startTime(span) {
return span.startDate;
},
endTime(span) {
return span.endDate;
},
};

View file

@ -1,5 +1,6 @@
import { parseDateRangeInput } from '../../../shared/helpers'; import { parseDateRangeInput } from '../../../shared/helpers';
import { OperationsManager } from '../providers/operations-manager'; import { OperationsManager } from '../providers/operations-manager';
import { Traces } from '../providers/traces';
import type { TargetResolvers } from './../../../__generated__/types'; import type { TargetResolvers } from './../../../__generated__/types';
export const Target: Pick< export const Target: Pick<
@ -10,6 +11,11 @@ export const Target: Pick<
| 'requestsOverTime' | 'requestsOverTime'
| 'schemaCoordinateStats' | 'schemaCoordinateStats'
| 'totalRequests' | 'totalRequests'
| 'trace'
| 'traces'
| 'tracesFilterOptions'
| 'tracesStatusBreakdown'
| 'viewerCanAccessTraces'
> = { > = {
totalRequests: (target, { period }, { injector }) => { totalRequests: (target, { period }, { injector }) => {
return injector.get(OperationsManager).countRequests({ return injector.get(OperationsManager).countRequests({
@ -69,4 +75,66 @@ export const Target: Pick<
schemaCoordinate: args.schemaCoordinate, schemaCoordinate: args.schemaCoordinate,
}; };
}, },
traces: async (target, { first, filter, sort, after }, { injector }) => {
return injector.get(Traces).findTracesForTargetId(
target.orgId,
target.id,
first ?? null,
{
period: filter?.period ?? null,
duration: filter?.duration
? {
min: filter.duration.min ?? null,
max: filter.duration.max ?? null,
}
: null,
traceIds: filter?.traceIds ?? null,
success: filter?.success ?? null,
errorCodes: filter?.errorCodes ?? null,
operationNames: filter?.operationNames ?? null,
operationTypes: filter?.operationTypes?.map(value => value ?? null) ?? null,
clientNames: filter?.clientNames ?? null,
subgraphNames: filter?.subgraphNames ?? null,
httpMethods: filter?.httpMethods ?? null,
httpStatusCodes: filter?.httpStatusCodes ?? null,
httpHosts: filter?.httpHosts ?? null,
httpRoutes: filter?.httpRoutes ?? null,
httpUrls: filter?.httpUrls ?? null,
},
sort ?? null,
after ?? null,
);
},
tracesFilterOptions(target, { filter }, { injector }) {
return injector.get(Traces).getTraceFilterOptions(target.id, filter ?? null);
},
trace(target, args, { injector }) {
return injector.get(Traces).findTraceById(target.orgId, target.id, args.traceId);
},
tracesStatusBreakdown: async (target, { filter }, { injector }) => {
return injector.get(Traces).getTraceStatusBreakdownForTargetId(target.orgId, target.id, {
period: filter?.period ?? null,
duration: filter?.duration
? {
min: filter.duration.min ?? null,
max: filter.duration.max ?? null,
}
: null,
traceIds: filter?.traceIds ?? null,
success: filter?.success ?? null,
errorCodes: filter?.errorCodes ?? null,
operationNames: filter?.operationNames ?? null,
operationTypes: filter?.operationTypes?.map(value => value ?? null) ?? null,
clientNames: filter?.clientNames ?? null,
subgraphNames: filter?.subgraphNames ?? null,
httpMethods: filter?.httpMethods ?? null,
httpStatusCodes: filter?.httpStatusCodes ?? null,
httpHosts: filter?.httpHosts ?? null,
httpRoutes: filter?.httpRoutes ?? null,
httpUrls: filter?.httpUrls ?? null,
});
},
viewerCanAccessTraces: async (target, _, { injector }) => {
return injector.get(Traces).viewerCanAccessTraces(target.orgId);
},
}; };

View file

@ -0,0 +1,40 @@
import { Traces } from '../providers/traces';
import type { TraceResolvers } from './../../../__generated__/types';
/*
* Note: This object type is generated because "TraceMapper" is declared. This is to ensure runtime safety.
*
* When a mapper is used, it is possible to hit runtime errors in some scenarios:
* - given a field name, the schema type's field type does not match mapper's field type
* - or a schema type's field does not exist in the mapper's fields
*
* If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config.
*/
export const Trace: TraceResolvers = {
/* Implement Trace resolver logic here */
id(trace) {
return trace.traceId;
},
operationName(trace) {
return trace.graphqlOperationName;
},
operationType(trace) {
return trace.graphqlOperationType;
},
spans(trace, _arg, { injector }) {
return injector.get(Traces).findSpansForTraceId(trace.traceId, trace.targetId);
},
subgraphs(trace) {
return trace.subgraphNames;
},
success(trace) {
return (
(trace.graphqlErrorCodes?.length ?? 0) === 0 &&
trace.graphqlErrorCount === 0 &&
(trace.httpStatusCode.startsWith('2') || trace.httpStatusCode.startsWith('3'))
);
},
operationHash(trace) {
return trace.graphqlOperationHash;
},
};

View file

@ -0,0 +1,47 @@
import type { TracesFilterOptionsResolvers } from './../../../__generated__/types';
/*
* Note: This object type is generated because "TracesFilterOptionsMapper" is declared. This is to ensure runtime safety.
*
* When a mapper is used, it is possible to hit runtime errors in some scenarios:
* - given a field name, the schema type's field type does not match mapper's field type
* - or a schema type's field does not exist in the mapper's fields
*
* If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config.
*/
export const TracesFilterOptions: TracesFilterOptionsResolvers = {
/* Implement TracesFilterOptions resolver logic here */
httpHost(loader, { top }) {
return loader.httpHost(top ?? null);
},
httpMethod(loader, { top }) {
return loader.httpMethod(top ?? null);
},
httpRoute(loader, { top }) {
return loader.httpRoute(top ?? null);
},
httpStatusCode(loader, { top }) {
return loader.httpStatusCode(top ?? null);
},
httpUrl(loader, { top }) {
return loader.httpUrl(top ?? null);
},
operationName(loader, { top }) {
return loader.operationName(top ?? null);
},
operationType(loader) {
return loader.operationType();
},
subgraphs(loader, { top }) {
return loader.subgraphs(top ?? null);
},
success(loader) {
return loader.success();
},
errorCode(loader, { top }) {
return loader.errorCode(top ?? null);
},
clientName(loader, { top }) {
return loader.clientName(top ?? null);
},
};

View file

@ -98,13 +98,18 @@ export const permissionGroups: Array<PermissionGroup> = [
}, },
{ {
id: 'usage-reporting', id: 'usage-reporting',
title: 'Usage Reporting', title: 'Usage Reporting and Tracing',
permissions: [ permissions: [
{ {
id: 'usage:report', id: 'usage:report',
title: 'Report usage data', title: 'Report usage data',
description: 'Grant access to report usage data.', description: 'Grant access to report usage data.',
}, },
{
id: 'traces:report',
title: 'Report OTEL traces',
description: 'Grant access to reporting traces.',
},
], ],
}, },
{ {

View file

@ -273,6 +273,7 @@ assertAllRulesAreAssigned([
'appDeployment:publish', 'appDeployment:publish',
'appDeployment:retire', 'appDeployment:retire',
'usage:report', 'usage:report',
'traces:report',
]); ]);
/** /**

View file

@ -18,6 +18,7 @@ import {
import { IdTranslator } from '../../shared/providers/id-translator'; import { IdTranslator } from '../../shared/providers/id-translator';
import { Logger } from '../../shared/providers/logger'; import { Logger } from '../../shared/providers/logger';
import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool'; import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool';
import { Storage } from '../../shared/providers/storage';
import * as OrganizationAccessKey from '../lib/organization-access-key'; import * as OrganizationAccessKey from '../lib/organization-access-key';
import { assignablePermissions } from '../lib/organization-access-token-permissions'; import { assignablePermissions } from '../lib/organization-access-token-permissions';
import { ResourceAssignmentModel } from '../lib/resource-assignment-model'; import { ResourceAssignmentModel } from '../lib/resource-assignment-model';
@ -92,6 +93,7 @@ export class OrganizationAccessTokens {
private idTranslator: IdTranslator, private idTranslator: IdTranslator,
private session: Session, private session: Session,
private auditLogs: AuditLogRecorder, private auditLogs: AuditLogRecorder,
private storage: Storage,
logger: Logger, logger: Logger,
) { ) {
this.logger = logger.child({ this.logger = logger.child({
@ -140,9 +142,16 @@ export class OrganizationAccessTokens {
args.assignedResources ?? { mode: 'GRANULAR' }, args.assignedResources ?? { mode: 'GRANULAR' },
); );
const organization = await this.storage.getOrganization({ organizationId });
const permissions = Array.from( const permissions = Array.from(
new Set( new Set(
args.permissions.filter(permission => assignablePermissions.has(permission as Permission)), args.permissions.filter(
permission =>
assignablePermissions.has(permission as Permission) &&
// can only assign traces report permission if otel tracing feature flag is enabled in organization
(permission === 'traces:report' ? organization.featureFlags.otelTracing : true),
),
), ),
); );

View file

@ -205,12 +205,22 @@ export const Organization: Pick<
return OrganizationMemberPermissions.permissionGroups; return OrganizationMemberPermissions.permissionGroups;
}, },
availableOrganizationAccessTokenPermissionGroups: async (organization, _, { injector }) => { availableOrganizationAccessTokenPermissionGroups: async (organization, _, { injector }) => {
const permissionGroups = OrganizationAccessTokensPermissions.permissionGroups; let permissionGroups = OrganizationAccessTokensPermissions.permissionGroups;
const isAppDeploymentsEnabled = const isAppDeploymentsEnabled =
injector.get<boolean>(APP_DEPLOYMENTS_ENABLED) || organization.featureFlags.appDeployments; injector.get<boolean>(APP_DEPLOYMENTS_ENABLED) || organization.featureFlags.appDeployments;
if (!isAppDeploymentsEnabled) { if (!isAppDeploymentsEnabled) {
return permissionGroups.filter(p => p.id !== 'app-deployments'); permissionGroups = permissionGroups.filter(p => p.id !== 'app-deployments');
} }
if (!organization.featureFlags.otelTracing) {
permissionGroups = permissionGroups.map(group => ({
...group,
permissions: group.permissions.filter(p => p.id !== 'traces:report'),
}));
}
return permissionGroups; return permissionGroups;
}, },
accessTokens: async (organization, args, { injector }) => { accessTokens: async (organization, args, { injector }) => {

View file

@ -14,6 +14,20 @@ export default gql`
scalar DateTime scalar DateTime
@tag(name: "public") @tag(name: "public")
@specifiedBy(url: "https://the-guild.dev/graphql/scalars/docs/scalars/date-time") @specifiedBy(url: "https://the-guild.dev/graphql/scalars/docs/scalars/date-time")
"""
A date-time string at UTC with nano-second precision, such as '2007-12-03T10:15:30.123456Z', is compliant with the extended date-time format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. The fractional part of the seconds can range up to nanosecond precision.
This scalar represents an exact instant on the timeline, such as the precise moment a user account was created, including fractional seconds.
This scalar ignores leap seconds (thereby assuming that a minute constitutes 59 seconds). In this respect, it diverges from the RFC 3339 profile.
Where an RFC 3339 compliant date-time string has a time-zone other than UTC, it is shifted to UTC. For example, the date-time string '2016-01-01T14:10:20.500+01:00' is shifted to '2016-01-01T13:10:20.500Z'.
"""
scalar DateTime64
scalar JSONObject
scalar Date scalar Date
scalar JSON scalar JSON
scalar JSONSchemaObject scalar JSONSchemaObject

View file

@ -0,0 +1,144 @@
/**
* @source https://github.com/graphql-hive/graphql-scalars/blob/8f02889d6fb9d391f86fa761ced271c9bb8d5d6f/src/scalars/iso-date/DateTime.ts#L1
*
* Most of this code originates from there. The only modifications is to not instantiate a JS DateTime but keep the value as a string in order to retain
* nano second precission.
*/
import { GraphQLScalarType, Kind } from 'graphql';
import { createGraphQLError } from 'graphql-yoga';
const leapYear = (year: number): boolean => {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
};
const validateDate = (datestring: string): boolean => {
const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))$/;
if (!RFC_3339_REGEX.test(datestring)) {
return false;
}
// Verify the correct number of days for
// the month contained in the date-string.
const year = Number(datestring.substr(0, 4));
const month = Number(datestring.substr(5, 2));
const day = Number(datestring.substr(8, 2));
switch (month) {
case 2: // February
if ((leapYear(year) && day > 29) || (!leapYear(year) && day > 28)) {
return false;
}
return true;
case 4: // April
case 6: // June
case 9: // September
case 11: // November
if (day > 30) {
return false;
}
break;
}
return true;
};
const validateDateTime = (dateTimeString: string): boolean => {
dateTimeString = dateTimeString?.toUpperCase();
const RFC_3339_REGEX =
/^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
// Validate the structure of the date-string
if (!RFC_3339_REGEX.test(dateTimeString)) {
return false;
}
// Check if it is a correct date using the javascript Date parse() method.
const time = Date.parse(dateTimeString);
if (time !== time) {
return false;
}
// Split the date-time-string up into the string-date and time-string part.
// and check whether these parts are RFC 3339 compliant.
const index = dateTimeString.indexOf('T');
const dateString = dateTimeString.substr(0, index);
const timeString = dateTimeString.substr(index + 1);
return validateDate(dateString) && validateTime(timeString);
};
const validateTime = (time: string): boolean => {
time = time?.toUpperCase();
const TIME_REGEX =
/^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/;
return TIME_REGEX.test(time);
};
const validateJSDate = (date: Date): boolean => {
const time = date.getTime();
return time === time;
};
const parseDateTime = (dateTime: string) => dateTime;
export const DateTime64 = new GraphQLScalarType({
name: 'DateTime64',
serialize(value) {
if (value instanceof Date) {
if (validateJSDate(value)) {
return value.toISOString();
}
throw createGraphQLError('DateTime cannot represent an invalid Date instance');
} else if (typeof value === 'string') {
if (validateDateTime(value)) {
return parseDateTime(value);
}
throw createGraphQLError(`DateTime cannot represent an invalid date-time-string ${value}.`);
} else if (typeof value === 'number') {
try {
return new Date(value).toISOString();
} catch {
throw createGraphQLError('DateTime cannot represent an invalid Unix timestamp ' + value);
}
} else {
throw createGraphQLError(
'DateTime cannot be serialized from a non string, ' +
'non numeric or non Date type ' +
JSON.stringify(value),
);
}
},
parseValue(value) {
if (value instanceof Date) {
if (validateJSDate(value)) {
return value.toISOString();
}
throw createGraphQLError('DateTime cannot represent an invalid Date instance');
}
if (typeof value === 'string') {
if (validateDateTime(value)) {
return parseDateTime(value);
}
throw createGraphQLError(`DateTime cannot represent an invalid date-time-string ${value}.`);
}
throw createGraphQLError(
`DateTime cannot represent non string or Date type ${JSON.stringify(value)}`,
);
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw createGraphQLError(
`DateTime cannot represent non string or Date type ${'value' in ast && ast.value}`,
{
nodes: ast,
},
);
}
const { value } = ast;
if (validateDateTime(value)) {
return parseDateTime(value);
}
throw createGraphQLError(
`DateTime cannot represent an invalid date-time-string ${String(value)}.`,
{ nodes: ast },
);
},
});

View file

@ -0,0 +1,7 @@
import { JSONObjectResolver } from 'graphql-scalars';
// `scalar JSON` in `module.graphql.ts` does not have a description
// and it messes up the static analysis
JSONObjectResolver.description = undefined;
export const JSONObject = JSONObjectResolver;

View file

@ -190,6 +190,7 @@ export interface Organization {
*/ */
forceLegacyCompositionInTargets: string[]; forceLegacyCompositionInTargets: string[];
appDeployments: boolean; appDeployments: boolean;
otelTracing: boolean;
}; };
zendeskId: string | null; zendeskId: string | null;
/** ID of the user that owns the organization */ /** ID of the user that owns the organization */

View file

@ -25,6 +25,8 @@ import {
} from '@hive/api'; } from '@hive/api';
import { HivePubSub } from '@hive/api/modules/shared/providers/pub-sub'; import { HivePubSub } from '@hive/api/modules/shared/providers/pub-sub';
import { createRedisClient } from '@hive/api/modules/shared/providers/redis'; import { createRedisClient } from '@hive/api/modules/shared/providers/redis';
import { TargetsByIdCache } from '@hive/api/modules/target/providers/targets-by-id-cache';
import { TargetsBySlugCache } from '@hive/api/modules/target/providers/targets-by-slug-cache';
import { createArtifactRequestHandler } from '@hive/cdn-script/artifact-handler'; import { createArtifactRequestHandler } from '@hive/cdn-script/artifact-handler';
import { ArtifactStorageReader } from '@hive/cdn-script/artifact-storage-reader'; import { ArtifactStorageReader } from '@hive/cdn-script/artifact-storage-reader';
import { AwsClient } from '@hive/cdn-script/aws'; import { AwsClient } from '@hive/cdn-script/aws';
@ -65,6 +67,7 @@ import { asyncStorage } from './async-storage';
import { env } from './environment'; import { env } from './environment';
import { graphqlHandler } from './graphql-handler'; import { graphqlHandler } from './graphql-handler';
import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics'; import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics';
import { createOtelAuthEndpoint } from './otel-auth-endpoint';
import { createPublicGraphQLHandler } from './public-graphql-handler'; import { createPublicGraphQLHandler } from './public-graphql-handler';
import { initSupertokens, oidcIdLookup } from './supertokens'; import { initSupertokens, oidcIdLookup } from './supertokens';
@ -459,6 +462,10 @@ export async function main() {
handler: graphql, handler: graphql,
}); });
const authN = new AuthN({
strategies: [organizationAccessTokenStrategy],
});
server.route({ server.route({
method: ['GET', 'POST'], method: ['GET', 'POST'],
url: '/graphql-public', url: '/graphql-public',
@ -466,9 +473,7 @@ export async function main() {
registry, registry,
logger: logger as any, logger: logger as any,
hiveUsageConfig: env.hive, hiveUsageConfig: env.hive,
authN: new AuthN({ authN,
strategies: [organizationAccessTokenStrategy],
}),
tracing, tracing,
}), }),
}); });
@ -592,6 +597,13 @@ export async function main() {
return; return;
}); });
createOtelAuthEndpoint({
server,
authN,
targetsBySlugCache: registry.injector.get(TargetsBySlugCache),
targetsByIdCache: registry.injector.get(TargetsByIdCache),
});
if (env.cdn.providers.api !== null) { if (env.cdn.providers.api !== null) {
const s3 = { const s3 = {
client: new AwsClient({ client: new AwsClient({

View file

@ -0,0 +1,119 @@
import type { FastifyInstance } from 'fastify';
import type { AuthN } from '@hive/api/modules/auth/lib/authz';
import type { TargetsByIdCache } from '@hive/api/modules/target/providers/targets-by-id-cache';
import type { TargetsBySlugCache } from '@hive/api/modules/target/providers/targets-by-slug-cache';
import { isUUID } from '@hive/api/shared/is-uuid';
export function createOtelAuthEndpoint(args: {
server: FastifyInstance;
authN: AuthN;
targetsByIdCache: TargetsByIdCache;
targetsBySlugCache: TargetsBySlugCache;
}) {
args.server.get('/otel-auth', async (req, reply) => {
const targetRefHeader = req.headers['x-hive-target-ref'];
const targetRefRaw = Array.isArray(targetRefHeader) ? targetRefHeader[0] : targetRefHeader;
if (typeof targetRefRaw !== 'string' || targetRefRaw.trim().length === 0) {
await reply.status(400).send({
message: `Missing required header: 'X-Hive-Target-Ref'. Please provide a valid target reference in the request headers.`,
});
return;
}
const targetRefParseResult = parseTargetRef(targetRefRaw);
if (!targetRefParseResult.ok) {
await reply.status(400).send({
message: targetRefParseResult.error,
});
return;
}
const targetRef = targetRefParseResult.data;
const session = await args.authN.authenticate({ req, reply });
const target = await (targetRef.kind === 'id'
? args.targetsByIdCache.get(targetRef.targetId)
: args.targetsBySlugCache.get(targetRef));
if (!target) {
await reply.status(404).send({
message: `The specified target does not exist. Verify the target reference and try again.`,
});
return;
}
const canReportUsage = await session.canPerformAction({
organizationId: target.orgId,
action: 'traces:report',
params: {
organizationId: target.orgId,
projectId: target.projectId,
targetId: target.id,
},
});
if (!canReportUsage) {
await reply.status(403).send({
message: `You do not have permission to send traces for this target.`,
});
return;
}
await reply.status(200).send({
message: 'Authenticated',
targetId: target.id,
});
return;
});
}
// TODO: https://github.com/open-telemetry/opentelemetry-collector/blob/ae0b83b94cc4d4cd90a73a2f390d23c25f848aec/config/confighttp/confighttp.go#L551C4-L551C84
// swallows the error and returns 401 Unauthorized to the OTel SDK.
const invalidTargetRefError =
'Invalid slug or ID provided for target reference. ' +
'Must match target slug "$organization_slug/$project_slug/$target_slug" (e.g. "the-guild/graphql-hive/staging") ' +
'or UUID (e.g. c8164307-0b42-473e-a8c5-2860bb4beff6).';
function parseTargetRef(targetRef: string) {
if (targetRef.includes('/')) {
const parts = targetRef.split('/');
if (parts.length !== 3) {
return {
ok: false,
error: invalidTargetRefError,
} as const;
}
const [organizationSlug, projectSlug, targetSlug] = parts;
return {
ok: true,
data: {
kind: 'slugs',
organizationSlug,
projectSlug,
targetSlug,
},
} as const;
}
if (!isUUID(targetRef)) {
return {
ok: false,
error: invalidTargetRefError,
} as const;
}
return {
ok: true,
data: {
kind: 'id',
targetId: targetRef,
},
} as const;
}

View file

@ -4737,6 +4737,8 @@ const FeatureFlagsModel = zod
forceLegacyCompositionInTargets: zod.array(zod.string()).default([]), forceLegacyCompositionInTargets: zod.array(zod.string()).default([]),
/** whether app deployments are enabled for the given organization */ /** whether app deployments are enabled for the given organization */
appDeployments: zod.boolean().default(false), appDeployments: zod.boolean().default(false),
/** whether otel tracing is enabled for the given organization */
otelTracing: zod.boolean().default(false),
}) })
.optional() .optional()
.nullable() .nullable()
@ -4747,6 +4749,7 @@ const FeatureFlagsModel = zod
compareToPreviousComposableVersion: false, compareToPreviousComposableVersion: false,
forceLegacyCompositionInTargets: [], forceLegacyCompositionInTargets: [],
appDeployments: false, appDeployments: false,
otelTracing: false,
}, },
); );

View file

@ -72,6 +72,7 @@
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/dompurify": "3.2.0", "@types/dompurify": "3.2.0",
"@types/js-cookie": "3.0.6", "@types/js-cookie": "3.0.6",
"@types/lodash.debounce": "4.0.9",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"@types/react-highlight-words": "0.20.0", "@types/react-highlight-words": "0.20.0",
@ -81,12 +82,14 @@
"@urql/exchange-auth": "2.2.0", "@urql/exchange-auth": "2.2.0",
"@urql/exchange-graphcache": "7.1.0", "@urql/exchange-graphcache": "7.1.0",
"@vitejs/plugin-react": "4.3.4", "@vitejs/plugin-react": "4.3.4",
"@xyflow/react": "12.4.4",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
"clsx": "2.1.1", "clsx": "2.1.1",
"cmdk": "0.2.1", "cmdk": "0.2.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
"dompurify": "3.2.6", "dompurify": "3.2.6",
"dotenv": "16.4.7", "dotenv": "16.4.7",
"echarts": "5.6.0", "echarts": "5.6.0",
@ -101,10 +104,13 @@
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"json-schema-typed": "8.0.1", "json-schema-typed": "8.0.1",
"json-schema-yup-transformer": "1.6.12", "json-schema-yup-transformer": "1.6.12",
"jsurl2": "2.2.0",
"lodash.debounce": "4.0.8",
"lucide-react": "0.469.0", "lucide-react": "0.469.0",
"mini-svg-data-uri": "1.4.4", "mini-svg-data-uri": "1.4.4",
"monaco-editor": "0.50.0", "monaco-editor": "0.50.0",
"monaco-themes": "0.4.4", "monaco-themes": "0.4.4",
"query-string": "9.1.1",
"react": "18.3.1", "react": "18.3.1",
"react-day-picker": "8.10.1", "react-day-picker": "8.10.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@ -112,6 +118,7 @@
"react-highlight-words": "0.20.0", "react-highlight-words": "0.20.0",
"react-hook-form": "7.54.2", "react-hook-form": "7.54.2",
"react-icons": "5.4.0", "react-icons": "5.4.0",
"react-resizable-panels": "2.1.7",
"react-select": "5.9.0", "react-select": "5.9.0",
"react-string-replace": "1.1.1", "react-string-replace": "1.1.1",
"react-textarea-autosize": "8.5.9", "react-textarea-autosize": "8.5.9",
@ -119,6 +126,7 @@
"react-virtualized-auto-sizer": "1.0.25", "react-virtualized-auto-sizer": "1.0.25",
"react-virtuoso": "4.12.3", "react-virtuoso": "4.12.3",
"react-window": "1.8.11", "react-window": "1.8.11",
"recharts": "2.15.1",
"regenerator-runtime": "0.14.1", "regenerator-runtime": "0.14.1",
"snarkdown": "2.0.0", "snarkdown": "2.0.0",
"storybook": "8.4.7", "storybook": "8.4.7",

View file

@ -0,0 +1,18 @@
import ghost from '../../../public/images/figures/ghost.svg?url';
import { useRouter } from '@tanstack/react-router';
import { Button } from '../ui/button';
export function NotFoundContent(props: { heading: React.ReactNode; subheading: React.ReactNode }) {
const router = useRouter();
return (
<div className="flex h-full flex-1 flex-col items-center justify-center gap-2.5 py-6">
<img src={ghost} alt="Ghost illustration" width="200" height="200" className="drag-none" />
<h2 className="text-xl font-bold">{props.heading}</h2>
<h3 className="font-semibold">{props.subheading}</h3>
<Button variant="secondary" className="mt-2" onClick={router.history.back}>
Go back
</Button>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { ReactElement, ReactNode, useMemo, useState } from 'react'; import { createContext, ReactElement, ReactNode, useContext, useMemo, useState } from 'react';
import { LinkIcon } from 'lucide-react'; import { LinkIcon } from 'lucide-react';
import { useQuery } from 'urql'; import { useQuery } from 'urql';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -39,11 +39,48 @@ export enum Page {
Checks = 'checks', Checks = 'checks',
History = 'history', History = 'history',
Insights = 'insights', Insights = 'insights',
Traces = 'traces',
Laboratory = 'laboratory', Laboratory = 'laboratory',
Apps = 'apps', Apps = 'apps',
Settings = 'settings', Settings = 'settings',
} }
type TargetReference = {
organizationSlug: string;
projectSlug: string;
targetSlug: string;
};
const TargetReferenceContext = createContext<TargetReference | undefined>(undefined);
type TargetReferenceProviderProps = {
children: ReactNode;
organizationSlug: string;
projectSlug: string;
targetSlug: string;
};
export const TargetReferenceProvider = ({
children,
organizationSlug,
projectSlug,
targetSlug,
}: TargetReferenceProviderProps) => {
return (
<TargetReferenceContext.Provider value={{ organizationSlug, projectSlug, targetSlug }}>
{children}
</TargetReferenceContext.Provider>
);
};
export const useTargetReference = () => {
const context = useContext(TargetReferenceContext);
if (!context) {
throw new Error('useTargetReference must be used within a TargetReferenceProvider');
}
return context;
};
const TargetLayoutQuery = graphql(` const TargetLayoutQuery = graphql(`
query TargetLayoutQuery($organizationSlug: String!, $projectSlug: String!, $targetSlug: String!) { query TargetLayoutQuery($organizationSlug: String!, $projectSlug: String!, $targetSlug: String!) {
me { me {
@ -67,6 +104,7 @@ const TargetLayoutQuery = graphql(`
viewerCanViewLaboratory viewerCanViewLaboratory
viewerCanViewAppDeployments viewerCanViewAppDeployments
viewerCanAccessSettings viewerCanAccessSettings
viewerCanAccessTraces
latestSchemaVersion { latestSchemaVersion {
id id
} }
@ -113,7 +151,11 @@ export const TargetLayout = ({
useLastVisitedOrganizationWriter(currentOrganization?.slug); useLastVisitedOrganizationWriter(currentOrganization?.slug);
return ( return (
<> <TargetReferenceProvider
organizationSlug={props.organizationSlug}
projectSlug={props.projectSlug}
targetSlug={props.targetSlug}
>
<header> <header>
<div className="container flex h-[--header-height] items-center justify-between"> <div className="container flex h-[--header-height] items-center justify-between">
<div className="flex flex-row items-center gap-4"> <div className="flex flex-row items-center gap-4">
@ -207,6 +249,20 @@ export const TargetLayout = ({
Insights Insights
</Link> </Link>
</TabsTrigger> </TabsTrigger>
{currentTarget.viewerCanAccessTraces && (
<TabsTrigger variant="menu" value={Page.Traces} asChild>
<Link
to="/$organizationSlug/$projectSlug/$targetSlug/traces"
params={{
organizationSlug: props.organizationSlug,
projectSlug: props.projectSlug,
targetSlug: props.targetSlug,
}}
>
Traces
</Link>
</TabsTrigger>
)}
{currentTarget.viewerCanViewAppDeployments && ( {currentTarget.viewerCanViewAppDeployments && (
<TabsTrigger variant="menu" value={Page.Apps} asChild> <TabsTrigger variant="menu" value={Page.Apps} asChild>
<Link <Link
@ -284,7 +340,7 @@ export const TargetLayout = ({
</div> </div>
</> </>
)} )}
</> </TargetReferenceProvider>
); );
}; };

View file

@ -23,6 +23,7 @@ const buttonVariants = cva(
lg: 'h-11 px-8 rounded-md', lg: 'h-11 px-8 rounded-md',
icon: 'size-10', icon: 'size-10',
'icon-sm': 'size-7', 'icon-sm': 'size-7',
'icon-xs': 'size-4',
}, },
}, },
defaultVariants: { defaultVariants: {

View file

@ -0,0 +1,326 @@
'use client';
import React from 'react';
import * as RechartsPrimitive from 'recharts';
import { cn } from '@/lib/utils';
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error('useChart must be used within a <ChartContainer />');
}
return context;
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
config: ChartConfig;
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className,
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
});
ChartContainer.displayName = 'Chart';
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join('\n')}
}
`,
)
.join('\n'),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<'div'> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: 'line' | 'dot' | 'dashed';
nameKey?: string;
labelKey?: string;
}
>(
(
{
active,
payload,
className,
indicator = 'dot',
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref,
) => {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === 'string'
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>
);
}
if (!value) {
return null;
}
return <div className={cn('font-medium', labelClassName)}>{value}</div>;
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== 'dot';
return (
<div
ref={ref}
className={cn(
'border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
className,
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5',
indicator === 'dot' && 'items-center',
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
{
'h-2.5 w-2.5': indicator === 'dot',
'w-1': indicator === 'line',
'w-0 border-[1.5px] border-dashed bg-transparent':
indicator === 'dashed',
'my-0.5': nestLabel && indicator === 'dashed',
},
)}
style={
{
'--color-bg': indicatorColor,
'--color-border': indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
'flex flex-1 justify-between leading-none',
nestLabel ? 'items-end' : 'items-center',
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
},
);
ChartTooltipContent.displayName = 'ChartTooltip';
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> &
Pick<RechartsPrimitive.LegendProps, 'payload' | 'verticalAlign'> & {
hideIcon?: boolean;
nameKey?: string;
}
>(({ className, hideIcon = false, payload, verticalAlign = 'bottom', nameKey }, ref) => {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
ref={ref}
className={cn(
'flex items-center justify-center gap-4',
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
className,
)}
>
{payload.map(item => {
const key = `${nameKey || item.dataKey || 'value'}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn('[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:size-3')}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="size-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
});
ChartLegendContent.displayName = 'ChartLegend';
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== 'object' || payload === null) {
return undefined;
}
const payloadPayload =
'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { endOfDay, endOfToday, subMonths } from 'date-fns'; import { endOfDay, endOfToday, formatDate, subMonths } from 'date-fns';
import { CalendarDays } from 'lucide-react'; import { CalendarDays } from 'lucide-react';
import { DateRange, Matcher } from 'react-day-picker'; import { DateRange, Matcher } from 'react-day-picker';
import { DurationUnit, formatDateToString, parse, units } from '@/lib/date-math'; import { DurationUnit, formatDateToString, parse, units } from '@/lib/date-math';
@ -32,13 +32,6 @@ export interface DateRangePickerProps {
validUnits?: DurationUnit[]; validUnits?: DurationUnit[];
} }
const formatDate = (date: Date, locale = 'en-us'): string => {
return date.toLocaleDateString(locale, {
month: 'short',
day: 'numeric',
});
};
interface ResolvedDateRange { interface ResolvedDateRange {
from: Date; from: Date;
to: Date; to: Date;
@ -50,8 +43,17 @@ export type Preset = {
range: { from: string; to: string }; range: { from: string; to: string };
}; };
export function buildDateRangeString(range: ResolvedDateRange, locale = 'en-us'): string { export function buildDateRangeString(range: ResolvedDateRange): string {
return `${formatDate(range.from, locale)} - ${formatDate(range.to, locale)}`; const fromDate = formatDate(range.from, 'MMM d');
const fromTime = formatDate(range.from, 'HH:mm');
const toDate = formatDate(range.to, 'MMM d');
const toTime = formatDate(range.to, 'HH:mm');
if (fromDate === toDate) {
return `${fromDate}, ${fromTime} - ${toTime}`;
}
return `${fromDate}, ${fromTime} - ${toDate}, ${toTime}`;
} }
function resolveRange(rawFrom: string, rawTo: string): ResolvedDateRange | null { function resolveRange(rawFrom: string, rawTo: string): ResolvedDateRange | null {
@ -64,16 +66,6 @@ function resolveRange(rawFrom: string, rawTo: string): ResolvedDateRange | null
return null; return null;
} }
function calculateWeight(preset: Preset): number {
const from = parse(preset.range.from);
const to = parse(preset.range.to);
if (from && to) {
const durationInMinutes = Math.round((to.getTime() - from.getTime()) / (1000 * 60));
return durationInMinutes;
}
return 0;
}
export const presetLast7Days: Preset = { export const presetLast7Days: Preset = {
name: 'last7d', name: 'last7d',
label: 'Last 7 days', label: 'Last 7 days',
@ -88,8 +80,12 @@ export const presetLast1Day: Preset = {
// Define presets // Define presets
export const availablePresets: Preset[] = [ export const availablePresets: Preset[] = [
{ name: 'last15m', label: 'Last 15 minutes', range: { from: 'now-15m', to: 'now' } },
{ name: 'last30m', label: 'Last 30 minutes', range: { from: 'now-30m', to: 'now' } }, { name: 'last30m', label: 'Last 30 minutes', range: { from: 'now-30m', to: 'now' } },
{ name: 'last1h', label: 'Last 1 hour', range: { from: 'now-1h', to: 'now' } }, { name: 'last1h', label: 'Last 1 hour', range: { from: 'now-1h', to: 'now' } },
{ name: 'last3h', label: 'Last 3 hours', range: { from: 'now-3h', to: 'now' } },
{ name: 'last6h', label: 'Last 6 hours', range: { from: 'now-6h', to: 'now' } },
{ name: 'last12h', label: 'Last 12 hours', range: { from: 'now-12h', to: 'now' } },
presetLast1Day, presetLast1Day,
presetLast7Days, presetLast7Days,
{ name: 'last14d', label: 'Last 14 days', range: { from: 'now-14d', to: 'now' } }, { name: 'last14d', label: 'Last 14 days', range: { from: 'now-14d', to: 'now' } },
@ -99,7 +95,60 @@ export const availablePresets: Preset[] = [
{ name: 'last1y', label: 'Last 1 year', range: { from: 'now-364d', to: 'now' } }, { name: 'last1y', label: 'Last 1 year', range: { from: 'now-364d', to: 'now' } },
]; ];
function findMatchingPreset(range: Preset['range']): Preset | undefined { function createQuickRangePresets(number: number, validUnits: DurationUnit[]): Preset[] {
const presets: Preset[] = [];
if (validUnits.includes('m')) {
presets.push({
name: `last${number}min`,
label: `Last ${number} minutes`,
range: { from: `now-${number}m`, to: 'now' },
});
}
if (validUnits.includes('h')) {
presets.push({
name: `last${number}h`,
label: `Last ${number} hours`,
range: { from: `now-${number}h`, to: 'now' },
});
}
if (validUnits.includes('d')) {
presets.push({
name: `last${number}d`,
label: `Last ${number} days`,
range: { from: `now-${number}d`, to: 'now' },
});
}
if (validUnits.includes('w')) {
presets.push({
name: `last${number}w`,
label: `Last ${number} weeks`,
range: { from: `now-${number}w`, to: 'now' },
});
}
if (validUnits.includes('M')) {
presets.push({
name: `last${number}M`,
label: `Last ${number} months`,
range: { from: `now-${number}M`, to: 'now' },
});
}
if (validUnits.includes('y')) {
presets.push({
name: `last${number}y`,
label: `Last ${number} years`,
range: { from: `now-${number}y`, to: 'now' },
});
}
return presets;
}
export function findMatchingPreset(
range: Preset['range'],
availablePresets: Preset[],
): Preset | undefined {
return availablePresets.find(preset => { return availablePresets.find(preset => {
return preset.range.from === range.from && preset.range.to === range.to; return preset.range.from === range.from && preset.range.to === range.to;
}); });
@ -113,10 +162,10 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
? new RegExp(`[0-9]+(${disallowedUnits.join('|')})`) ? new RegExp(`[0-9]+(${disallowedUnits.join('|')})`)
: null; : null;
let presets = props.presets ?? availablePresets; let staticPresets = props.presets ?? availablePresets;
if (hasInvalidUnitRegex) { if (hasInvalidUnitRegex) {
presets = presets.filter( staticPresets = staticPresets.filter(
preset => preset =>
!hasInvalidUnitRegex.test(preset.range.from) && !hasInvalidUnitRegex.test(preset.range.to), !hasInvalidUnitRegex.test(preset.range.from) && !hasInvalidUnitRegex.test(preset.range.to),
); );
@ -138,34 +187,59 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
const [showCalendar, setShowCalendar] = useState(false); const [showCalendar, setShowCalendar] = useState(false);
function getInitialPreset() { function getInitialPreset() {
let preset: Preset | undefined; const fallbackPreset = staticPresets.at(0) ?? null;
if ( if (
props.selectedRange && !props.selectedRange ||
!hasInvalidUnitRegex?.test(props.selectedRange.from) && hasInvalidUnitRegex?.test(props.selectedRange.from) ||
!hasInvalidUnitRegex?.test(props.selectedRange.to) hasInvalidUnitRegex?.test(props.selectedRange.to)
) { ) {
preset = findMatchingPreset(props.selectedRange); return fallbackPreset;
}
if (preset) { // Attempt to find preset from out pre-defined presets first
return preset; const preset = findMatchingPreset(props.selectedRange, staticPresets);
}
const resolvedRange = resolveRange(props.selectedRange.from, props.selectedRange.to); if (preset) {
if (resolvedRange) { return preset;
return { }
name: `${props.selectedRange.from}_${props.selectedRange.to}`,
label: buildDateRangeString(resolvedRange), // attempt to find the preset based on dynamic presets (so we show something like "last x days" instead of 10. September - 12.September for `now-2d`)
range: props.selectedRange, if (props.selectedRange.from.startsWith('now-')) {
}; const number = parseInt(props.selectedRange.from.replace(/\D/g, ''), 10);
if (!Number.isNaN(number)) {
const quickRangPresets = createQuickRangePresets(number, validUnits);
const preset = quickRangPresets.find(
preset =>
preset.range.from === props.selectedRange?.from &&
preset.range.to === props.selectedRange.to,
);
if (preset) {
return preset;
}
} }
} }
return presets.at(0) ?? null; // if everything else fails we show an absolute range!
const resolvedRange = resolveRange(props.selectedRange.from, props.selectedRange.to);
if (resolvedRange) {
return {
name: `${props.selectedRange.from}_${props.selectedRange.to}`,
label: buildDateRangeString(resolvedRange),
range: props.selectedRange,
};
}
return fallbackPreset;
} }
const [activePreset, setActivePreset] = useResetState<Preset | null>(getInitialPreset, [ const [activePreset, setActivePreset] = useResetState<Preset | null>(getInitialPreset, [
props.selectedRange, props.selectedRange,
]); ]);
const [fromValue, setFromValue] = useState(activePreset?.range.from ?? ''); const [fromValue, setFromValue] = useState(activePreset?.range.from ?? '');
const [toValue, setToValue] = useState(activePreset?.range.to ?? ''); const [toValue, setToValue] = useState(activePreset?.range.to ?? '');
const [range, setRange] = useState<DateRange | undefined>(undefined); const [range, setRange] = useState<DateRange | undefined>(undefined);
@ -202,74 +276,56 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
setActivePreset(getInitialPreset()); setActivePreset(getInitialPreset());
}; };
const PresetButton = ({ preset }: { preset: Preset }): JSX.Element => { const PresetButton = useMemo(
let isDisabled = false; () =>
function PresetButton({ preset }: { preset: Preset }): React.ReactNode {
let isDisabled = false;
if (props.startDate) { if (props.startDate) {
const from = parse(preset.range.from); const from = parse(preset.range.from);
if (from && from.getTime() < props.startDate.getTime()) { const time = from?.getTime();
isDisabled = true; const startTime = props.startDate?.getTime();
}
}
return ( if (
<Button !time ||
variant="ghost" !startTime ||
onClick={() => { Number.isNaN(time) ||
setActivePreset(preset); Number.isNaN(startTime) ||
setFromValue(preset.range.from); time < props.startDate.getTime()
setToValue(preset.range.to); ) {
setRange(undefined); isDisabled = true;
setShowCalendar(false); }
setIsOpen(false); }
setQuickRangeFilter('');
}}
disabled={isDisabled}
className="w-full justify-start text-left"
>
{preset.label}
</Button>
);
};
const [dynamicPresets, setDynamicPresets] = useState<Preset[]>([]); return (
useEffect(() => { <Button
variant="ghost"
onClick={() => {
setActivePreset(preset);
setFromValue(preset.range.from);
setToValue(preset.range.to);
setRange(undefined);
setShowCalendar(false);
setIsOpen(false);
setQuickRangeFilter('');
}}
disabled={isDisabled}
className="w-full justify-start text-left"
>
{preset.label}
</Button>
);
},
[props.startDate],
);
const dynamicPresets = useMemo(() => {
const number = parseInt(quickRangeFilter.replace(/\D/g, ''), 10); const number = parseInt(quickRangeFilter.replace(/\D/g, ''), 10);
const dynamicPresets: Preset[] = [
{ const dynamicPresets = createQuickRangePresets(number, validUnits);
name: `last${number}min`,
label: `Last ${number} minutes`,
range: { from: `now-${number}m`, to: 'now' },
},
{
name: `last${number}h`,
label: `Last ${number} hours`,
range: { from: `now-${number}h`, to: 'now' },
},
{
name: `last${number}d`,
label: `Last ${number} days`,
range: { from: `now-${number}d`, to: 'now' },
},
{
name: `last${number}w`,
label: `Last ${number} weeks`,
range: { from: `now-${number}w`, to: 'now' },
},
{
name: `last${number}M`,
label: `Last ${number} months`,
range: { from: `now-${number}M`, to: 'now' },
},
{
name: `last${number}y`,
label: `Last ${number} years`,
range: { from: `now-${number}y`, to: 'now' },
},
];
const uniqueDynamicPresets = dynamicPresets.filter( const uniqueDynamicPresets = dynamicPresets.filter(
preset => !presets.some(p => p.name === preset.name), preset => !staticPresets.some(p => p.name === preset.name),
); );
const validDynamicPresets = uniqueDynamicPresets.filter( const validDynamicPresets = uniqueDynamicPresets.filter(
@ -279,17 +335,11 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
); );
if (number > 0 && validDynamicPresets.length > 0) { if (number > 0 && validDynamicPresets.length > 0) {
setDynamicPresets(validDynamicPresets); return validDynamicPresets;
} else {
setDynamicPresets([]);
} }
}, [quickRangeFilter, validUnits]);
presets = [...presets, ...dynamicPresets].sort((a, b) => {
const aWeight = calculateWeight(a);
const bWeight = calculateWeight(b);
return aWeight - bWeight; return [];
}); }, [quickRangeFilter, validUnits]);
return ( return (
<Popover <Popover
@ -311,25 +361,34 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align={props.align} className="mt-1 flex h-[380px] w-auto p-0"> <PopoverContent align={props.align} className="mt-1 flex h-[380px] w-auto p-0">
<div className="flex flex-col py-4"> <div className="flex flex-col py-2">
<div className="flex flex-col items-center justify-end gap-2 lg:flex-row lg:items-start"> <div className="flex flex-col items-center justify-end gap-2 lg:flex-row lg:items-start">
<div className="flex flex-col gap-1 pl-3"> <div className="flex flex-col gap-1 pl-3">
<div className="mb-2 font-bold">Absolute date range</div> <div className="mb-2 text-sm">Absolute date range</div>
<div className="space-y-2"> <div className="space-y-2">
<div className="grid w-full max-w-sm items-center gap-1.5"> <div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="from">From</Label> <Label htmlFor="from" className="text-xs text-gray-400">
From
</Label>
<div className="flex w-full max-w-sm items-center space-x-2"> <div className="flex w-full max-w-sm items-center space-x-2">
<Input <div className="relative flex w-full">
type="text" <Input
id="from" type="text"
value={fromValue} id="from"
onChange={ev => { value={fromValue}
setFromValue(ev.target.value); onChange={ev => {
}} setFromValue(ev.target.value);
/> }}
<Button size="icon" variant="outline" onClick={() => setShowCalendar(true)}> className="font-mono"
<CalendarDays className="size-4" /> />
</Button> <Button
variant="ghost"
className="absolute right-2 top-1/2 size-6 -translate-y-1/2 px-0"
onClick={() => setShowCalendar(true)}
>
<CalendarDays className="size-3.5" />
</Button>
</div>
</div> </div>
<div className="text-red-500"> <div className="text-red-500">
{hasInvalidUnitRegex?.test(fromValue) ? ( {hasInvalidUnitRegex?.test(fromValue) ? (
@ -340,19 +399,28 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
</div> </div>
</div> </div>
<div className="grid w-full max-w-sm items-center gap-1.5"> <div className="grid w-full max-w-sm items-center gap-1.5">
<Label htmlFor="to">To</Label> <Label htmlFor="to" className="text-xs text-gray-400">
To
</Label>
<div className="flex w-full max-w-sm items-center space-x-2"> <div className="flex w-full max-w-sm items-center space-x-2">
<Input <div className="relative flex w-full">
type="text" <Input
id="to" type="text"
value={toValue} id="to"
onChange={ev => { value={toValue}
setToValue(ev.target.value); onChange={ev => {
}} setToValue(ev.target.value);
/> }}
<Button size="icon" variant="outline" onClick={() => setShowCalendar(true)}> className="font-mono"
<CalendarDays className="size-4" /> />
</Button> <Button
variant="ghost"
className="absolute right-2 top-1/2 size-6 -translate-y-1/2 px-0"
onClick={() => setShowCalendar(true)}
>
<CalendarDays className="size-3.5" />
</Button>
</div>
</div> </div>
<div className="text-red-500"> <div className="text-red-500">
{hasInvalidUnitRegex?.test(toValue) ? ( {hasInvalidUnitRegex?.test(toValue) ? (
@ -374,10 +442,13 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
if (resolvedRange) { if (resolvedRange) {
setActivePreset( setActivePreset(
() => () =>
findMatchingPreset({ findMatchingPreset(
from: fromWithoutWhitespace, {
to: toWithoutWhitespace, from: fromWithoutWhitespace,
}) ?? { to: toWithoutWhitespace,
},
availablePresets,
) ?? {
name: `${fromWithoutWhitespace}_${toWithoutWhitespace}`, name: `${fromWithoutWhitespace}_${toWithoutWhitespace}`,
label: buildDateRangeString(resolvedRange), label: buildDateRangeString(resolvedRange),
range: { from: fromWithoutWhitespace, to: toWithoutWhitespace }, range: { from: fromWithoutWhitespace, to: toWithoutWhitespace },
@ -418,7 +489,7 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
preset.label.toLowerCase().includes(quickRangeFilter.toLowerCase().trim()), preset.label.toLowerCase().includes(quickRangeFilter.toLowerCase().trim()),
) )
.map(preset => <PresetButton key={preset.name} preset={preset} />) .map(preset => <PresetButton key={preset.name} preset={preset} />)
: presets : staticPresets
.filter(preset => .filter(preset =>
preset.label.toLowerCase().includes(quickRangeFilter.toLowerCase().trim()), preset.label.toLowerCase().includes(quickRangeFilter.toLowerCase().trim()),
) )
@ -426,7 +497,7 @@ export function DateRangePicker(props: DateRangePickerProps): JSX.Element {
</div> </div>
</div> </div>
{showCalendar && ( {showCalendar && (
<div className="absolute left-0 top-0 -translate-x-full"> <div className="absolute left-0 top-[4px] -translate-x-full">
<div className="bg-popover mr-1 rounded-md border p-4"> <div className="bg-popover mr-1 rounded-md border p-4">
<Button <Button
variant="ghost" variant="ghost"

View file

@ -57,10 +57,11 @@ type SubPageLayoutHeaderProps = {
children?: ReactNode; children?: ReactNode;
subPageTitle?: ReactNode; subPageTitle?: ReactNode;
description?: string | ReactNode; description?: string | ReactNode;
sideContent?: ReactNode;
} & HTMLAttributes<HTMLDivElement>; } & HTMLAttributes<HTMLDivElement>;
const SubPageLayoutHeader = forwardRef<HTMLDivElement, SubPageLayoutHeaderProps>((props, ref) => ( const SubPageLayoutHeader = forwardRef<HTMLDivElement, SubPageLayoutHeaderProps>((props, ref) => {
<div className="flex flex-row items-center justify-between" ref={ref}> const header = (
<div className="space-y-1.5"> <div className="space-y-1.5">
<CardTitle>{props.subPageTitle}</CardTitle> <CardTitle>{props.subPageTitle}</CardTitle>
{typeof props.description === 'string' ? ( {typeof props.description === 'string' ? (
@ -69,9 +70,21 @@ const SubPageLayoutHeader = forwardRef<HTMLDivElement, SubPageLayoutHeaderProps>
props.description props.description
)} )}
</div> </div>
<div>{props.children}</div> );
</div> return (
)); <div className="flex flex-row items-center justify-between" ref={ref}>
{props.sideContent ? (
<div className="flex w-full">
{header}
{props.sideContent}
</div>
) : (
header
)}
<div>{props.children}</div>
</div>
);
});
SubPageLayoutHeader.displayName = 'SubPageLayoutHeader'; SubPageLayoutHeader.displayName = 'SubPageLayoutHeader';
export { PageLayout, NavLayout, PageLayoutContent, SubPageLayout, SubPageLayoutHeader }; export { PageLayout, NavLayout, PageLayoutContent, SubPageLayout, SubPageLayoutHeader };

View file

@ -0,0 +1,39 @@
import { GripVertical } from 'lucide-react';
import * as ResizablePrimitive from 'react-resizable-panels';
import { cn } from '@/lib/utils';
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn('flex size-full data-[panel-group-direction=vertical]:flex-col', className)}
{...props}
/>
);
const ResizablePanel = ResizablePrimitive.Panel;
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
className,
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-sm border">
<GripVertical className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View file

@ -11,7 +11,8 @@ const ScrollArea = React.forwardRef<
className={cn('relative overflow-hidden', className)} className={cn('relative overflow-hidden', className)}
{...props} {...props}
> >
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]"> {/** LOL https://github.com/radix-ui/primitives/issues/2722 */}
<ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit] [&>div]:!block">
{children} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
<ScrollBar /> <ScrollBar />

View file

@ -49,14 +49,16 @@ const sheetVariants = cva(
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {} VariantProps<typeof sheetVariants> {
noOverlay?: boolean;
}
const SheetContent = React.forwardRef< const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>, React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => ( >(({ side = 'right', className, children, noOverlay, ...props }, ref) => (
<SheetPortal> <SheetPortal>
<SheetOverlay /> {noOverlay ? null : <SheetOverlay />}
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}> <SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children} {children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"> <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">

View file

@ -0,0 +1,732 @@
import React from 'react';
import { cva, VariantProps } from 'class-variance-authority';
import { PanelLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { Sheet, SheetContent } from '@/components/ui/sheet';
import { Skeleton } from '@/components/ui/skeleton';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
const SIDEBAR_COOKIE_NAME = 'sidebar_state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = {
state: 'expanded' | 'collapsed';
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContext | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a SidebarProvider.');
}
return context;
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref,
) => {
const isMobile = false;
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [internalIsOpen, setInternalIsOpen] = React.useState(defaultOpen);
const open = openProp ?? internalIsOpen;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === 'function' ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
setInternalIsOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? 'expanded' : 'collapsed';
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
'group/sidebar-wrapper has-[[data-variant=inset]]:bg-sidebar flex min-h-svh w-full',
className,
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
},
);
SidebarProvider.displayName = 'SidebarProvider';
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
}
>(
(
{
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
className,
children,
...props
},
ref,
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === 'none') {
return (
<div
className={cn(
'bg-sidebar text-sidebar-foreground flex h-full w-[--sidebar-width] flex-col',
className,
)}
ref={ref}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-[--sidebar-width] p-0 [&>button]:hidden"
style={
{
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex size-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
ref={ref}
className="text-sidebar-foreground group peer hidden md:block"
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
)}
/>
<div
className={cn(
'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
);
},
);
Sidebar.displayName = 'Sidebar';
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn('size-7', className)}
onClick={event => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
});
SidebarTrigger.displayName = 'SidebarTrigger';
const SidebarRail = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'>>(
({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar();
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
'[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
'[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
'group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
'[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
'[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
className,
)}
{...props}
/>
);
},
);
SidebarRail.displayName = 'SidebarRail';
const SidebarInset = React.forwardRef<HTMLDivElement, React.ComponentProps<'main'>>(
({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
'bg-background relative flex min-h-svh flex-1 flex-col',
'peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
className,
)}
{...props}
/>
);
},
);
SidebarInset.displayName = 'SidebarInset';
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
'bg-background focus-visible:ring-sidebar-ring h-8 w-full shadow-none focus-visible:ring-2',
className,
)}
{...props}
/>
);
});
SidebarInput.displayName = 'SidebarInput';
const SidebarHeader = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
},
);
SidebarHeader.displayName = 'SidebarHeader';
const SidebarFooter = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn('flex flex-col gap-2 p-2', className)}
{...props}
/>
);
},
);
SidebarFooter.displayName = 'SidebarFooter';
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn('bg-sidebar-border mx-2 w-auto', className)}
{...props}
/>
);
});
SidebarSeparator.displayName = 'SidebarSeparator';
const SidebarContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
className,
)}
{...props}
/>
);
},
);
SidebarContent.displayName = 'SidebarContent';
const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
{...props}
/>
);
},
);
SidebarGroup.displayName = 'SidebarGroup';
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-none transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className,
)}
{...props}
/>
);
});
SidebarGroupLabel.displayName = 'SidebarGroupLabel';
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
});
SidebarGroupAction.displayName = 'SidebarGroupAction';
const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn('w-full text-sm', className)}
{...props}
/>
),
);
SidebarGroupContent.displayName = 'SidebarGroupContent';
const SidebarMenu = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn('flex w-full min-w-0 flex-col gap-1', className)}
{...props}
/>
),
);
SidebarMenu.displayName = 'SidebarMenu';
const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn('group/menu-item relative', className)}
{...props}
/>
),
);
SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
outline:
'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
},
size: {
default: 'h-8 text-sm',
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = 'default',
size = 'default',
tooltip,
className,
...props
},
ref,
) => {
const Comp = asChild ? Slot : 'button';
const { isMobile, state } = useSidebar();
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === 'string') {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== 'collapsed' || isMobile}
{...tooltip}
/>
</Tooltip>
);
},
);
SidebarMenuButton.displayName = 'SidebarMenuButton';
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<'button'> & {
asChild?: boolean;
showOnHover?: boolean;
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-none transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
showOnHover &&
'peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0',
className,
)}
{...props}
/>
);
});
SidebarMenuAction.displayName = 'SidebarMenuAction';
const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
'text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums',
'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
),
);
SidebarMenuBadge.displayName = 'SidebarMenuBadge';
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
showIcon?: boolean;
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
{...props}
>
{showIcon && <Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
'--skeleton-width': width,
} as React.CSSProperties
}
/>
</div>
);
});
SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
),
);
SidebarMenuSub.displayName = 'SidebarMenuSub';
const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
({ ...props }, ref) => <li ref={ref} {...props} />,
);
SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<'a'> & {
asChild?: boolean;
size?: 'sm' | 'md';
isActive?: boolean;
}
>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'a';
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
className,
)}
{...props}
/>
);
});
SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
};

View file

@ -1,7 +1,12 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('bg-primary/10 animate-pulse rounded-md', className)} {...props} />; return (
<div
className={cn('bg-primary/10 animate-pulse rounded-md align-middle', className)}
{...props}
/>
);
} }
export { Skeleton }; export { Skeleton };

View file

@ -40,6 +40,18 @@
--header-height: 84px; --header-height: 84px;
--tabs-navbar-height: 47px; --tabs-navbar-height: 47px;
--content-height: calc(100vh - var(--header-height) - var(--tabs-navbar-height)); --content-height: calc(100vh - var(--header-height) - var(--tabs-navbar-height));
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
--chart-1: 173 58% 39%;
--chart-2: 12 76% 61%;
} }
.dark { .dark {
@ -76,6 +88,18 @@
--ring: 216 34% 17%; --ring: 216 34% 17%;
--radius: 0.5rem; --radius: 0.5rem;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
} }
} }
@ -84,6 +108,10 @@
@apply border-border; @apply border-border;
} }
html {
@apply bg-[#030711];
}
body { body {
@apply text-foreground bg-[#030711]; @apply text-foreground bg-[#030711];
font-feature-settings: font-feature-settings:

View file

@ -2,7 +2,7 @@
* The original source code was taken from Grafana's date-math.ts file and adjusted for Hive needs. * The original source code was taken from Grafana's date-math.ts file and adjusted for Hive needs.
* @source https://github.com/grafana/grafana/blob/411c89012febe13323e4b8aafc8d692f4460e680/packages/grafana-data/src/datetime/datemath.ts#L1C1-L208C2 * @source https://github.com/grafana/grafana/blob/411c89012febe13323e4b8aafc8d692f4460e680/packages/grafana-data/src/datetime/datemath.ts#L1C1-L208C2
*/ */
import { add, format, formatISO, parse as parseDate, sub, type Duration } from 'date-fns'; import { add, format, formatISO, parse as parseDate, parseISO, sub, type Duration } from 'date-fns';
import { z } from 'zod'; import { z } from 'zod';
import { UTCDate } from '@date-fns/utc'; import { UTCDate } from '@date-fns/utc';
@ -32,7 +32,7 @@ function unitToDurationKey(unit: DurationUnit): keyof Duration {
} }
} }
const dateStringFormat = 'yyyy-MM-dd'; const dateStringFormat = 'yyyy-MM-dd HH:mm';
function parseDateString(input: string) { function parseDateString(input: string) {
try { try {
@ -46,38 +46,37 @@ export function formatDateToString(date: Date) {
return format(date, dateStringFormat); return format(date, dateStringFormat);
} }
function isValidDateString(input: string) {
return input.length === 10 && parseDateString(input) !== undefined;
}
/** /**
* Parses different types input to a moment instance. There is a specific formatting language that can be used * Parse a time iso string or formular into an actual date
* if text arg is string. See unit tests for examples.
* @param text
* @param roundUp See parseDateMath function.
* @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used.
*/ */
export function parse(text: string, now = new UTCDate()): Date | undefined { export function parse(text: string, now = new UTCDate()): Date | undefined {
if (!text) { if (!text) {
return undefined; return undefined;
} }
let mathString = ''; if (!text.startsWith('now')) {
// Try parsing as yyyy-MM-dd HH:mm
const date = parseDateString(text);
if (date && !Number.isNaN(date.getTime())) {
return date;
}
// Try parsing as ISO
const isoDate = parseISO(text);
if (isoDate && !Number.isNaN(isoDate.getTime())) {
return isoDate;
}
if (text.substring(0, 3) === 'now') {
// time = dateTimeForTimeZone(timezone);
mathString = text.substring('now'.length);
} else if (isValidDateString(text)) {
return parseDateString(text);
} else {
return undefined; return undefined;
} }
if (!mathString.length) { const mathExpression = text.slice('now'.length);
if (!mathExpression) {
return now; return now;
} }
return parseDateMath(mathString, now); // Handle "now" with date math (e.g., "now+1d")
return parseDateMath(mathExpression, now);
} }
/** /**

View file

@ -16,8 +16,12 @@ import { useRouter } from '@tanstack/react-router';
import { useResetState } from './use-reset-state'; import { useResetState } from './use-reset-state';
export function useDateRangeController(args: { export function useDateRangeController(args: {
/** the data retention aka minimum time range. */
dataRetentionInDays: number; dataRetentionInDays: number;
/** the default preset to pick if no range is provided. */
defaultPreset: Preset; defaultPreset: Preset;
/** controlled input range */
range?: Preset['range'];
}) { }) {
const router = useRouter(); const router = useRouter();
@ -25,11 +29,10 @@ export function useDateRangeController(args: {
() => subDays(new Date(), args.dataRetentionInDays), () => subDays(new Date(), args.dataRetentionInDays),
[args.dataRetentionInDays], [args.dataRetentionInDays],
); );
const searchParams = router.latestLocation.search; const searchParams = router.latestLocation.search;
// const params = new URLSearchParams(urlParameter); const fromRaw =
const fromRaw = (('from' in searchParams && searchParams.from) ?? '') as string; args.range?.from ?? ((('from' in searchParams && searchParams.from) ?? '') as string);
const toRaw = (('to' in searchParams && searchParams.to) ?? 'now') as string; const toRaw = args.range?.to ?? ((('to' in searchParams && searchParams.to) ?? 'now') as string);
const [selectedPreset] = useResetState(() => { const [selectedPreset] = useResetState(() => {
const preset = availablePresets.find(p => p.range.from === fromRaw && p.range.to === toRaw); const preset = availablePresets.find(p => p.range.from === fromRaw && p.range.to === toRaw);

View file

@ -35,6 +35,7 @@ export const urqlClient = createClient({
resolvers: { resolvers: {
Target: { Target: {
appDeployments: relayPagination(), appDeployments: relayPagination(),
traces: relayPagination(),
}, },
AppDeployment: { AppDeployment: {
documents: relayPagination(), documents: relayPagination(),
@ -77,6 +78,10 @@ export const urqlClient = createClient({
MetadataAttribute: noKey, MetadataAttribute: noKey,
RateLimit: noKey, RateLimit: noKey,
DeprecatedSchemaExplorer: noKey, DeprecatedSchemaExplorer: noKey,
TraceStatusBreakdownBucket: noKey,
FilterStringOption: noKey,
FilterBooleanOption: noKey,
TracesFilterOptions: noKey,
}, },
globalIDs: ['SuccessfulSchemaCheck', 'FailedSchemaCheck'], globalIDs: ['SuccessfulSchemaCheck', 'FailedSchemaCheck'],
}), }),

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ghost from '../../public/images/figures/ghost.svg?url';
import { LoaderCircleIcon } from 'lucide-react'; import { LoaderCircleIcon } from 'lucide-react';
import { useClient, useQuery } from 'urql'; import { useClient, useQuery } from 'urql';
import { AppFilter } from '@/components/apps/AppFilter'; import { AppFilter } from '@/components/apps/AppFilter';
import { NotFoundContent } from '@/components/common/not-found-content';
import { Page, TargetLayout } from '@/components/layouts/target'; import { Page, TargetLayout } from '@/components/layouts/target';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { CardDescription } from '@/components/ui/card'; import { CardDescription } from '@/components/ui/card';
@ -157,20 +157,10 @@ function TargetAppVersionContent(props: {
return ( return (
<> <>
<Meta title="App Version Not found" /> <Meta title="App Version Not found" />
<div className="flex h-full flex-1 flex-col items-center justify-center gap-2.5 py-6"> <NotFoundContent
<img heading="App Version not found."
src={ghost} subheading="This app does not seem to exist anymore."
alt="Ghost illustration" />
width="200"
height="200"
className="drag-none"
/>
<h2 className="text-xl font-bold">App Version not found.</h2>
<h3 className="font-semibold">This app does not seem to exist anymore.</h3>
<Button variant="secondary" className="mt-2" onClick={router.history.back}>
Go back
</Button>
</div>
</> </>
); );
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,677 @@
import {
ChangeEventHandler,
ComponentPropsWithoutRef,
ElementRef,
forwardRef,
Fragment,
InputHTMLAttributes,
memo,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { addDays, formatDate, setHours, setMinutes } from 'date-fns';
import debounce from 'lodash.debounce';
import {
CalendarIcon,
CheckIcon,
ChevronRightIcon,
CircleXIcon,
MinusIcon,
PlusIcon,
} from 'lucide-react';
import type { DateRange } from 'react-day-picker';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { findMatchingPreset, Preset } from '@/components/ui/date-range-picker';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarSeparator,
} from '@/components/ui/sidebar';
import { formatNumber } from '@/lib/hooks';
import { useResetState } from '@/lib/hooks/use-reset-state';
import { cn } from '@/lib/utils';
import * as SliderPrimitive from '@radix-ui/react-slider';
import * as dateMath from '../../lib/date-math';
interface FilterInputProps extends InputHTMLAttributes<HTMLInputElement> {}
export const FilterInput = forwardRef<HTMLInputElement, FilterInputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
);
},
);
export function FilterLocalSearch(props: { value: string; onChange(value: string): void }) {
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
props.onChange(e.target.value);
}, []);
return (
<div className="mt-4 flex w-full max-w-sm items-center space-x-2">
<FilterInput
type="text"
placeholder="Search values"
value={props.value}
onChange={handleChange}
/>
</div>
);
}
export function FilterTitle(props: { children: ReactNode; changes?: number; onReset(): void }) {
return (
<SidebarGroupLabel
asChild
className="group/label text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground w-full text-sm"
>
<CollapsibleTrigger>
<ChevronRightIcon className="mr-2 transition-transform group-data-[state=open]/collapsible:rotate-90" />
{props.children}
{props.changes ? (
<Button
variant="secondary"
size="sm"
className="hover:bg-secondary group ml-auto h-6 w-8 px-1 py-0 text-xs text-gray-500"
onClick={e => {
e.preventDefault();
props.onReset();
}}
asChild
>
<div>
<CircleXIcon className="hidden size-3 group-hover:block" />
<span className="block group-hover:hidden">{props.changes}</span>
</div>
</Button>
) : null}
</CollapsibleTrigger>
</SidebarGroupLabel>
);
}
export function FilterContent(props: { children: ReactNode }) {
return (
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>{props.children}</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
);
}
export const MultiInputFilter = memo(
(props: {
name: string;
/**
* Filter's key for the backend and url state
*/
key: string;
selectedValues: string[];
onChange(selectedValues: string[]): void;
}) => {
const [traceId, setTraceId] = useState('');
const handleTraceIdChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setTraceId(e.target.value);
},
[setTraceId],
);
const addTraceId = useCallback(() => {
if (!traceId) {
return;
}
if (!props.selectedValues.includes(traceId)) {
props.onChange(props.selectedValues.concat(traceId));
}
setTraceId('');
}, [traceId, setTraceId]);
return (
<Filter name={props.name}>
<FilterTitle changes={props.selectedValues.length} onReset={() => props.onChange([])}>
{props.name}
</FilterTitle>
<FilterContent>
<form
className="mt-4 flex w-full max-w-sm items-center space-x-2"
onSubmit={e => {
e.preventDefault();
addTraceId();
}}
>
<FilterInput
type="text"
placeholder="Trace ID..."
value={traceId}
onChange={handleTraceIdChange}
/>
<Button
variant="secondary"
className="size-9 p-0"
type="submit"
onClick={() => {
addTraceId();
}}
>
<PlusIcon className="size-4" />
</Button>
</form>
{props.selectedValues.map(value => (
<SidebarMenuButton
key={value}
onClick={() => props.onChange(props.selectedValues.filter(val => val !== value))}
className="group/trace-id hover:bg-sidebar-accent/50"
>
<div
data-active
className="text-sidebar-primary-foreground border-sidebar-primary bg-sidebar-primary group-hover/trace-id:border-sidebar-border flex aspect-square size-4 shrink-0 items-center justify-center rounded-sm border group-hover/trace-id:bg-transparent"
>
<CheckIcon className="block size-3 group-hover/trace-id:hidden" />
<MinusIcon className="hidden size-3 group-hover/trace-id:block" />
</div>
{value}
</SidebarMenuButton>
))}
</FilterContent>
</Filter>
);
},
);
export const MultiSelectFilter = function MultiSelectFilter<$Value>(props: {
name: string;
/**
* Filter's key for the backend and url state
*/
key: string;
hideSearch?: boolean;
options: Array<{
/**
* How often it occurs
*/
count: number;
/**
* What to display
*/
label: ReactNode;
/**
* What to use when searching
*/
searchContent: string;
/**
* A value to use when the filter is selected
*/
value: $Value;
}>;
selectedValues: $Value[];
onChange(selectedValues: $Value[]): void;
}) {
const [searchPhrase, setSearchPhrase] = useState('');
const filteredOptions = useMemo(() => {
const lowerSearchPhrase = searchPhrase.toLowerCase().trim();
if (!lowerSearchPhrase) {
return props.options;
}
return props.options.filter(option =>
option.searchContent.toLowerCase().includes(lowerSearchPhrase),
);
}, [searchPhrase, props.options]);
return (
<Filter name={props.name}>
<FilterTitle changes={props.selectedValues.length} onReset={() => props.onChange([])}>
{props.name}
</FilterTitle>
<FilterContent>
{!props.hideSearch && <FilterLocalSearch value={searchPhrase} onChange={setSearchPhrase} />}
{filteredOptions.map((option, index) => (
<FilterOption
key={index}
selected={props.selectedValues.includes(option.value)}
count={option.count}
onClick={() => {
if (props.selectedValues.includes(option.value)) {
props.onChange(props.selectedValues.filter(val => val !== option.value));
} else {
props.onChange(props.selectedValues.concat(option.value));
}
}}
>
{option.label === '' ? (
<span className="text-gray-400">{'<unknown>'}</span>
) : (
option.label
)}
</FilterOption>
))}
</FilterContent>
</Filter>
);
};
function FilterOption(props: {
onClick(): void;
selected: boolean;
children: ReactNode;
count?: number;
}) {
return (
<SidebarMenuButton
onClick={props.onClick}
className="hover:bg-sidebar-accent/50 flex-row items-center justify-between"
>
<div className="flex items-center gap-2 overflow-hidden">
<div
data-active={props.selected}
className="group/filter-item border-sidebar-border text-sidebar-primary-foreground data-[active=true]:border-sidebar-primary data-[active=true]:bg-sidebar-primary flex aspect-square size-4 shrink-0 items-center justify-center rounded-sm border"
>
<CheckIcon className="hidden size-3 group-data-[active=true]/filter-item:block" />
</div>
{props.children}
</div>
{props.count ? (
<Badge variant="secondary" className="rounded-sm px-1 font-mono font-normal">
{formatNumber(props.count)}
</Badge>
) : null}
</SidebarMenuButton>
);
}
function Filter(props: { name: string; children: ReactNode }) {
return (
<Fragment key={props.name}>
<SidebarGroup key={props.name} className="py-0">
<Collapsible className="group/collapsible">{props.children}</Collapsible>
</SidebarGroup>
<SidebarSeparator className="mx-0" />
</Fragment>
);
}
const DoubleSlider = forwardRef<
ElementRef<typeof SliderPrimitive.Root>,
ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full bg-gray-800">
<SliderPrimitive.Range className="absolute h-full bg-gray-400" />
</SliderPrimitive.Track>
{props.value?.map((_, index) => (
<SliderPrimitive.Thumb
key={index}
className="block size-4 rounded-full border border-gray-700 bg-gray-800 transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
));
export const DurationFilter = memo(
(props: { value: [number, number] | []; onChange(value: [number, number]): void }) => {
const minValue = 0;
const maxValue = 100_000;
const defaultValues: [number, number] = [minValue, maxValue];
const [values, setValues] = useState<[number, number]>(
props.value.length ? props.value : defaultValues,
);
const handleStateChange = useMemo(
() =>
debounce((newValues: [number, number]) => {
props.onChange(newValues);
}, 1000),
[props.onChange],
);
const handleSliderChange = useCallback(
(newValues: [number, number]) => {
handleStateChange(newValues);
setValues(newValues);
},
[handleStateChange, setValues],
);
useEffect(() => {
return () => handleStateChange.cancel();
}, [handleStateChange]);
const handleInputChange = useCallback(
(index: number, value: string) => {
const numValue = Number.parseInt(value) || minValue;
const newValues: [number, number] = [...values];
newValues[index] = Math.min(Math.max(numValue, minValue), maxValue);
handleStateChange(newValues);
setValues(newValues);
},
[handleStateChange, setValues],
);
const handleMinInputChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
e => handleInputChange(0, e.target.value),
[handleInputChange],
);
const handleMaxInputChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
e => handleInputChange(1, e.target.value),
[handleInputChange],
);
return (
<Filter name="Duration">
<FilterTitle
changes={values[0] === minValue && values[1] === maxValue ? 0 : 1}
onReset={() => props.onChange(defaultValues)}
>
Duration
</FilterTitle>
<FilterContent>
<div className="space-y-6 p-2">
<div className="space-y-2">
<div className="space-y-1">
<label className="font-mono text-xs text-zinc-400">MIN</label>
<div className="relative">
<FilterInput
type="number"
value={values[0]}
onChange={handleMinInputChange}
className="h-7 border-zinc-800 bg-transparent px-2 pr-8 font-mono text-white"
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 font-mono text-xs text-zinc-400">
ms
</span>
</div>
</div>
<div className="space-y-1">
<label className="font-mono text-xs text-zinc-400">MAX</label>
<div className="relative">
<FilterInput
type="number"
value={values[1]}
onChange={handleMaxInputChange}
className="h-7 border-gray-800 bg-transparent px-2 pr-8 font-mono text-white"
/>
<span className="absolute right-2 top-1/2 -translate-y-1/2 font-mono text-xs text-gray-400">
ms
</span>
</div>
</div>
</div>
<DoubleSlider
defaultValue={defaultValues}
max={maxValue}
min={minValue}
step={1}
value={values}
onValueChange={handleSliderChange}
className="[&_[role=slider]]:size-4"
/>
</div>
</FilterContent>
</Filter>
);
},
);
const availableTimelineFilterPresets: Array<Preset> = [
{ name: 'last5m', label: 'Last 5 minutes', range: { from: 'now-5m', to: 'now' } },
{ name: 'last1hour', label: 'Last 1 hour', range: { from: 'now-1h', to: 'now' } },
{ name: 'last3hours', label: 'Last 3 hours', range: { from: 'now-3h', to: 'now' } },
{ name: 'last12hours', label: 'Last 12 hours', range: { from: 'now-12h', to: 'now' } },
{ name: 'last24hours', label: 'Last 24 hours', range: { from: 'now-24h', to: 'now' } },
];
export const TimelineFilter = memo(
(props: { value: [string, string] | []; onChange(value: [string, string] | []): void }) => {
const selectedPreset = useMemo<Preset | null>(() => {
if (!props.value.length) {
return null;
}
return (
findMatchingPreset(
{
from: props.value[0],
to: props.value[1],
},
availableTimelineFilterPresets,
) ?? {
name: 'custom',
label: 'Custom',
range: {
from: props.value[0],
to: props.value[1],
},
}
);
}, [props.value]);
const [isRangeSelectorPopupOpen, setIsRangeSelectorPopupOpen] = useState(false);
const [dateRange, setDateRange] = useResetState<DateRange | undefined>(
() =>
selectedPreset?.name === 'custom'
? {
from: new Date(selectedPreset.range.from),
to: new Date(selectedPreset.range.to),
}
: {
from: addDays(new Date(), -3),
to: new Date(),
},
[props.value[0], props.value[1], isRangeSelectorPopupOpen],
);
const formatted = useMemo(() => {
if (!dateRange?.from || !dateRange.to || selectedPreset?.name !== 'custom') {
return 'Select time period';
}
const fromDate = formatDate(dateRange.from, 'MMM d');
const fromTime = formatDate(dateRange.from, 'HH:mm');
const toDate = formatDate(dateRange.to, 'MMM d');
const toTime = formatDate(dateRange.to, 'HH:mm');
if (fromDate === toDate) {
return `${fromDate}, ${fromTime} - ${toTime}`;
}
return `${fromDate}, ${fromTime} - ${toDate}, ${toTime}`;
}, [dateRange, selectedPreset]);
return (
<Filter name="Timeline">
<FilterTitle changes={props.value.length ? 1 : 0} onReset={() => props.onChange([])}>
Timeline
</FilterTitle>
<FilterContent>
<div className="space-y-2 p-2">
<Select
// The key is needed to reset the select state to show the SelectTrigger after resetting the filters.
key={`${props.value.at(0)}_${props.value.at(1)}`}
value={selectedPreset?.name ?? undefined}
onValueChange={value => {
if (value === 'custom') {
const preset = selectedPreset ?? availableTimelineFilterPresets[0];
props.onChange([
dateMath.parse(preset.range.from)?.toISOString() ?? new Date().toISOString(),
dateMath.parse(preset.range.to)?.toISOString() ?? new Date().toISOString(),
]);
return;
}
const preset = availableTimelineFilterPresets.find(preset => preset.name === value);
if (preset) {
props.onChange([preset.range.from, preset.range.to]);
}
}}
>
<SelectTrigger className="bg-background w-full">
<SelectValue placeholder="Select time period" />
</SelectTrigger>
<SelectContent>
{availableTimelineFilterPresets.map(preset => (
<SelectItem value={preset.name} key={preset.name}>
{preset.label}
</SelectItem>
))}
<SelectItem value="custom">Custom</SelectItem>
</SelectContent>
</Select>
{selectedPreset?.name === 'custom' ? (
<>
<Popover
open={isRangeSelectorPopupOpen}
onOpenChange={isOpen => {
setIsRangeSelectorPopupOpen(isOpen);
}}
>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start px-2 text-left">
<CalendarIcon className="mr-2 size-4" />{' '}
<span className="text-xs">{formatted}</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<Calendar
initialFocus
mode="range"
defaultMonth={dateRange?.from}
selected={dateRange}
onSelect={setDateRange}
numberOfMonths={1}
className="p-2 pb-0"
/>
<div className="border-border mt-4 space-y-2 border-t p-2">
<div>
<Label className="text-sm font-normal text-gray-500">Start</Label>
<div className="flex items-center gap-x-2">
<Input
className="h-8 w-[152px] py-0"
value={dateRange?.from ? formatDate(dateRange.from, 'yyyy-MM-dd') : ''}
/>
<Input
className="h-8 w-16 py-0"
value={dateRange?.from ? formatDate(dateRange.from, 'HH:mm') : ''}
type="time"
min="00:00"
max="23:59"
onChange={ev => {
setDateRange(range =>
range
? {
...range,
from: setMinutes(
setHours(
range.from ?? new Date(),
parseInt(ev.target.value.substr(0, 2)),
),
parseInt(ev.target.value.substr(3, 5)),
),
}
: undefined,
);
}}
/>
</div>
</div>
<div>
<Label className="text-sm font-normal text-gray-500">End</Label>
<div className="flex items-center gap-x-2">
<Input
className="h-8 w-[152px] py-0"
value={dateRange?.to ? formatDate(dateRange.to, 'yyyy-MM-dd') : ''}
/>
<Input
className="h-8 w-16 py-0"
value={dateRange?.to ? formatDate(dateRange.to, 'HH:mm') : ''}
type="time"
min="00:00"
max="23:59"
onChange={ev => {
setDateRange(range =>
range
? {
...range,
to: setMinutes(
setHours(
range.to ?? new Date(),
parseInt(ev.target.value.substr(0, 2)),
),
parseInt(ev.target.value.substr(3, 5)),
),
}
: undefined,
);
}}
/>
</div>
</div>
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
if (!dateRange?.from || !dateRange.to) {
return;
}
props.onChange([
dateRange.from.toISOString(),
dateRange.to.toISOString(),
]);
setIsRangeSelectorPopupOpen(false);
}}
>
<span className="relative">
Apply
<span className="absolute top-[4px] ml-2 text-xs"></span>
</span>
</Button>
</div>
</PopoverContent>
</Popover>
</>
) : null}
</div>
</FilterContent>
</Filter>
);
},
);

View file

@ -0,0 +1,49 @@
import { createContext, FC, ReactNode, useCallback, useContext, useRef, useState } from 'react';
interface WidthSyncContextType {
width: number;
updateWidth: (newWidth: number) => void;
}
const WidthSyncContext = createContext<WidthSyncContextType | null>(null);
interface WidthSyncProviderProps {
children: ReactNode;
defaultWidth: number;
}
export const WidthSyncProvider: FC<WidthSyncProviderProps> = props => {
const [width, setWidth] = useState<number>(props.defaultWidth);
// Throttle state to avoid too many updates during slider dragging
const throttleTimerRef = useRef<number | null>(null);
// Ensure width stays within bounds and apply to all registered components
const handleSetWidth = useCallback((newWidth: number) => {
// Clear any existing throttle timer
if (throttleTimerRef.current !== null) {
window.cancelAnimationFrame(throttleTimerRef.current);
}
// Use requestAnimationFrame for smoother updates
throttleTimerRef.current = window.requestAnimationFrame(() => {
const boundedWidth = newWidth;
setWidth(boundedWidth);
throttleTimerRef.current = null;
});
}, []);
return (
<WidthSyncContext.Provider value={{ width, updateWidth: handleSetWidth }}>
{props.children}
</WidthSyncContext.Provider>
);
};
export const useWidthSync = () => {
const context = useContext(WidthSyncContext);
if (!context) {
throw new Error('useWidthSync must be used within a WidthSyncProvider');
}
return [context.width, context.updateWidth] as const;
};

View file

@ -1,4 +1,5 @@
import { lazy, useCallback, useEffect } from 'react'; import { lazy, useCallback, useEffect, useMemo } from 'react';
import { parse as jsUrlParse, stringify as jsUrlStringify } from 'jsurl2';
import { Helmet, HelmetProvider } from 'react-helmet-async'; import { Helmet, HelmetProvider } from 'react-helmet-async';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import SuperTokens, { SuperTokensWrapper } from 'supertokens-auth-react'; import SuperTokens, { SuperTokensWrapper } from 'supertokens-auth-react';
@ -19,6 +20,8 @@ import {
createRouter, createRouter,
Navigate, Navigate,
Outlet, Outlet,
parseSearchWith,
stringifySearchWith,
useNavigate, useNavigate,
} from '@tanstack/react-router'; } from '@tanstack/react-router';
import { ErrorComponent } from './components/error'; import { ErrorComponent } from './components/error';
@ -74,6 +77,13 @@ import { TargetInsightsCoordinatePage } from './pages/target-insights-coordinate
import { TargetInsightsOperationPage } from './pages/target-insights-operation'; import { TargetInsightsOperationPage } from './pages/target-insights-operation';
import { TargetLaboratoryPage } from './pages/target-laboratory'; import { TargetLaboratoryPage } from './pages/target-laboratory';
import { TargetSettingsPage, TargetSettingsPageEnum } from './pages/target-settings'; import { TargetSettingsPage, TargetSettingsPageEnum } from './pages/target-settings';
import { TargetTracePage } from './pages/target-trace';
import {
FilterState,
TargetTracesFilterState,
TargetTracesPage,
TargetTracesSort,
} from './pages/target-traces';
SuperTokens.init(frontendConfig()); SuperTokens.init(frontendConfig());
if (env.sentry) { if (env.sentry) {
@ -646,6 +656,85 @@ const targetInsightsRoute = createRoute({
}, },
}); });
const TargetTracesRouteSearch = z.object({
filter: TargetTracesFilterState.optional(),
sort: TargetTracesSort.shape.optional(),
from: z.string().optional(),
to: z.string().optional(),
});
const targetTracesRoute = createRoute({
getParentRoute: () => targetRoute,
path: 'traces',
validateSearch: TargetTracesRouteSearch.parse,
component: function TargetTracesRoute() {
const { organizationSlug, projectSlug, targetSlug } = targetTracesRoute.useParams();
const {
filter = {
'graphql.client': [],
'graphql.errorCode': [],
'graphql.kind': [],
'graphql.operation': [],
'graphql.status': [],
'graphql.subgraph': [],
'http.host': [],
'http.method': [],
'http.route': [],
'http.status': [],
'http.url': [],
'trace.id': [],
duration: [],
} satisfies FilterState,
sort = {
id: 'timestamp',
desc: true,
},
from,
to,
} = targetTracesRoute.useSearch();
const range = useMemo(() => (from && to ? { from, to } : null), [from, to]);
return (
<TargetTracesPage
organizationSlug={organizationSlug}
projectSlug={projectSlug}
targetSlug={targetSlug}
sorting={sort}
filter={filter}
range={range}
/>
);
},
});
const TargetTraceRouteSearchModel = z.object({
activeSpanId: z.string().optional(),
activeSpanTab: z.string().optional(),
});
const targetTraceRoute = createRoute({
getParentRoute: () => targetRoute,
validateSearch(search) {
return TargetTraceRouteSearchModel.parse(search);
},
path: 'trace/$traceId',
component: function TargetTraceRoute() {
const { organizationSlug, projectSlug, targetSlug, traceId } = targetTraceRoute.useParams();
const { activeSpanId, activeSpanTab } = targetTraceRoute.useSearch();
return (
<TargetTracePage
organizationSlug={organizationSlug}
projectSlug={projectSlug}
targetSlug={targetSlug}
traceId={traceId}
activeSpanId={activeSpanId ?? null}
activeSpanTab={activeSpanTab ?? null}
/>
);
},
});
const targetInsightsCoordinateRoute = createRoute({ const targetInsightsCoordinateRoute = createRoute({
getParentRoute: () => targetRoute, getParentRoute: () => targetRoute,
path: 'insights/schema-coordinate/$coordinate', path: 'insights/schema-coordinate/$coordinate',
@ -876,6 +965,8 @@ const routeTree = root.addChildren([
targetLaboratoryRoute, targetLaboratoryRoute,
targetHistoryRoute.addChildren([targetHistoryVersionRoute]), targetHistoryRoute.addChildren([targetHistoryVersionRoute]),
targetInsightsRoute, targetInsightsRoute,
targetTraceRoute,
targetTracesRoute,
targetInsightsCoordinateRoute, targetInsightsCoordinateRoute,
targetInsightsClientRoute, targetInsightsClientRoute,
targetInsightsOperationsRoute, targetInsightsOperationsRoute,
@ -890,7 +981,21 @@ const routeTree = root.addChildren([
]), ]),
]); ]);
export const router = createRouter({ routeTree }); export const router = createRouter({
routeTree,
parseSearch: parseSearchWith(str => {
if (window.location.pathname.endsWith('/traces')) {
return jsUrlParse(str);
}
return JSON.parse(str);
}),
stringifySearch: stringifySearchWith(search => {
if (window.location.pathname.endsWith('/traces')) {
return jsUrlStringify(search);
}
return JSON.stringify(search);
}),
});
router.history.subscribe(() => { router.history.subscribe(() => {
gtag.pageview(router.history.location.href); gtag.pageview(router.history.location.href);

View file

@ -75,7 +75,6 @@ module.exports = {
900: '#005b43', 900: '#005b43',
}, },
cyan: '#0acccc', cyan: '#0acccc',
purple: '#5f2eea',
blue: colors.sky, blue: colors.sky,
gray: colors.stone, gray: colors.stone,
magenta: '#f11197', magenta: '#f11197',
@ -91,6 +90,8 @@ module.exports = {
800: '#926e26', 800: '#926e26',
900: '#785a1f', 900: '#785a1f',
}, },
zinc: colors.zinc,
purple: colors.purple,
}, },
extend: { extend: {
colors: { colors: {
@ -127,11 +128,22 @@ module.exports = {
DEFAULT: 'hsl(var(--card))', DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))', foreground: 'hsl(var(--card-foreground))',
}, },
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))',
},
}, },
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)', md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)', sm: 'calc(var(--radius) - 4px)',
xs: 'calc(var(--radius) - 6px)',
}, },
ringColor: theme => ({ ringColor: theme => ({
DEFAULT: theme('colors.orange.500/75'), DEFAULT: theme('colors.orange.500/75'),

View file

@ -1,13 +1,28 @@
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import type { UserConfig } from 'vite'; import type { Plugin, UserConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
const __dirname = new URL('.', import.meta.url).pathname; const __dirname = new URL('.', import.meta.url).pathname;
// Add react-scan in local development mode
const reactScanPlugin: Plugin = {
name: 'react-scan',
transformIndexHtml(html, ctx) {
if (ctx.server?.config.command === 'serve') {
return html.replace(
'<head>',
'<head><script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>',
);
}
return html;
},
};
export default { export default {
root: __dirname, root: __dirname,
plugins: [tsconfigPaths(), react()], plugins: [tsconfigPaths(), react(), reactScanPlugin],
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {

View file

@ -376,6 +376,12 @@ importers:
specifier: 3.25.76 specifier: 3.25.76
version: 3.25.76 version: 3.25.76
load-tests/otel-traces:
devDependencies:
'@types/k6':
specifier: 1.2.0
version: 1.2.0
packages/libraries/apollo: packages/libraries/apollo:
dependencies: dependencies:
'@graphql-hive/core': '@graphql-hive/core':
@ -1415,7 +1421,7 @@ importers:
devDependencies: devDependencies:
'@graphql-inspector/core': '@graphql-inspector/core':
specifier: 5.1.0-alpha-20231208113249-34700c8a specifier: 5.1.0-alpha-20231208113249-34700c8a
version: 5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0) version: 5.1.0-alpha-20231208113249-34700c8a(graphql@16.11.0)
'@hive/service-common': '@hive/service-common':
specifier: workspace:* specifier: workspace:*
version: link:../service-common version: link:../service-common
@ -1851,6 +1857,9 @@ importers:
'@types/js-cookie': '@types/js-cookie':
specifier: 3.0.6 specifier: 3.0.6
version: 3.0.6 version: 3.0.6
'@types/lodash.debounce':
specifier: 4.0.9
version: 4.0.9
'@types/react': '@types/react':
specifier: 18.3.18 specifier: 18.3.18
version: 18.3.18 version: 18.3.18
@ -1878,6 +1887,9 @@ importers:
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: 4.3.4 specifier: 4.3.4
version: 4.3.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) version: 4.3.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))
'@xyflow/react':
specifier: 12.4.4
version: 12.4.4(@types/react@18.3.18)(immer@10.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
autoprefixer: autoprefixer:
specifier: 10.4.21 specifier: 10.4.21
version: 10.4.21(postcss@8.5.6) version: 10.4.21(postcss@8.5.6)
@ -1896,6 +1908,9 @@ importers:
date-fns: date-fns:
specifier: 4.1.0 specifier: 4.1.0
version: 4.1.0 version: 4.1.0
date-fns-tz:
specifier: 3.2.0
version: 3.2.0(date-fns@4.1.0)
dompurify: dompurify:
specifier: 3.2.6 specifier: 3.2.6
version: 3.2.6 version: 3.2.6
@ -1938,6 +1953,12 @@ importers:
json-schema-yup-transformer: json-schema-yup-transformer:
specifier: 1.6.12 specifier: 1.6.12
version: 1.6.12 version: 1.6.12
jsurl2:
specifier: 2.2.0
version: 2.2.0
lodash.debounce:
specifier: 4.0.8
version: 4.0.8
lucide-react: lucide-react:
specifier: 0.469.0 specifier: 0.469.0
version: 0.469.0(react@18.3.1) version: 0.469.0(react@18.3.1)
@ -1950,6 +1971,9 @@ importers:
monaco-themes: monaco-themes:
specifier: 0.4.4 specifier: 0.4.4
version: 0.4.4 version: 0.4.4
query-string:
specifier: 9.1.1
version: 9.1.1
react: react:
specifier: 18.3.1 specifier: 18.3.1
version: 18.3.1 version: 18.3.1
@ -1971,6 +1995,9 @@ importers:
react-icons: react-icons:
specifier: 5.4.0 specifier: 5.4.0
version: 5.4.0(react@18.3.1) version: 5.4.0(react@18.3.1)
react-resizable-panels:
specifier: 2.1.7
version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-select: react-select:
specifier: 5.9.0 specifier: 5.9.0
version: 5.9.0(@babel/core@7.26.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 5.9.0(@babel/core@7.26.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -1992,6 +2019,9 @@ importers:
react-window: react-window:
specifier: 1.8.11 specifier: 1.8.11
version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts:
specifier: 2.15.1
version: 2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
regenerator-runtime: regenerator-runtime:
specifier: 0.14.1 specifier: 0.14.1
version: 0.14.1 version: 0.14.1
@ -2155,9 +2185,15 @@ importers:
scripts: scripts:
devDependencies: devDependencies:
'@faker-js/faker':
specifier: 9.9.0
version: 9.9.0
'@graphql-hive/core': '@graphql-hive/core':
specifier: workspace:* specifier: workspace:*
version: link:../packages/libraries/core/dist version: link:../packages/libraries/core/dist
immer:
specifier: 10.1.1
version: 10.1.1
packages: packages:
@ -2997,10 +3033,6 @@ packages:
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/template@7.25.9':
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'}
'@babel/template@7.26.9': '@babel/template@7.26.9':
resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -3581,6 +3613,10 @@ packages:
resolution: {integrity: sha512-o41riCGPiOEStayoikBCAqwa6igbv9L9rP+k5UCfQ24EJD/wGrdDs/KTNwkHG5JzDK3T60D5dMkWkLKEPy8gjA==} resolution: {integrity: sha512-o41riCGPiOEStayoikBCAqwa6igbv9L9rP+k5UCfQ24EJD/wGrdDs/KTNwkHG5JzDK3T60D5dMkWkLKEPy8gjA==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@faker-js/faker@9.9.0':
resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==}
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
'@fastify/accept-negotiator@1.1.0': '@fastify/accept-negotiator@1.1.0':
resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -3624,6 +3660,7 @@ packages:
'@fastify/vite@6.0.7': '@fastify/vite@6.0.7':
resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==} resolution: {integrity: sha512-+dRo9KUkvmbqdmBskG02SwigWl06Mwkw8SBDK1zTNH6vd4DyXbRvI7RmJEmBkLouSU81KTzy1+OzwHSffqSD6w==}
bundledDependencies: []
'@floating-ui/core@1.2.6': '@floating-ui/core@1.2.6':
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==} resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
@ -8299,6 +8336,9 @@ packages:
'@types/crypto-js@4.2.2': '@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
'@types/d3-array@3.2.2': '@types/d3-array@3.2.2':
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
@ -8474,12 +8514,18 @@ packages:
'@types/json5@0.0.29': '@types/json5@0.0.29':
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
'@types/k6@1.2.0':
resolution: {integrity: sha512-7F/vjekOUCzFi5AY+yYBOL38iEtN9EXufdKOAMFkJsdLryJ3/bB/W9jppED1FO5o7zeaPf8M46ardYPD8xgy/w==}
'@types/katex@0.16.7': '@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/keyv@3.1.4': '@types/keyv@3.1.4':
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
'@types/lodash.debounce@4.0.9':
resolution: {integrity: sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==}
'@types/lodash.sortby@4.7.9': '@types/lodash.sortby@4.7.9':
resolution: {integrity: sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==} resolution: {integrity: sha512-PDmjHnOlndLS59GofH0pnxIs+n9i4CWeXGErSB5JyNFHu2cmvW6mQOaUKjG8EDPkni14IgF8NsRW8bKvFzTm9A==}
@ -8887,6 +8933,15 @@ packages:
resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==}
engines: {node: '>=14.6'} engines: {node: '>=14.6'}
'@xyflow/react@12.4.4':
resolution: {integrity: sha512-9RZ9dgKZNJOlbrXXST5HPb5TcXPOIDGondjwcjDro44OQRPl1E0ZRPTeWPGaQtVjbg4WpR4BUYwOeshNI2TuVg==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@xyflow/system@0.0.52':
resolution: {integrity: sha512-pJBMaoh/GEebIABWEIxAai0yf57dm+kH7J/Br+LnLFPuJL87Fhcmm4KFWd/bCUy/kCWUg+2/yFAGY0AUHRPOnQ==}
abbrev@1.1.1: abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
@ -9601,6 +9656,9 @@ packages:
class-variance-authority@0.7.1: class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
clean-regexp@1.0.0: clean-regexp@1.0.0:
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -10133,6 +10191,11 @@ packages:
dataloader@2.2.3: dataloader@2.2.3:
resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==}
date-fns-tz@3.2.0:
resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
peerDependencies:
date-fns: ^3.0.0 || ^4.0.0
date-fns@2.30.0: date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'} engines: {node: '>=0.11'}
@ -10203,9 +10266,16 @@ packages:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decode-named-character-reference@1.0.2: decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
decode-uri-component@0.4.1:
resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==}
engines: {node: '>=14.16'}
decompress-response@6.0.0: decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -10934,6 +11004,10 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-equals@5.2.2:
resolution: {integrity: sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==}
engines: {node: '>=6.0.0'}
fast-glob@3.3.2: fast-glob@3.3.2:
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@ -11072,6 +11146,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
filter-obj@5.1.0:
resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==}
engines: {node: '>=14.16'}
finalhandler@1.3.1: finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -11886,6 +11964,9 @@ packages:
immediate@3.0.6: immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@10.1.1:
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
immer@10.1.3: immer@10.1.3:
resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==}
@ -12490,6 +12571,9 @@ packages:
resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==}
engines: {'0': node >=0.6.0} engines: {'0': node >=0.6.0}
jsurl2@2.2.0:
resolution: {integrity: sha512-jFwgc2G7eVMF6/uyxsF+7DbNEg7fj7SRWmJuVfu/SAOK0iIH9gOpLK9FL1jLvENCXS7kYsYcmFxRFQFyQEb7jg==}
jsx-ast-utils@3.3.5: jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -12729,6 +12813,9 @@ packages:
lodash.castarray@4.4.0: lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.defaults@4.2.0: lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
@ -14647,6 +14734,10 @@ packages:
quansync@0.2.11: quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
query-string@9.1.1:
resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==}
engines: {node: '>=18'}
querystringify@2.2.0: querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
@ -14761,6 +14852,9 @@ packages:
react-is@18.2.0: react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-medium-image-zoom@5.3.0: react-medium-image-zoom@5.3.0:
resolution: {integrity: sha512-RCIzVlsKqy3BYgGgYbolUfuvx0aSKC7YhX/IJGEp+WJxsqdIVYJHkBdj++FAj6VD7RiWj6VVmdCfa/9vJE9hZg==} resolution: {integrity: sha512-RCIzVlsKqy3BYgGgYbolUfuvx0aSKC7YhX/IJGEp+WJxsqdIVYJHkBdj++FAj6VD7RiWj6VVmdCfa/9vJE9hZg==}
peerDependencies: peerDependencies:
@ -14811,12 +14905,24 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
react-resizable-panels@2.1.7:
resolution: {integrity: sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-select@5.9.0: react-select@5.9.0:
resolution: {integrity: sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==} resolution: {integrity: sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-smooth@4.0.4:
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-string-replace@1.1.1: react-string-replace@1.1.1:
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==} resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
engines: {node: '>=0.12.0'} engines: {node: '>=0.12.0'}
@ -14942,6 +15048,16 @@ packages:
resolution: {integrity: sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==} resolution: {integrity: sha512-9FHoNjX1yjuesMwuthAmPKabxYQdOgihFYmT5ebXfYGBcnqXZf3WOVz+5foEZ8Y83P4ZY6yQD5GMmtV+pgCCAQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
recharts-scale@0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
recharts@2.15.1:
resolution: {integrity: sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==}
engines: {node: '>=14'}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
recma-build-jsx@1.0.0: recma-build-jsx@1.0.0:
resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==}
@ -15577,6 +15693,10 @@ packages:
split-ca@1.0.1: split-ca@1.0.1:
resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
split-on-first@3.0.0:
resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==}
engines: {node: '>=12'}
split2@4.1.0: split2@4.1.0:
resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==} resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==}
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
@ -16513,6 +16633,11 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
use-sync-external-store@1.4.0:
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
use-sync-external-store@1.5.0: use-sync-external-store@1.5.0:
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
peerDependencies: peerDependencies:
@ -16608,6 +16733,9 @@ packages:
vfile@6.0.1: vfile@6.0.1:
resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==}
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
vite-node@3.2.4: vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -17006,6 +17134,21 @@ packages:
zrender@5.6.1: zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
zustand@4.5.6:
resolution: {integrity: sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zustand@5.0.8: zustand@5.0.8:
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@ -18322,10 +18465,10 @@ snapshots:
'@babel/helper-compilation-targets': 7.25.9 '@babel/helper-compilation-targets': 7.25.9
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
'@babel/helpers': 7.26.10 '@babel/helpers': 7.26.10
'@babel/parser': 7.26.3 '@babel/parser': 7.26.10
'@babel/template': 7.25.9 '@babel/template': 7.26.9
'@babel/traverse': 7.26.4 '@babel/traverse': 7.26.4
'@babel/types': 7.26.3 '@babel/types': 7.26.10
convert-source-map: 2.0.0 convert-source-map: 2.0.0
debug: 4.4.1(supports-color@8.1.1) debug: 4.4.1(supports-color@8.1.1)
gensync: 1.0.0-beta.2 gensync: 1.0.0-beta.2
@ -18635,12 +18778,6 @@ snapshots:
dependencies: dependencies:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
'@babel/template@7.25.9':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.26.3
'@babel/types': 7.26.10
'@babel/template@7.26.9': '@babel/template@7.26.9':
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
@ -18651,7 +18788,7 @@ snapshots:
dependencies: dependencies:
'@babel/code-frame': 7.26.2 '@babel/code-frame': 7.26.2
'@babel/generator': 7.26.3 '@babel/generator': 7.26.3
'@babel/parser': 7.26.3 '@babel/parser': 7.26.10
'@babel/template': 7.26.9 '@babel/template': 7.26.9
'@babel/types': 7.26.10 '@babel/types': 7.26.10
debug: 4.4.1(supports-color@8.1.1) debug: 4.4.1(supports-color@8.1.1)
@ -19339,6 +19476,8 @@ snapshots:
'@esm2cjs/strip-final-newline@3.0.1-cjs.0': {} '@esm2cjs/strip-final-newline@3.0.1-cjs.0': {}
'@faker-js/faker@9.9.0': {}
'@fastify/accept-negotiator@1.1.0': {} '@fastify/accept-negotiator@1.1.0': {}
'@fastify/ajv-compiler@3.6.0': '@fastify/ajv-compiler@3.6.0':
@ -19819,6 +19958,13 @@ snapshots:
object-inspect: 1.12.3 object-inspect: 1.12.3
tslib: 2.6.2 tslib: 2.6.2
'@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@16.11.0)':
dependencies:
dependency-graph: 0.11.0
graphql: 16.11.0
object-inspect: 1.12.3
tslib: 2.6.2
'@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0)': '@graphql-inspector/core@5.1.0-alpha-20231208113249-34700c8a(graphql@16.9.0)':
dependencies: dependencies:
dependency-graph: 0.11.0 dependency-graph: 0.11.0
@ -21524,7 +21670,7 @@ snapshots:
nopt: 7.2.0 nopt: 7.2.0
proc-log: 3.0.0 proc-log: 3.0.0
read-package-json-fast: 3.0.2 read-package-json-fast: 3.0.2
semver: 7.6.3 semver: 7.7.2
walk-up-path: 3.0.1 walk-up-path: 3.0.1
'@npmcli/fs@3.1.0': '@npmcli/fs@3.1.0':
@ -21539,7 +21685,7 @@ snapshots:
proc-log: 3.0.0 proc-log: 3.0.0
promise-inflight: 1.0.1 promise-inflight: 1.0.1
promise-retry: 2.0.1 promise-retry: 2.0.1
semver: 7.6.3 semver: 7.7.2
which: 4.0.0 which: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
- bluebird - bluebird
@ -25772,8 +25918,8 @@ snapshots:
'@types/babel__core@7.20.5': '@types/babel__core@7.20.5':
dependencies: dependencies:
'@babel/parser': 7.26.3 '@babel/parser': 7.26.10
'@babel/types': 7.26.3 '@babel/types': 7.26.10
'@types/babel__generator': 7.6.4 '@types/babel__generator': 7.6.4
'@types/babel__template': 7.4.1 '@types/babel__template': 7.4.1
'@types/babel__traverse': 7.18.3 '@types/babel__traverse': 7.18.3
@ -25784,7 +25930,7 @@ snapshots:
'@types/babel__template@7.4.1': '@types/babel__template@7.4.1':
dependencies: dependencies:
'@babel/parser': 7.26.3 '@babel/parser': 7.26.10
'@babel/types': 7.26.10 '@babel/types': 7.26.10
'@types/babel__traverse@7.18.3': '@types/babel__traverse@7.18.3':
@ -25841,6 +25987,8 @@ snapshots:
'@types/crypto-js@4.2.2': {} '@types/crypto-js@4.2.2': {}
'@types/d3-array@3.2.1': {}
'@types/d3-array@3.2.2': {} '@types/d3-array@3.2.2': {}
'@types/d3-axis@3.0.6': '@types/d3-axis@3.0.6':
@ -26048,12 +26196,18 @@ snapshots:
'@types/json5@0.0.29': {} '@types/json5@0.0.29': {}
'@types/k6@1.2.0': {}
'@types/katex@0.16.7': {} '@types/katex@0.16.7': {}
'@types/keyv@3.1.4': '@types/keyv@3.1.4':
dependencies: dependencies:
'@types/node': 22.10.5 '@types/node': 22.10.5
'@types/lodash.debounce@4.0.9':
dependencies:
'@types/lodash': 4.17.14
'@types/lodash.sortby@4.7.9': '@types/lodash.sortby@4.7.9':
dependencies: dependencies:
'@types/lodash': 4.17.13 '@types/lodash': 4.17.13
@ -26312,7 +26466,7 @@ snapshots:
globby: 11.1.0 globby: 11.1.0
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
semver: 7.6.3 semver: 7.7.2
ts-api-utils: 1.3.0(typescript@5.7.3) ts-api-utils: 1.3.0(typescript@5.7.3)
optionalDependencies: optionalDependencies:
typescript: 5.7.3 typescript: 5.7.3
@ -26557,6 +26711,27 @@ snapshots:
'@xmldom/xmldom@0.9.8': {} '@xmldom/xmldom@0.9.8': {}
'@xyflow/react@12.4.4(@types/react@18.3.18)(immer@10.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@xyflow/system': 0.0.52
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.6(@types/react@18.3.18)(immer@10.1.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.52':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
abbrev@1.1.1: {} abbrev@1.1.1: {}
abbrev@2.0.0: {} abbrev@2.0.0: {}
@ -27398,6 +27573,8 @@ snapshots:
dependencies: dependencies:
clsx: 2.1.1 clsx: 2.1.1
classcat@5.0.5: {}
clean-regexp@1.0.0: clean-regexp@1.0.0:
dependencies: dependencies:
escape-string-regexp: 1.0.5 escape-string-regexp: 1.0.5
@ -27981,6 +28158,10 @@ snapshots:
dataloader@2.2.3: {} dataloader@2.2.3: {}
date-fns-tz@3.2.0(date-fns@4.1.0):
dependencies:
date-fns: 4.1.0
date-fns@2.30.0: date-fns@2.30.0:
dependencies: dependencies:
'@babel/runtime': 7.26.10 '@babel/runtime': 7.26.10
@ -28025,10 +28206,14 @@ snapshots:
decamelize@1.2.0: {} decamelize@1.2.0: {}
decimal.js-light@2.5.1: {}
decode-named-character-reference@1.0.2: decode-named-character-reference@1.0.2:
dependencies: dependencies:
character-entities: 2.0.2 character-entities: 2.0.2
decode-uri-component@0.4.1: {}
decompress-response@6.0.0: decompress-response@6.0.0:
dependencies: dependencies:
mimic-response: 3.1.0 mimic-response: 3.1.0
@ -29008,6 +29193,8 @@ snapshots:
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-equals@5.2.2: {}
fast-glob@3.3.2: fast-glob@3.3.2:
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@ -29173,6 +29360,8 @@ snapshots:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
filter-obj@5.1.0: {}
finalhandler@1.3.1: finalhandler@1.3.1:
dependencies: dependencies:
debug: 2.6.9 debug: 2.6.9
@ -30244,6 +30433,8 @@ snapshots:
immediate@3.0.6: {} immediate@3.0.6: {}
immer@10.1.1: {}
immer@10.1.3: {} immer@10.1.3: {}
immutable@3.7.6: {} immutable@3.7.6: {}
@ -30747,7 +30938,7 @@ snapshots:
acorn: 8.14.0 acorn: 8.14.0
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
espree: 9.6.1 espree: 9.6.1
semver: 7.6.3 semver: 7.7.2
jsonfile@4.0.0: jsonfile@4.0.0:
optionalDependencies: optionalDependencies:
@ -30772,7 +30963,7 @@ snapshots:
lodash.isstring: 4.0.1 lodash.isstring: 4.0.1
lodash.once: 4.1.1 lodash.once: 4.1.1
ms: 2.1.3 ms: 2.1.3
semver: 7.6.3 semver: 7.7.2
jsox@1.2.119: {} jsox@1.2.119: {}
@ -30783,6 +30974,8 @@ snapshots:
json-schema: 0.4.0 json-schema: 0.4.0
verror: 1.10.0 verror: 1.10.0
jsurl2@2.2.0: {}
jsx-ast-utils@3.3.5: jsx-ast-utils@3.3.5:
dependencies: dependencies:
array-includes: 3.1.7 array-includes: 3.1.7
@ -31033,6 +31226,8 @@ snapshots:
lodash.castarray@4.4.0: {} lodash.castarray@4.4.0: {}
lodash.debounce@4.0.8: {}
lodash.defaults@4.2.0: {} lodash.defaults@4.2.0: {}
lodash.get@4.4.2: {} lodash.get@4.4.2: {}
@ -32481,7 +32676,7 @@ snapshots:
make-fetch-happen: 13.0.0 make-fetch-happen: 13.0.0
nopt: 7.2.0 nopt: 7.2.0
proc-log: 3.0.0 proc-log: 3.0.0
semver: 7.6.3 semver: 7.7.2
tar: 6.2.1 tar: 6.2.1
which: 4.0.0 which: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
@ -33489,6 +33684,12 @@ snapshots:
quansync@0.2.11: {} quansync@0.2.11: {}
query-string@9.1.1:
dependencies:
decode-uri-component: 0.4.1
filter-obj: 5.1.0
split-on-first: 3.0.0
querystringify@2.2.0: {} querystringify@2.2.0: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
@ -33611,6 +33812,8 @@ snapshots:
react-is@18.2.0: {} react-is@18.2.0: {}
react-is@18.3.1: {}
react-medium-image-zoom@5.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): react-medium-image-zoom@5.3.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies: dependencies:
react: 19.0.0 react: 19.0.0
@ -33662,6 +33865,11 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 18.3.18 '@types/react': 18.3.18
react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-select@5.9.0(@babel/core@7.26.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-select@5.9.0(@babel/core@7.26.0)(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.26.10 '@babel/runtime': 7.26.10
@ -33680,6 +33888,14 @@ snapshots:
- '@types/react' - '@types/react'
- supports-color - supports-color
react-smooth@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
fast-equals: 5.2.2
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-string-replace@1.1.1: {} react-string-replace@1.1.1: {}
react-style-singleton@2.2.3(@types/react@18.3.18)(react@18.3.1): react-style-singleton@2.2.3(@types/react@18.3.18)(react@18.3.1):
@ -33825,6 +34041,23 @@ snapshots:
tiny-invariant: 1.3.3 tiny-invariant: 1.3.3
tslib: 2.8.1 tslib: 2.8.1
recharts-scale@0.4.5:
dependencies:
decimal.js-light: 2.5.1
recharts@2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
clsx: 2.1.1
eventemitter3: 4.0.7
lodash: 4.17.21
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 18.3.1
react-smooth: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts-scale: 0.4.5
tiny-invariant: 1.3.3
victory-vendor: 36.9.2
recma-build-jsx@1.0.0: recma-build-jsx@1.0.0:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -34695,6 +34928,8 @@ snapshots:
split-ca@1.0.1: {} split-ca@1.0.1: {}
split-on-first@3.0.0: {}
split2@4.1.0: {} split2@4.1.0: {}
sponge-case@1.0.1: sponge-case@1.0.1:
@ -35711,6 +35946,10 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
use-sync-external-store@1.4.0(react@18.3.1):
dependencies:
react: 18.3.1
use-sync-external-store@1.5.0(react@18.3.1): use-sync-external-store@1.5.0(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@ -35818,6 +36057,23 @@ snapshots:
unist-util-stringify-position: 4.0.0 unist-util-stringify-position: 4.0.0
vfile-message: 4.0.2 vfile-message: 4.0.2
victory-vendor@36.9.2:
dependencies:
'@types/d3-array': 3.2.1
'@types/d3-ease': 3.0.2
'@types/d3-interpolate': 3.0.4
'@types/d3-scale': 4.0.9
'@types/d3-shape': 3.1.7
'@types/d3-time': 3.0.4
'@types/d3-timer': 3.0.2
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
vite-node@3.2.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): vite-node@3.2.4(@types/node@22.10.5)(jiti@2.3.3)(less@4.2.0)(lightningcss@1.28.2)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
@ -36256,6 +36512,14 @@ snapshots:
dependencies: dependencies:
tslib: 2.3.0 tslib: 2.3.0
zustand@4.5.6(@types/react@18.3.18)(immer@10.1.3)(react@18.3.1):
dependencies:
use-sync-external-store: 1.4.0(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.18
immer: 10.1.3
react: 18.3.1
zustand@5.0.8(@types/react@18.3.18)(immer@10.1.3)(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)): zustand@5.0.8(@types/react@18.3.18)(immer@10.1.3)(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)):
optionalDependencies: optionalDependencies:
'@types/react': 18.3.18 '@types/react': 18.3.18

View file

@ -9,3 +9,4 @@ packages:
- deployment - deployment
- scripts - scripts
- rules - rules
- load-tests/otel-traces

View file

@ -3,6 +3,8 @@
"type": "module", "type": "module",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@graphql-hive/core": "workspace:*" "@faker-js/faker": "9.9.0",
"@graphql-hive/core": "workspace:*",
"immer": "10.1.1"
} }
} }

View file

@ -0,0 +1,32 @@
## Readme
Each of the JSON file in this folder contains an array of requests sent to the otel-collector for
the specific GraphQL operation.
All the files will be parsed and used as a template for seeding the clickhouse database with somehow
realistic trace data.
### Usage
- Create a federation target and publish our federation example
(https://the-guild.dev/graphql/hive/docs/get-started/apollo-federation).
- Create a organization access token and target to which you want to push the data.
Then run the following script:
```
HIVE_ORGANIZATION_ACCESS_TOKEN=hvo1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx= \
HIVE_TARGET_REF=the-guild/otel-demo/development \
node --experimental-strip-types seed-traces.mts
```
You can adjust the amount of days you want to seed by setting `USAGE_DAYS` (e.g. `USAGE_DAYS=5` will
seed the last 5 days). By default we seed the last 14 days.
You can adjust how frequent the reporting interval is by setting `USAGE_INTERVAL` (e.g.
`USAGE_DAYS=20`). By default the value is 20 (minutes).
### Adding more fixtures
In case you want to add additional traces starte the intercept server (`interspect-service.mts`),
point the gateway to that endpoint and copy the output to a new file within this folder.

View file

@ -0,0 +1,10 @@
import * as http from 'node:http';
import * as foo from '@whatwg-node/server';
const adapter = foo.createServerAdapter(async request => {
console.log(JSON.stringify(await request.json(), null, 2));
return new Response('{}');
});
http.createServer(adapter).listen(9898);

View file

@ -0,0 +1,296 @@
[
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "f42cb37f3811f650f06ca078e7c6625f",
"spanId": "c55a1a27cfe0a563",
"parentSpanId": "6df96e8ebb5f3dd7",
"name": "graphql.parse",
"kind": 1,
"startTimeUnixNano": "1751541577384000000",
"endTimeUnixNano": "1751541577385261166",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "IntrospectionQuery"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "f42cb37f3811f650f06ca078e7c6625f",
"spanId": "ca97bb80da3c0dbe",
"parentSpanId": "6df96e8ebb5f3dd7",
"name": "graphql.validate",
"kind": 1,
"startTimeUnixNano": "1751541577385000000",
"endTimeUnixNano": "1751541577397343958",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "IntrospectionQuery"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "f42cb37f3811f650f06ca078e7c6625f",
"spanId": "697d38f300df2167",
"parentSpanId": "6df96e8ebb5f3dd7",
"name": "graphql.context",
"kind": 1,
"startTimeUnixNano": "1751541577398000000",
"endTimeUnixNano": "1751541577399414208",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "f42cb37f3811f650f06ca078e7c6625f",
"spanId": "f479d33b7320cd0f",
"parentSpanId": "6df96e8ebb5f3dd7",
"name": "graphql.execute",
"kind": 1,
"startTimeUnixNano": "1751541577400000000",
"endTimeUnixNano": "1751541577410081959",
"attributes": [
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "IntrospectionQuery"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "f42cb37f3811f650f06ca078e7c6625f",
"spanId": "6df96e8ebb5f3dd7",
"parentSpanId": "af8929e2e40e00af",
"name": "graphql.operation IntrospectionQuery",
"kind": 1,
"startTimeUnixNano": "1751541577382000000",
"endTimeUnixNano": "1751541577409557042",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "IntrospectionQuery"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "f42cb37f3811f650f06ca078e7c6625f",
"spanId": "af8929e2e40e00af",
"name": "query IntrospectionQuery",
"kind": 2,
"startTimeUnixNano": "1751541577154000000",
"endTimeUnixNano": "1751541577412387709",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "http://localhost:4000/graphql"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/graphql"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "http:"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "localhost"
}
},
{
"key": "http.host",
"value": {
"stringValue": "localhost:4000"
}
},
{
"key": "http.user_agent",
"value": {
"stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "IntrospectionQuery"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}"
}
},
{
"key": "hive.graphql.error.count",
"value": {
"intValue": 0
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
},
{
"key": "hive.graphql",
"value": {
"boolValue": true
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
}
]

View file

@ -0,0 +1,788 @@
[
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "a64ae7143330edd5",
"parentSpanId": "53b24c8e7ff72fa4",
"name": "graphql.parse",
"kind": 1,
"startTimeUnixNano": "1751541696992000000",
"endTimeUnixNano": "1751541696992404084",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{me{id username name reviews{id product{upc name}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "a58c9a61e1c74cee",
"parentSpanId": "53b24c8e7ff72fa4",
"name": "graphql.validate",
"kind": 1,
"startTimeUnixNano": "1751541696993000000",
"endTimeUnixNano": "1751541696994822625",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{me{id username name reviews{id product{upc name}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "8fc953974f3a8242",
"parentSpanId": "53b24c8e7ff72fa4",
"name": "graphql.context",
"kind": 1,
"startTimeUnixNano": "1751541696994000000",
"endTimeUnixNano": "1751541696994156500",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "db7a050eb7c01cc6",
"parentSpanId": "53e7ea110fc140bc",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541697013000000",
"endTimeUnixNano": "1751541697246498001",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/users"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/users"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "53e7ea110fc140bc",
"parentSpanId": "5950033302028628",
"name": "subgraph.execute (users)",
"kind": 3,
"startTimeUnixNano": "1751541697012000000",
"endTimeUnixNano": "1751541697247851792",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{__typename me{__typename id id username name}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{__typename me{__typename id id username name}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "600df68244bcc074",
"parentSpanId": "3fa530a61e87352d",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541697254000000",
"endTimeUnixNano": "1751541697287733167",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/reviews"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/reviews"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "3fa530a61e87352d",
"parentSpanId": "5950033302028628",
"name": "subgraph.execute (reviews)",
"kind": 3,
"startTimeUnixNano": "1751541697254000000",
"endTimeUnixNano": "1751541697288384625",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on User{id reviews{__typename id id product{__typename upc upc}}}}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "reviews"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "reviews"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on User{id reviews{__typename id id product{__typename upc upc}}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "e6f23702d0c6dca1",
"parentSpanId": "01288c4b66e39646",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541697291000000",
"endTimeUnixNano": "1751541697328195667",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/products"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/products"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "01288c4b66e39646",
"parentSpanId": "5950033302028628",
"name": "subgraph.execute (products)",
"kind": 3,
"startTimeUnixNano": "1751541697291000000",
"endTimeUnixNano": "1751541697328841166",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on Product{upc name}}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "products"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "products"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on Product{upc name}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "5950033302028628",
"parentSpanId": "53b24c8e7ff72fa4",
"name": "graphql.execute",
"kind": 1,
"startTimeUnixNano": "1751541696995000000",
"endTimeUnixNano": "1751541697329399042",
"attributes": [
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{me{id username name reviews{id product{upc name}}}}"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "53b24c8e7ff72fa4",
"parentSpanId": "af9ade51d3bdaa1e",
"name": "graphql.operation MyProfile",
"kind": 1,
"startTimeUnixNano": "1751541696992000000",
"endTimeUnixNano": "1751541697329427167",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{me{id username name reviews{id product{upc name}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "807b71f8b7b4f9c827ce8220c6540eac",
"spanId": "af9ade51d3bdaa1e",
"name": "query MyProfile",
"kind": 2,
"startTimeUnixNano": "1751541696982000000",
"endTimeUnixNano": "1751541697329384584",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "http://localhost:4000/graphql"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/graphql"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "http:"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "localhost"
}
},
{
"key": "http.host",
"value": {
"stringValue": "localhost:4000"
}
},
{
"key": "http.user_agent",
"value": {
"stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
},
{
"key": "hive.gateway.operation.subgraph.names",
"value": {
"arrayValue": {
"values": [
{ "stringValue": "users" },
{ "stringValue": "reviews" },
{ "stringValue": "products" }
]
}
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "MyProfile"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query MyProfile{me{id username name reviews{id product{upc name}}}}"
}
},
{
"key": "hive.graphql.error.count",
"value": {
"intValue": 0
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
},
{
"key": "hive.graphql",
"value": {
"boolValue": true
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
}
]

View file

@ -0,0 +1,788 @@
[
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "1b80c5676106d88c",
"parentSpanId": "94c73be93cd98d46",
"name": "graphql.parse",
"kind": 1,
"startTimeUnixNano": "1751541486715000000",
"endTimeUnixNano": "1751541486715238750",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview{topProducts{name upc price weight reviews{id body author{id username name}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "d7d5f35afa053c99",
"parentSpanId": "94c73be93cd98d46",
"name": "graphql.validate",
"kind": 1,
"startTimeUnixNano": "1751541486715000000",
"endTimeUnixNano": "1751541486715310750",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview{topProducts{name upc price weight reviews{id body author{id username name}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "2ea02406bc1b76d1",
"parentSpanId": "94c73be93cd98d46",
"name": "graphql.context",
"kind": 1,
"startTimeUnixNano": "1751541486716000000",
"endTimeUnixNano": "1751541486716082041",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "4b3aa6b08fd35de1",
"parentSpanId": "2707eb31347c2542",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541486718000000",
"endTimeUnixNano": "1751541486925992541",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/products"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/products"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "2707eb31347c2542",
"parentSpanId": "0deba53eb2628007",
"name": "subgraph.execute (products)",
"kind": 3,
"startTimeUnixNano": "1751541486718000000",
"endTimeUnixNano": "1751541486926520250",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview($first:Int){__typename topProducts(first:$first){__typename upc name upc price weight}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "products"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "products"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview($first:Int){__typename topProducts(first:$first){__typename upc name upc price weight}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "f81b8ed0b08b23f7",
"parentSpanId": "233473cfb358e83f",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541486928000000",
"endTimeUnixNano": "1751541486959338875",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/reviews"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/reviews"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "233473cfb358e83f",
"parentSpanId": "0deba53eb2628007",
"name": "subgraph.execute (reviews)",
"kind": 3,
"startTimeUnixNano": "1751541486927000000",
"endTimeUnixNano": "1751541486959231209",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on Product{upc reviews{__typename id id body author{username __typename __typename id id}}}}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "reviews"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "reviews"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on Product{upc reviews{__typename id id body author{username __typename __typename id id}}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "f8652f4f214127da",
"parentSpanId": "6bfe00463408e3de",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541486962000000",
"endTimeUnixNano": "1751541486998254500",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/users"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/users"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "6bfe00463408e3de",
"parentSpanId": "0deba53eb2628007",
"name": "subgraph.execute (users)",
"kind": 3,
"startTimeUnixNano": "1751541486961000000",
"endTimeUnixNano": "1751541486998039417",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on User{id name}}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on User{id name}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "0deba53eb2628007",
"parentSpanId": "94c73be93cd98d46",
"name": "graphql.execute",
"kind": 1,
"startTimeUnixNano": "1751541486716000000",
"endTimeUnixNano": "1751541486999119791",
"attributes": [
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview{topProducts{name upc price weight reviews{id body author{id username name}}}}"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "94c73be93cd98d46",
"parentSpanId": "93b59a82d5aab081",
"name": "graphql.operation TopProductsOverview",
"kind": 1,
"startTimeUnixNano": "1751541486715000000",
"endTimeUnixNano": "1751541486999203084",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview{topProducts{name upc price weight reviews{id body author{id username name}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "beec0bf1fc0eed2ef7721f6b4827be86",
"spanId": "93b59a82d5aab081",
"name": "query TopProductsOverview",
"kind": 2,
"startTimeUnixNano": "1751541486711000000",
"endTimeUnixNano": "1751541486998914125",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "http://localhost:4000/graphql"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/graphql"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "http:"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "localhost"
}
},
{
"key": "http.host",
"value": {
"stringValue": "localhost:4000"
}
},
{
"key": "http.user_agent",
"value": {
"stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
},
{
"key": "hive.gateway.operation.subgraph.names",
"value": {
"arrayValue": {
"values": [
{ "stringValue": "users" },
{ "stringValue": "reviews" },
{ "stringValue": "products" }
]
}
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "TopProductsOverview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query TopProductsOverview{topProducts{name upc price weight reviews{id body author{id username name}}}}"
}
},
{
"key": "hive.graphql.error.count",
"value": {
"intValue": 0
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
},
{
"key": "hive.graphql",
"value": {
"boolValue": true
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
}
]

View file

@ -0,0 +1,330 @@
[
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "b05d5ce706fe45f33fd02f12ff2b538c",
"spanId": "0424db5a14505873",
"parentSpanId": "80d07c2b29e6a98f",
"name": "graphql.parse",
"kind": 1,
"startTimeUnixNano": "1751541923666000000",
"endTimeUnixNano": "1751541923666275959",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "b05d5ce706fe45f33fd02f12ff2b538c",
"spanId": "c2b0e3a5107b7d8d",
"parentSpanId": "80d07c2b29e6a98f",
"name": "graphql.validate",
"kind": 1,
"startTimeUnixNano": "1751541923666000000",
"endTimeUnixNano": "1751541923666099959",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "b05d5ce706fe45f33fd02f12ff2b538c",
"spanId": "51a8cf06eab30356",
"parentSpanId": "80d07c2b29e6a98f",
"name": "graphql.context",
"kind": 1,
"startTimeUnixNano": "1751541923666000000",
"endTimeUnixNano": "1751541923666065666",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "b05d5ce706fe45f33fd02f12ff2b538c",
"spanId": "262de13a78519369",
"parentSpanId": "80d07c2b29e6a98f",
"name": "graphql.execute",
"kind": 1,
"startTimeUnixNano": "1751541923666000000",
"endTimeUnixNano": "1751541923667922500",
"attributes": [
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.error.count",
"value": {
"intValue": 1
}
}
],
"droppedAttributesCount": 0,
"events": [
{
"attributes": [
{
"key": "exception.type",
"value": {
"stringValue": "GraphQLError"
}
},
{
"key": "exception.message",
"value": {
"stringValue": "Variable \"$id\" of required type \"ID!\" was not provided."
}
},
{
"key": "exception.stacktrace",
"value": {
"stringValue": "GraphQLError: Variable \"$id\" of required type \"ID!\" was not provided.\n at createGraphQLError (file:///node_modules/.chunk/abortSignalAny-BG8Lg0X_.mjs:29:12)\n at coerceVariableValues (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:566:25)\n at getVariableValues (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:533:25)\n at buildExecutionContext (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:746:35)\n at execute (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:615:24)\n at file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:1960:37\n at Promise.then (file:///node_modules/.chunk/index-DYl9M8N-.mjs:31:40)\n at handleMaybePromise (file:///node_modules/.chunk/index-DYl9M8N-.mjs:10:33)\n at normalizedExecutor (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:1960:12)\n at _4.args (file:///node_modules/.chunk/use-engine-CPlZaEw6.mjs:580:17)"
}
}
],
"name": "exception",
"timeUnixNano": "1751541923667636834",
"droppedAttributesCount": 0
}
],
"droppedEventsCount": 0,
"status": {
"code": 2,
"message": "Variable \"$id\" of required type \"ID!\" was not provided."
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "b05d5ce706fe45f33fd02f12ff2b538c",
"spanId": "80d07c2b29e6a98f",
"parentSpanId": "86ebca8621ea012f",
"name": "graphql.operation UserReview",
"kind": 1,
"startTimeUnixNano": "1751541923665000000",
"endTimeUnixNano": "1751541923667755750",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "b05d5ce706fe45f33fd02f12ff2b538c",
"spanId": "86ebca8621ea012f",
"name": "query UserReview",
"kind": 2,
"startTimeUnixNano": "1751541923662000000",
"endTimeUnixNano": "1751541923668269583",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "http://localhost:4000/graphql"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/graphql"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "http:"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "localhost"
}
},
{
"key": "http.host",
"value": {
"stringValue": "localhost:4000"
}
},
{
"key": "http.user_agent",
"value": {
"stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "hive.graphql.error.count",
"value": {
"intValue": 1
}
},
{
"key": "http.status_code",
"value": {
"intValue": 400
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
},
{
"key": "hive.graphql",
"value": {
"boolValue": true
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 2,
"message": "Bad Request"
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
}
]

View file

@ -0,0 +1,464 @@
[
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "1a647eb9b4147b10",
"parentSpanId": "608141aa264650c6",
"name": "graphql.parse",
"kind": 1,
"startTimeUnixNano": "1751541851834000000",
"endTimeUnixNano": "1751541851834925292",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "2225b5183449d944",
"parentSpanId": "608141aa264650c6",
"name": "graphql.validate",
"kind": 1,
"startTimeUnixNano": "1751541851836000000",
"endTimeUnixNano": "1751541851838385167",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "22a0be695f656a93",
"parentSpanId": "608141aa264650c6",
"name": "graphql.context",
"kind": 1,
"startTimeUnixNano": "1751541851838000000",
"endTimeUnixNano": "1751541851838188625",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "f567c64730c382b4",
"parentSpanId": "49624b099fea77b7",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541851841000000",
"endTimeUnixNano": "1751541852120716584",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/users"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/users"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "49624b099fea77b7",
"parentSpanId": "c0d8db7a0d7cd325",
"name": "subgraph.execute (users)",
"kind": 3,
"startTimeUnixNano": "1751541851840000000",
"endTimeUnixNano": "1751541852121396834",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){__typename user(id:$id){__typename id id}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){__typename user(id:$id){__typename id id}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "c0d8db7a0d7cd325",
"parentSpanId": "608141aa264650c6",
"name": "graphql.execute",
"kind": 1,
"startTimeUnixNano": "1751541851838000000",
"endTimeUnixNano": "1751541852121068708",
"attributes": [
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "608141aa264650c6",
"parentSpanId": "9bacab5be0803b57",
"name": "graphql.operation UserReview",
"kind": 1,
"startTimeUnixNano": "1751541851834000000",
"endTimeUnixNano": "1751541852121572417",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "395970847a47f14794c1c1af2499eb94",
"spanId": "9bacab5be0803b57",
"name": "query UserReview",
"kind": 2,
"startTimeUnixNano": "1751541851832000000",
"endTimeUnixNano": "1751541852121678542",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "http://localhost:4000/graphql"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/graphql"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "http:"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "localhost"
}
},
{
"key": "http.host",
"value": {
"stringValue": "localhost:4000"
}
},
{
"key": "http.user_agent",
"value": {
"stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
},
{
"key": "hive.gateway.operation.subgraph.names",
"value": {
"arrayValue": {
"values": [{ "stringValue": "users" }]
}
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "hive.graphql.error.count",
"value": {
"intValue": 0
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
},
{
"key": "hive.graphql",
"value": {
"boolValue": true
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
}
]

View file

@ -0,0 +1,788 @@
[
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "8e7742a266dfa50e",
"parentSpanId": "b0d7fd9d6fc79c8e",
"name": "graphql.parse",
"kind": 1,
"startTimeUnixNano": "1751541997641000000",
"endTimeUnixNano": "1751541997641163583",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "88da58c0b0307abc",
"parentSpanId": "b0d7fd9d6fc79c8e",
"name": "graphql.validate",
"kind": 1,
"startTimeUnixNano": "1751541997642000000",
"endTimeUnixNano": "1751541997642059625",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "8b990366087e0267",
"parentSpanId": "b0d7fd9d6fc79c8e",
"name": "graphql.context",
"kind": 1,
"startTimeUnixNano": "1751541997642000000",
"endTimeUnixNano": "1751541997642070875",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "a082a6e591be0dc0",
"parentSpanId": "034aa685d1130fa9",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541997644000000",
"endTimeUnixNano": "1751541997808645667",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/users"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/users"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "034aa685d1130fa9",
"parentSpanId": "4709e78906f7ee93",
"name": "subgraph.execute (users)",
"kind": 3,
"startTimeUnixNano": "1751541997643000000",
"endTimeUnixNano": "1751541997808068667",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){__typename user(id:$id){__typename id id}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "users"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){__typename user(id:$id){__typename id id}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "0cbf86b7f4e5a251",
"parentSpanId": "879cef6dd0df538d",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541997811000000",
"endTimeUnixNano": "1751541997859345000",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/reviews"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/reviews"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "879cef6dd0df538d",
"parentSpanId": "4709e78906f7ee93",
"name": "subgraph.execute (reviews)",
"kind": 3,
"startTimeUnixNano": "1751541997810000000",
"endTimeUnixNano": "1751541997859110458",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on User{id reviews{__typename id id product{__typename upc upc}body}}}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "reviews"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "reviews"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on User{id reviews{__typename id id product{__typename upc upc}body}}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
},
{
"resourceSpans": [
{
"resource": {
"attributes": [
{
"key": "service.name",
"value": {
"stringValue": "hive-gateway"
}
},
{
"key": "service.version",
"value": {
"stringValue": "5.13.4"
}
}
],
"droppedAttributesCount": 0
},
"scopeSpans": [
{
"scope": {
"name": "gateway"
},
"spans": [
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "242481d587116eb4",
"parentSpanId": "37bb599ff9427274",
"name": "http.fetch",
"kind": 3,
"startTimeUnixNano": "1751541997861000000",
"endTimeUnixNano": "1751541997902277625",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "https://federation-demo.theguild.workers.dev/products"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.host",
"value": {
"stringValue": "federation-demo.theguild.workers.dev"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/products"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "https:"
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "37bb599ff9427274",
"parentSpanId": "4709e78906f7ee93",
"name": "subgraph.execute (products)",
"kind": 3,
"startTimeUnixNano": "1751541997861000000",
"endTimeUnixNano": "1751541997903041792",
"attributes": [
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on Product{upc name weight}}}"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.upstream.subgraph.name",
"value": {
"stringValue": "products"
}
},
{
"key": "hive.gateway.upstream.subgraph.name",
"value": {
"stringValue": "products"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($representations:[_Any!]!){__typename _entities(representations:$representations){__typename ...on Product{upc name weight}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "4709e78906f7ee93",
"parentSpanId": "b0d7fd9d6fc79c8e",
"name": "graphql.execute",
"kind": 1,
"startTimeUnixNano": "1751541997642000000",
"endTimeUnixNano": "1751541997903080750",
"attributes": [
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "b0d7fd9d6fc79c8e",
"parentSpanId": "bbc7518b553093df",
"name": "graphql.operation UserReview",
"kind": 1,
"startTimeUnixNano": "1751541997641000000",
"endTimeUnixNano": "1751541997902778250",
"attributes": [
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 0
},
"links": [],
"droppedLinksCount": 0
},
{
"traceId": "8f7b3cc381c16ab7a1ff9f5e95b7076b",
"spanId": "bbc7518b553093df",
"name": "query UserReview",
"kind": 2,
"startTimeUnixNano": "1751541997638000000",
"endTimeUnixNano": "1751541997902695084",
"attributes": [
{
"key": "http.method",
"value": {
"stringValue": "POST"
}
},
{
"key": "http.url",
"value": {
"stringValue": "http://localhost:4000/graphql"
}
},
{
"key": "http.route",
"value": {
"stringValue": "/graphql"
}
},
{
"key": "http.scheme",
"value": {
"stringValue": "http:"
}
},
{
"key": "net.host.name",
"value": {
"stringValue": "localhost"
}
},
{
"key": "http.host",
"value": {
"stringValue": "localhost:4000"
}
},
{
"key": "http.user_agent",
"value": {
"stringValue": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
}
},
{
"key": "hive.gateway.operation.subgraph.names",
"value": {
"arrayValue": {
"values": [
{ "stringValue": "users" },
{ "stringValue": "reviews" },
{ "stringValue": "products" }
]
}
}
},
{
"key": "graphql.operation.type",
"value": {
"stringValue": "query"
}
},
{
"key": "graphql.operation.name",
"value": {
"stringValue": "UserReview"
}
},
{
"key": "graphql.document",
"value": {
"stringValue": "query UserReview($id:ID!){user(id:$id){id reviews{id product{upc name weight}body}}}"
}
},
{
"key": "hive.graphql.error.count",
"value": {
"intValue": 0
}
},
{
"key": "http.status_code",
"value": {
"intValue": 200
}
},
{
"key": "gateway.cache.response_cache",
"value": {
"stringValue": "miss"
}
},
{
"key": "hive.graphql",
"value": {
"boolValue": true
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"status": {
"code": 1
},
"links": [],
"droppedLinksCount": 0
}
]
}
]
}
]
}
]

View file

@ -0,0 +1,817 @@
import * as crypto from 'node:crypto';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as date from 'date-fns';
import * as immer from 'immer';
import * as faker from '@faker-js/faker';
// This is just copy pasted here to infer the TS type
// Please feel free to collapse this section.
const reference = [
{
resourceSpans: [
{
resource: {
attributes: [
{
key: 'service.name',
value: {
stringValue: 'hive-gateway',
},
},
{
key: 'service.version',
value: {
stringValue: '5.13.4',
},
},
],
droppedAttributesCount: 0,
},
scopeSpans: [
{
scope: {
name: 'gateway',
},
spans: [
{
traceId: 'd72a6b878beb3fee4287243c1c51ebaa',
spanId: 'bf7578eaabcd9963',
name: 'GET /graphql',
kind: 2,
startTimeUnixNano: '1751541577026000000',
endTimeUnixNano: '1751541577027457584',
attributes: [
{
key: 'http.method',
value: {
stringValue: 'GET',
},
},
{
key: 'http.url',
value: {
stringValue:
'http://localhost:4000/graphql?query=query+TopProductsOverview+%7B%0A++topProducts+%7B%0A++++name%0A++++upc%0A++++price%0A++++weight%0A++++reviews+%7B%0A++++++id%0A++++++body%0A++++++author+%7B%0A++++++++id%0A++++++++username%0A++++++++name%0A++++++%7D%0A++++%7D%0A++%7D%0A%7D',
},
},
{
key: 'http.route',
value: {
stringValue: '/graphql',
},
},
{
key: 'http.scheme',
value: {
stringValue: 'http:',
},
},
{
key: 'net.host.name',
value: {
stringValue: 'localhost',
},
},
{
key: 'http.host',
value: {
stringValue: 'localhost:4000',
},
},
{
key: 'http.user_agent',
value: {
stringValue:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
},
},
{
key: 'http.status_code',
value: {
intValue: 200,
},
},
{
key: 'gateway.cache.response_cache',
value: {
stringValue: 'miss',
},
},
],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 1,
},
links: [],
droppedLinksCount: 0,
},
],
},
],
},
],
},
{
resourceSpans: [
{
resource: {
attributes: [
{
key: 'service.name',
value: {
stringValue: 'hive-gateway',
},
},
{
key: 'service.version',
value: {
stringValue: '5.13.4',
},
},
],
droppedAttributesCount: 0,
},
scopeSpans: [
{
scope: {
name: 'gateway',
},
spans: [
{
traceId: '6045da1fe92d42a36f0ffdcca55efa46',
spanId: '17fd8c5b3d731702',
parentSpanId: 'b763ec1ad8a305ca',
name: 'http.fetch',
kind: 3,
startTimeUnixNano: '1751541577186000000',
endTimeUnixNano: '1751541577337256209',
attributes: [],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 0,
},
links: [],
droppedLinksCount: 0,
},
],
},
],
},
],
},
{
resourceSpans: [
{
resource: {
attributes: [
{
key: 'service.name',
value: {
stringValue: 'hive-gateway',
},
},
{
key: 'service.version',
value: {
stringValue: '5.13.4',
},
},
],
droppedAttributesCount: 0,
},
scopeSpans: [
{
scope: {
name: 'gateway',
},
spans: [
{
traceId: '6045da1fe92d42a36f0ffdcca55efa46',
spanId: 'b763ec1ad8a305ca',
name: 'gateway.initialization',
kind: 1,
startTimeUnixNano: '1751541567757845981',
endTimeUnixNano: '1751541577382000000',
attributes: [
{
key: 'http.method',
value: {
stringValue: 'GET',
},
},
{
key: 'http.url',
value: {
stringValue:
'http://host.docker.internal:3001/artifacts/v1/219fedb6-8a9b-44d7-929f-b0b16750653c/supergraph',
},
},
{
key: 'net.host.name',
value: {
stringValue: 'host.docker.internal',
},
},
{
key: 'http.host',
value: {
stringValue: 'host.docker.internal:3001',
},
},
{
key: 'http.route',
value: {
stringValue: '/artifacts/v1/219fedb6-8a9b-44d7-929f-b0b16750653c/supergraph',
},
},
{
key: 'http.scheme',
value: {
stringValue: 'http:',
},
},
{
key: 'http.status_code',
value: {
intValue: 200,
},
},
],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 1,
},
links: [],
droppedLinksCount: 0,
},
{
traceId: 'f42cb37f3811f650f06ca078e7c6625f',
spanId: 'c55a1a27cfe0a563',
parentSpanId: '6df96e8ebb5f3dd7',
name: 'graphql.parse',
kind: 1,
startTimeUnixNano: '1751541577384000000',
endTimeUnixNano: '1751541577385261166',
attributes: [
{
key: 'graphql.document',
value: {
stringValue:
'query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}',
},
},
{
key: 'graphql.operation.name',
value: {
stringValue: 'IntrospectionQuery',
},
},
],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 0,
},
links: [],
droppedLinksCount: 0,
},
{
traceId: 'f42cb37f3811f650f06ca078e7c6625f',
spanId: 'ca97bb80da3c0dbe',
parentSpanId: '6df96e8ebb5f3dd7',
name: 'graphql.validate',
kind: 1,
startTimeUnixNano: '1751541577385000000',
endTimeUnixNano: '1751541577397343958',
attributes: [
{
key: 'graphql.document',
value: {
stringValue:
'query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}',
},
},
{
key: 'graphql.operation.name',
value: {
stringValue: 'IntrospectionQuery',
},
},
],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 0,
},
links: [],
droppedLinksCount: 0,
},
{
traceId: 'f42cb37f3811f650f06ca078e7c6625f',
spanId: '697d38f300df2167',
parentSpanId: '6df96e8ebb5f3dd7',
name: 'graphql.context',
kind: 1,
startTimeUnixNano: '1751541577398000000',
endTimeUnixNano: '1751541577399414208',
attributes: [],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 0,
},
links: [],
droppedLinksCount: 0,
},
{
traceId: 'f42cb37f3811f650f06ca078e7c6625f',
spanId: 'f479d33b7320cd0f',
parentSpanId: '6df96e8ebb5f3dd7',
name: 'graphql.execute',
kind: 1,
startTimeUnixNano: '1751541577400000000',
endTimeUnixNano: '1751541577410081959',
attributes: [
{
key: 'graphql.operation.type',
value: {
stringValue: 'query',
},
},
{
key: 'graphql.operation.name',
value: {
stringValue: 'IntrospectionQuery',
},
},
{
key: 'graphql.document',
value: {
stringValue:
'query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}',
},
},
],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 0,
},
links: [],
droppedLinksCount: 0,
},
{
traceId: 'f42cb37f3811f650f06ca078e7c6625f',
spanId: '6df96e8ebb5f3dd7',
parentSpanId: 'af8929e2e40e00af',
name: 'graphql.operation IntrospectionQuery',
kind: 1,
startTimeUnixNano: '1751541577382000000',
endTimeUnixNano: '1751541577409557042',
attributes: [
{
key: 'graphql.document',
value: {
stringValue:
'query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}',
},
},
{
key: 'graphql.operation.name',
value: {
stringValue: 'IntrospectionQuery',
},
},
{
key: 'graphql.operation.type',
value: {
stringValue: 'query',
},
},
{
key: 'gateway.cache.response_cache',
value: {
stringValue: 'miss',
},
},
],
droppedAttributesCount: 0,
events: [],
droppedEventsCount: 0,
status: {
code: 0,
},
links: [],
droppedLinksCount: 0,
},
{
traceId: 'f42cb37f3811f650f06ca078e7c6625f',
spanId: 'af8929e2e40e00af',
name: 'query IntrospectionQuery',
kind: 2,
startTimeUnixNano: '1751541577154000000',
endTimeUnixNano: '1751541577412387709',
attributes: [
{
key: 'http.method',
value: {
stringValue: 'POST',
},
},
{
key: 'http.url',
value: {
stringValue: 'http://localhost:4000/graphql',
},
},
{
key: 'http.route',
value: {
stringValue: '/graphql',
},
},
{
key: 'http.scheme',
value: {
stringValue: 'http:',
},
},
{
key: 'net.host.name',
value: {
stringValue: 'localhost',
},
},
{
key: 'http.host',
value: {
stringValue: 'localhost:4000',
},
},
{
key: 'http.user_agent',
value: {
stringValue:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
},
},
{
key: 'graphql.operation.type',
value: {
stringValue: 'query',
},
},
{
key: 'graphql.operation.name',
value: {
stringValue: 'IntrospectionQuery',
},
},
{
key: 'graphql.document',
value: {
stringValue:
'query IntrospectionQuery{__schema{description queryType{name kind}mutationType{name kind}subscriptionType{name kind}types{...FullType}directives{name description locations args(includeDeprecated:true){...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args(includeDeprecated:true){...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields(includeDeprecated:true){...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue isDeprecated deprecationReason}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}}}}',
},
},
{
key: 'hive.graphql.error.count',
value: {
intValue: 0,
},
},
{
key: 'hive.graphql.error.codes',
value: {
arrayValue: {
values: [{ stringValue: 'ERR_NOT_FOUND' }],
},
},
},
{
key: 'http.status_code',
value: {
intValue: 200,
},
},
{
key: 'gateway.cache.response_cache',
value: {
stringValue: 'miss',
},
},
],
droppedAttributesCount: 0,
events: [
{
attributes: [
{
key: 'exception.type',
value: {
stringValue: 'GraphQLError',
},
},
{
key: 'exception.message',
value: {
stringValue: 'Variable "$id" of required type "ID!" was not provided.',
},
},
{
key: 'exception.stacktrace',
value: {
stringValue:
'GraphQLError: Variable "$id" of required type "ID!" was not provided.\n at createGraphQLError (file:///node_modules/.chunk/abortSignalAny-BG8Lg0X_.mjs:29:12)\n at coerceVariableValues (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:566:25)\n at getVariableValues (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:533:25)\n at buildExecutionContext (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:746:35)\n at execute (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:615:24)\n at file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:1960:37\n at Promise.then (file:///node_modules/.chunk/index-DYl9M8N-.mjs:31:40)\n at handleMaybePromise (file:///node_modules/.chunk/index-DYl9M8N-.mjs:10:33)\n at normalizedExecutor (file:///node_modules/.chunk/normalizedExecutor-C4nYkuV1.mjs:1960:12)\n at _4.args (file:///node_modules/.chunk/use-engine-CPlZaEw6.mjs:580:17)',
},
},
],
name: 'exception',
timeUnixNano: '1751541923667636834',
droppedAttributesCount: 0,
},
],
droppedEventsCount: 0,
status: {
code: 1,
},
links: [],
droppedLinksCount: 0,
},
],
},
],
},
],
},
];
type Reference = typeof reference;
function randomId(len = 32) {
return crypto.randomBytes(len / 2).toString('hex');
}
function toTimeUnixNano(date = new Date()) {
const milliseconds = date.getTime(); // ms since epoch
const nanoseconds = BigInt(milliseconds) * 1_000_000n; // ns = ms × 1_000_000
return nanoseconds;
}
function getRandomIndex(length: number) {
return Math.floor(Math.random() * length);
}
function randomArrayItem<T>(arr: Array<T>) {
return arr[getRandomIndex(arr.length)];
}
const clientNames = ['sales-app', 'product-cron-sync', 'analytics-dashboard'];
const appVersions = new Map<string, Array<string>>();
for (const name of clientNames) {
const versions = new Array<string>();
for (let i = 0; i <= 10; i++) {
versions.push(faker.faker.system.semver());
}
appVersions.set(name, versions);
}
function generateRandomClient() {
const name = randomArrayItem(clientNames);
const version = randomArrayItem(appVersions.get(name)!);
return {
name,
version,
};
}
const errorCodes = [
'INTERNAL_SERVER_ERROR',
'ERR_UNAUTHENTICATED',
'ERR_TEAPOD_NOT_FOUND',
'ERR_NOT_FOUND',
];
function getRandomErrorCodes() {
if (Math.random() > 0.2) {
return '';
}
return [randomArrayItem(errorCodes)];
}
function mutate(currentTime: Date, reference: Reference) {
const newTraceId = randomId();
const newSpanIds = new Map<string, string>();
function getNewSpanId(spanId: string) {
let newSpanId = newSpanIds.get(spanId);
if (!newSpanId) {
newSpanId = randomId(16);
newSpanIds.set(spanId, newSpanId);
}
return newSpanId;
}
let rootTrace:
| Reference[number]['resourceSpans'][number]['scopeSpans'][number]['spans'][number]
| null = null;
for (const payload of reference) {
for (const resourceSpan of payload.resourceSpans) {
for (const scopeSpan of resourceSpan.scopeSpans) {
for (const span of scopeSpan.spans) {
if (span.parentSpanId === undefined) {
rootTrace = span;
const client = generateRandomClient();
rootTrace.attributes.push(
{
key: 'hive.client.name',
value: { stringValue: client.name },
},
{
key: 'hive.client.version',
value: { stringValue: client.version },
},
// TODO: actually calculate this based on the operation.
{
key: 'hive.graphql.operation.hash',
value: { stringValue: faker.faker.git.commitSha() },
},
);
const errors = getRandomErrorCodes();
if (errors) {
rootTrace.attributes.push(
{
key: 'hive.graphql.error.codes',
value: {
arrayValue: {
values: errors.map(code => ({ stringValue: code })),
},
},
},
{
key: 'hive.graphql.error.count',
value: { intValue: errors.length },
},
);
}
break;
}
}
}
}
}
if (!rootTrace) {
throw new Error('Parent Span must always be the first span in the file.');
}
const startTime = BigInt(rootTrace.startTimeUnixNano);
const currentTimeB = toTimeUnixNano(currentTime);
for (const payload of reference) {
for (const resourceSpans of payload.resourceSpans) {
for (const scopeSpan of resourceSpans.scopeSpans) {
for (const span of scopeSpan.spans) {
if (span.parentSpanId) {
span.parentSpanId = getNewSpanId(span.parentSpanId);
}
span.spanId = getNewSpanId(span.spanId);
span.traceId = newTraceId;
const spanStartTime = BigInt(span.startTimeUnixNano);
const spanEndTime = BigInt(span.endTimeUnixNano);
const spanDuration = spanEndTime - spanStartTime;
const spanOffset = spanStartTime - startTime;
const newStartTime = currentTimeB + spanOffset;
span.startTimeUnixNano = newStartTime.toString();
span.endTimeUnixNano = (newStartTime + spanDuration).toString();
if (span.events.length) {
for (const event of span.events) {
const spanStartTime = BigInt(event.timeUnixNano);
const spanOffset = spanStartTime - startTime;
const newStartTime = currentTimeB + spanOffset;
event.timeUnixNano = newStartTime.toString();
}
}
}
}
}
}
}
function createTrace(date: Date, reference: Reference) {
return immer.produce(reference, draft => mutate(date, draft));
}
const __dirname = import.meta.dirname;
const seedFolder = path.join(__dirname);
const files = await fs.readdir(seedFolder);
const references: Array<Reference> = [];
console.log(`Load samples from the '${seedFolder}' folder.`);
for (const file of files) {
if (!file.startsWith('sample-') || !file.endsWith('.json')) {
console.log('Skip ' + file);
continue;
}
console.log('Load' + file);
references.push(JSON.parse(await fs.readFile(path.join(seedFolder, file), 'utf-8')));
}
console.log(`Loaded ${references.length} references.`);
if (references.length === 0) {
throw new Error('No references where found.');
}
const USAGE_DAYS = process.env.USAGE_DAYS || '14';
console.log(
`Seeding usage for the last ${USAGE_DAYS} day(s). (Overwrite using the USAGE_DAYS environment variable)`,
);
const USAGE_INTERVAL = process.env.USAGE_INTERVAL || '1';
console.log(
`Seeding every ${USAGE_INTERVAL} minute(s). (Overwrite using the USAGE_INTERVAL environment variable)`,
);
const otelEndpointUrl = process.env.OTEL_ENDPOINT || 'http://localhost:4318/v1/traces';
console.log(
`Endpoint: ${otelEndpointUrl}. (Overwrite using the OTEL_ENDPOINT environment variable)`,
);
const HIVE_ORGANIZATION_ACCESS_TOKEN = process.env.HIVE_ORGANIZATION_ACCESS_TOKEN;
if (!HIVE_ORGANIZATION_ACCESS_TOKEN) {
throw new Error('Environment variable HIVE_ORGANIZATION_ACCESS_TOKEN is missing.');
}
const HIVE_TARGET_REF = process.env.HIVE_TARGET_REF;
if (!HIVE_TARGET_REF) {
throw new Error('Environment variable HIVE_TARGET_REF is missing.');
}
const intervalMinutes = parseInt(USAGE_INTERVAL, 10);
const usageDays = parseInt(USAGE_DAYS, 10);
const now = new Date();
let currentDate = date.subDays(now, usageDays);
while (currentDate.getTime() < now.getTime()) {
console.log(currentDate.toISOString());
const promises: Array<Promise<unknown>> = [];
for (const reference of references) {
for (let i = randomInt(10); i > 0; i--) {
const tracePayloads = createTrace(currentDate, reference);
promises.push(
...tracePayloads.map(body =>
fetch(otelEndpointUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${HIVE_ORGANIZATION_ACCESS_TOKEN}`,
'X-Hive-Target-Ref': HIVE_TARGET_REF,
},
body: JSON.stringify(body),
}).then(res => {
if (!res.ok) {
throw new Error('Something went wrong');
}
return null;
}),
),
);
}
}
await Promise.all(promises);
currentDate = date.addMinutes(currentDate, intervalMinutes);
}
function randomInt(until = 10) {
return Math.floor(Math.random() * (until + 1));
}