Compare commits

..

1 Commits

Author SHA1 Message Date
swapnil-signoz
d8abbce47e refactor: moving types to cloud provider specific namespace/pkg 2026-04-17 11:57:41 +05:30
56 changed files with 713 additions and 1321 deletions

View File

@@ -75,7 +75,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return signoz.NewSQLSchemaProviderFactories(sqlstore)
},

View File

@@ -48,7 +48,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -96,7 +96,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
signoz.NewWebProviderFactories(),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
@@ -159,9 +159,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return nil, err
}
azureCloudProviderModule := implcloudprovider.NewAzureCloudProvider()
cloudProvidersMap := map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule{
cloudintegrationtypes.CloudProviderTypeAWS: awsCloudProviderModule,
cloudintegrationtypes.CloudProviderTypeAzure: azureCloudProviderModule,
cloudProvidersMap := map[cptypes.CloudProviderType]cloudintegration.CloudProviderModule{
cptypes.CloudProviderTypeAWS: awsCloudProviderModule,
cptypes.CloudProviderTypeAzure: azureCloudProviderModule,
}
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)

View File

@@ -6,8 +6,6 @@
##################### Global #####################
global:
# the url under which the signoz apiserver is externally reachable.
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
@@ -52,8 +50,8 @@ pprof:
web:
# Whether to enable the web frontend
enabled: true
# The index file to use as the SPA entrypoint.
index: index.html
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web

View File

@@ -7,12 +7,12 @@ This guide explains how to add new data sources to the SigNoz onboarding flow. T
The configuration is located at:
```
frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.ts
frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json
```
## Structure Overview
## JSON Structure Overview
The configuration file exports a TypeScript array (`onboardingConfigWithLinks`) containing data source objects. Each object represents a selectable option in the onboarding flow. SVG logos are imported as ES modules at the top of the file.
The configuration file is a JSON array containing data source objects. Each object represents a selectable option in the onboarding flow.
## Data Source Object Keys
@@ -24,7 +24,7 @@ The configuration file exports a TypeScript array (`onboardingConfigWithLinks`)
| `label` | `string` | Display name shown to users (e.g., `"AWS EC2"`) |
| `tags` | `string[]` | Array of category tags for grouping (e.g., `["AWS"]`, `["database"]`) |
| `module` | `string` | Destination module after onboarding completion |
| `imgUrl` | `string` | Imported SVG URL **(SVG required)** (e.g., `import ec2Url from '@/assets/Logos/ec2.svg'`, then use `ec2Url`) |
| `imgUrl` | `string` | Path to the logo/icon **(SVG required)** (e.g., `"/Logos/ec2.svg"`) |
### Optional Keys
@@ -57,34 +57,36 @@ The `module` key determines where users are redirected after completing onboardi
The `question` object enables multi-step selection flows:
```ts
question: {
desc: 'What would you like to monitor?',
type: 'select',
helpText: 'Choose the telemetry type you want to collect.',
helpLink: '/docs/azure-monitoring/overview/',
helpLinkText: 'Read the guide →',
options: [
{
key: 'logging',
label: 'Logs',
imgUrl: azureVmUrl,
link: '/docs/azure-monitoring/app-service/logging/',
},
{
key: 'metrics',
label: 'Metrics',
imgUrl: azureVmUrl,
link: '/docs/azure-monitoring/app-service/metrics/',
},
{
key: 'tracing',
label: 'Traces',
imgUrl: azureVmUrl,
link: '/docs/azure-monitoring/app-service/tracing/',
},
],
},
```json
{
"question": {
"desc": "What would you like to monitor?",
"type": "select",
"helpText": "Choose the telemetry type you want to collect.",
"helpLink": "/docs/azure-monitoring/overview/",
"helpLinkText": "Read the guide →",
"options": [
{
"key": "logging",
"label": "Logs",
"imgUrl": "/Logos/azure-vm.svg",
"link": "/docs/azure-monitoring/app-service/logging/"
},
{
"key": "metrics",
"label": "Metrics",
"imgUrl": "/Logos/azure-vm.svg",
"link": "/docs/azure-monitoring/app-service/metrics/"
},
{
"key": "tracing",
"label": "Traces",
"imgUrl": "/Logos/azure-vm.svg",
"link": "/docs/azure-monitoring/app-service/tracing/"
}
]
}
}
```
### Question Keys
@@ -104,161 +106,152 @@ Options can be simple (direct link) or nested (with another question):
### Simple Option (Direct Link)
```ts
```json
{
key: 'aws-ec2-logs',
label: 'Logs',
imgUrl: ec2Url,
link: '/docs/userguide/collect_logs_from_file/',
},
"key": "aws-ec2-logs",
"label": "Logs",
"imgUrl": "/Logos/ec2.svg",
"link": "/docs/userguide/collect_logs_from_file/"
}
```
### Option with Internal Redirect
```ts
```json
{
key: 'aws-ec2-metrics-one-click',
label: 'One Click AWS',
imgUrl: ec2Url,
link: '/integrations?integration=aws-integration&service=ec2',
internalRedirect: true,
},
"key": "aws-ec2-metrics-one-click",
"label": "One Click AWS",
"imgUrl": "/Logos/ec2.svg",
"link": "/integrations?integration=aws-integration&service=ec2",
"internalRedirect": true
}
```
> **Important**: Set `internalRedirect: true` only for internal app routes (like `/integrations?...`). Docs links should NOT have this flag.
### Nested Option (Multi-step Flow)
```ts
```json
{
key: 'aws-ec2-metrics',
label: 'Metrics',
imgUrl: ec2Url,
question: {
desc: 'How would you like to set up monitoring?',
helpText: 'Choose your setup method.',
options: [...],
},
},
"key": "aws-ec2-metrics",
"label": "Metrics",
"imgUrl": "/Logos/ec2.svg",
"question": {
"desc": "How would you like to set up monitoring?",
"helpText": "Choose your setup method.",
"options": [...]
}
}
```
## Examples
### Simple Data Source (Direct Link)
```ts
import elbUrl from '@/assets/Logos/elb.svg';
// inside the onboardingConfigWithLinks array:
```json
{
dataSource: 'aws-elb',
label: 'AWS ELB',
tags: ['AWS'],
module: 'logs',
relatedSearchKeywords: [
'aws',
'aws elb',
'elb logs',
'elastic load balancer',
"dataSource": "aws-elb",
"label": "AWS ELB",
"tags": ["AWS"],
"module": "logs",
"relatedSearchKeywords": [
"aws",
"aws elb",
"elb logs",
"elastic load balancer"
],
imgUrl: elbUrl,
link: '/docs/aws-monitoring/elb/',
},
"imgUrl": "/Logos/elb.svg",
"link": "/docs/aws-monitoring/elb/"
}
```
### Data Source with Single Question Level
```ts
import azureVmUrl from '@/assets/Logos/azure-vm.svg';
// inside the onboardingConfigWithLinks array:
```json
{
dataSource: 'app-service',
label: 'App Service',
imgUrl: azureVmUrl,
tags: ['Azure'],
module: 'apm',
relatedSearchKeywords: ['azure', 'app service'],
question: {
desc: 'What telemetry data do you want to visualise?',
type: 'select',
options: [
"dataSource": "app-service",
"label": "App Service",
"imgUrl": "/Logos/azure-vm.svg",
"tags": ["Azure"],
"module": "apm",
"relatedSearchKeywords": ["azure", "app service"],
"question": {
"desc": "What telemetry data do you want to visualise?",
"type": "select",
"options": [
{
key: 'logging',
label: 'Logs',
imgUrl: azureVmUrl,
link: '/docs/azure-monitoring/app-service/logging/',
"key": "logging",
"label": "Logs",
"imgUrl": "/Logos/azure-vm.svg",
"link": "/docs/azure-monitoring/app-service/logging/"
},
{
key: 'metrics',
label: 'Metrics',
imgUrl: azureVmUrl,
link: '/docs/azure-monitoring/app-service/metrics/',
"key": "metrics",
"label": "Metrics",
"imgUrl": "/Logos/azure-vm.svg",
"link": "/docs/azure-monitoring/app-service/metrics/"
},
{
key: 'tracing',
label: 'Traces',
imgUrl: azureVmUrl,
link: '/docs/azure-monitoring/app-service/tracing/',
},
],
},
},
"key": "tracing",
"label": "Traces",
"imgUrl": "/Logos/azure-vm.svg",
"link": "/docs/azure-monitoring/app-service/tracing/"
}
]
}
}
```
### Data Source with Nested Questions (2-3 Levels)
```ts
import ec2Url from '@/assets/Logos/ec2.svg';
// inside the onboardingConfigWithLinks array:
```json
{
dataSource: 'aws-ec2',
label: 'AWS EC2',
tags: ['AWS'],
module: 'logs',
relatedSearchKeywords: ['aws', 'aws ec2', 'ec2 logs', 'ec2 metrics'],
imgUrl: ec2Url,
question: {
desc: 'What would you like to monitor for AWS EC2?',
type: 'select',
helpText: 'Choose the type of telemetry data you want to collect.',
options: [
"dataSource": "aws-ec2",
"label": "AWS EC2",
"tags": ["AWS"],
"module": "logs",
"relatedSearchKeywords": ["aws", "aws ec2", "ec2 logs", "ec2 metrics"],
"imgUrl": "/Logos/ec2.svg",
"question": {
"desc": "What would you like to monitor for AWS EC2?",
"type": "select",
"helpText": "Choose the type of telemetry data you want to collect.",
"options": [
{
key: 'aws-ec2-logs',
label: 'Logs',
imgUrl: ec2Url,
link: '/docs/userguide/collect_logs_from_file/',
"key": "aws-ec2-logs",
"label": "Logs",
"imgUrl": "/Logos/ec2.svg",
"link": "/docs/userguide/collect_logs_from_file/"
},
{
key: 'aws-ec2-metrics',
label: 'Metrics',
imgUrl: ec2Url,
question: {
desc: 'How would you like to set up EC2 Metrics monitoring?',
helpText: 'One Click uses AWS CloudWatch integration. Manual setup uses OpenTelemetry.',
helpLink: '/docs/aws-monitoring/one-click-vs-manual/',
helpLinkText: 'Read the comparison guide →',
options: [
"key": "aws-ec2-metrics",
"label": "Metrics",
"imgUrl": "/Logos/ec2.svg",
"question": {
"desc": "How would you like to set up EC2 Metrics monitoring?",
"helpText": "One Click uses AWS CloudWatch integration. Manual setup uses OpenTelemetry.",
"helpLink": "/docs/aws-monitoring/one-click-vs-manual/",
"helpLinkText": "Read the comparison guide →",
"options": [
{
key: 'aws-ec2-metrics-one-click',
label: 'One Click AWS',
imgUrl: ec2Url,
link: '/integrations?integration=aws-integration&service=ec2',
internalRedirect: true,
"key": "aws-ec2-metrics-one-click",
"label": "One Click AWS",
"imgUrl": "/Logos/ec2.svg",
"link": "/integrations?integration=aws-integration&service=ec2",
"internalRedirect": true
},
{
key: 'aws-ec2-metrics-manual',
label: 'Manual Setup',
imgUrl: ec2Url,
link: '/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/',
},
],
},
},
],
},
},
"key": "aws-ec2-metrics-manual",
"label": "Manual Setup",
"imgUrl": "/Logos/ec2.svg",
"link": "/docs/tutorial/opentelemetry-binary-usage-in-virtual-machine/"
}
]
}
}
]
}
}
```
## Best Practices
@@ -277,16 +270,11 @@ import ec2Url from '@/assets/Logos/ec2.svg';
### 3. Logos
- Place logo files in `src/assets/Logos/`
- Place logo files in `public/Logos/`
- Use SVG format
- Import the SVG at the top of the file and reference the imported variable:
```ts
import myServiceUrl from '@/assets/Logos/my-service.svg';
// then in the config object:
imgUrl: myServiceUrl,
```
- Reference as `"/Logos/your-logo.svg"`
- **Fetching Icons**: New icons can be easily fetched from [OpenBrand](https://openbrand.sh/). Use the pattern `https://openbrand.sh/?url=<TARGET_URL>`, where `<TARGET_URL>` is the URL-encoded link to the service's website. For example, to get Render's logo, use [https://openbrand.sh/?url=https%3A%2F%2Frender.com](https://openbrand.sh/?url=https%3A%2F%2Frender.com).
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo src/assets/Logos/your-logo.svg` to minimise their size before committing.
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo public/Logos/your-logo.svg` to minimise their size before committing.
### 4. Links
@@ -302,8 +290,8 @@ import ec2Url from '@/assets/Logos/ec2.svg';
## Adding a New Data Source
1. Add the logo SVG to `src/assets/Logos/` and add a top-level import in the config file (e.g., `import myServiceUrl from '@/assets/Logos/my-service.svg'`)
2. Add your data source object to the `onboardingConfigWithLinks` array, referencing the imported variable for `imgUrl`
1. Add your data source object to the JSON array
2. Ensure the logo exists in `public/Logos/`
3. Test the flow locally with `yarn dev`
4. Validation:
- Navigate to the [onboarding page](http://localhost:3301/get-started-with-signoz-cloud) on your local machine

View File

@@ -8,6 +8,8 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes/awstypes"
)
type awscloudprovider struct {
@@ -19,7 +21,7 @@ func NewAWSCloudProvider(defStore cloudintegrationtypes.ServiceDefinitionStore)
}
func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, account *cloudintegrationtypes.Account, req *cloudintegrationtypes.GetConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
baseURL := fmt.Sprintf(cloudintegrationtypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.Aws.DeploymentRegion)
baseURL := fmt.Sprintf(awstypes.CloudFormationQuickCreateBaseURL.StringValue(), req.Config.Aws.DeploymentRegion)
u, _ := url.Parse(baseURL)
q := u.Query()
@@ -29,8 +31,8 @@ func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, acc
u.RawQuery = q.Encode()
q = u.Query()
q.Set("stackName", cloudintegrationtypes.AgentCloudFormationBaseStackName.StringValue())
q.Set("templateURL", fmt.Sprintf(cloudintegrationtypes.AgentCloudFormationTemplateS3Path.StringValue(), req.Config.AgentVersion))
q.Set("stackName", awstypes.AgentCloudFormationBaseStackName.StringValue())
q.Set("templateURL", fmt.Sprintf(awstypes.AgentCloudFormationTemplateS3Path.StringValue(), req.Config.AgentVersion))
q.Set("param_SigNozIntegrationAgentVersion", req.Config.AgentVersion)
q.Set("param_SigNozApiUrl", req.Credentials.SigNozAPIURL)
q.Set("param_SigNozApiKey", req.Credentials.SigNozAPIKey)
@@ -39,25 +41,23 @@ func (provider *awscloudprovider) GetConnectionArtifact(ctx context.Context, acc
q.Set("param_IngestionKey", req.Credentials.IngestionKey)
return &cloudintegrationtypes.ConnectionArtifact{
Aws: &cloudintegrationtypes.AWSConnectionArtifact{
ConnectionURL: u.String() + "?&" + q.Encode(), // this format is required by AWS
},
AWS: awstypes.NewConnectionArtifact(u.String() + "?&" + q.Encode()), // this format is required by AWS
}, nil
}
func (provider *awscloudprovider) ListServiceDefinitions(ctx context.Context) ([]*cloudintegrationtypes.ServiceDefinition, error) {
return provider.serviceDefinitions.List(ctx, cloudintegrationtypes.CloudProviderTypeAWS)
return provider.serviceDefinitions.List(ctx, cptypes.CloudProviderTypeAWS)
}
func (provider *awscloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
serviceDef, err := provider.serviceDefinitions.Get(ctx, cloudintegrationtypes.CloudProviderTypeAWS, serviceID)
func (provider *awscloudprovider) GetServiceDefinition(ctx context.Context, serviceID cptypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
serviceDef, err := provider.serviceDefinitions.Get(ctx, cptypes.CloudProviderTypeAWS, serviceID)
if err != nil {
return nil, err
}
// override cloud integration dashboard id
for index, dashboard := range serviceDef.Assets.Dashboards {
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cloudintegrationtypes.CloudProviderTypeAWS, serviceID.StringValue(), dashboard.ID)
serviceDef.Assets.Dashboards[index].ID = cloudintegrationtypes.GetCloudIntegrationDashboardID(cptypes.CloudProviderTypeAWS, serviceID.StringValue(), dashboard.ID)
}
return serviceDef, nil
@@ -73,12 +73,12 @@ func (provider *awscloudprovider) BuildIntegrationConfig(
return services[i].Type.StringValue() < services[j].Type.StringValue()
})
compiledMetrics := new(cloudintegrationtypes.AWSMetricsCollectionStrategy)
compiledLogs := new(cloudintegrationtypes.AWSLogsCollectionStrategy)
compiledMetrics := new(awstypes.AWSMetricsCollectionStrategy)
compiledLogs := new(awstypes.AWSLogsCollectionStrategy)
var compiledS3Buckets map[string][]string
for _, storedSvc := range services {
svcCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(cloudintegrationtypes.CloudProviderTypeAWS, storedSvc.Config)
svcCfg, err := cloudintegrationtypes.NewServiceConfigFromJSON(cptypes.CloudProviderTypeAWS, storedSvc.Config)
if err != nil {
return nil, err
}
@@ -89,10 +89,10 @@ func (provider *awscloudprovider) BuildIntegrationConfig(
}
strategy := svcDef.TelemetryCollectionStrategy.AWS
logsEnabled := svcCfg.IsLogsEnabled(cloudintegrationtypes.CloudProviderTypeAWS)
logsEnabled := svcCfg.IsLogsEnabled(cptypes.CloudProviderTypeAWS)
// S3Sync: logs come directly from configured S3 buckets, not CloudWatch subscriptions
if storedSvc.Type == cloudintegrationtypes.AWSServiceS3Sync {
if storedSvc.Type == cptypes.AWSServiceS3Sync {
if logsEnabled && svcCfg.AWS.Logs.S3Buckets != nil {
compiledS3Buckets = svcCfg.AWS.Logs.S3Buckets
}
@@ -104,14 +104,14 @@ func (provider *awscloudprovider) BuildIntegrationConfig(
compiledLogs.Subscriptions = append(compiledLogs.Subscriptions, strategy.Logs.Subscriptions...)
}
metricsEnabled := svcCfg.IsMetricsEnabled(cloudintegrationtypes.CloudProviderTypeAWS)
metricsEnabled := svcCfg.IsMetricsEnabled(cptypes.CloudProviderTypeAWS)
if metricsEnabled && strategy.Metrics != nil {
compiledMetrics.StreamFilters = append(compiledMetrics.StreamFilters, strategy.Metrics.StreamFilters...)
}
}
collectionStrategy := new(cloudintegrationtypes.AWSTelemetryCollectionStrategy)
collectionStrategy := new(awstypes.AWSTelemetryCollectionStrategy)
if len(compiledMetrics.StreamFilters) > 0 {
collectionStrategy.Metrics = compiledMetrics
@@ -124,9 +124,6 @@ func (provider *awscloudprovider) BuildIntegrationConfig(
}
return &cloudintegrationtypes.ProviderIntegrationConfig{
AWS: &cloudintegrationtypes.AWSIntegrationConfig{
EnabledRegions: account.Config.AWS.Regions,
TelemetryCollectionStrategy: collectionStrategy,
},
AWS: awstypes.NewIntegrationConfig(account.Config.AWS.Regions, collectionStrategy),
}, nil
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
)
type azurecloudprovider struct{}
@@ -21,7 +22,7 @@ func (provider *azurecloudprovider) ListServiceDefinitions(ctx context.Context)
panic("implement me")
}
func (provider *azurecloudprovider) GetServiceDefinition(ctx context.Context, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
func (provider *azurecloudprovider) GetServiceDefinition(ctx context.Context, serviceID cptypes.ServiceID) (*cloudintegrationtypes.ServiceDefinition, error) {
panic("implement me")
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
@@ -28,7 +29,7 @@ type module struct {
licensing licensing.Licensing
global global.Global
serviceAccount serviceaccount.Module
cloudProvidersMap map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule
cloudProvidersMap map[cptypes.CloudProviderType]cloudintegration.CloudProviderModule
config cloudintegration.Config
}
@@ -39,7 +40,7 @@ func NewModule(
gateway gateway.Gateway,
licensing licensing.Licensing,
serviceAccount serviceaccount.Module,
cloudProvidersMap map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProviderModule,
cloudProvidersMap map[cptypes.CloudProviderType]cloudintegration.CloudProviderModule,
config cloudintegration.Config,
) (cloudintegration.Module, error) {
return &module{
@@ -56,7 +57,7 @@ func NewModule(
// GetConnectionCredentials returns credentials required to generate connection artifact. eg. apiKey, ingestionKey etc.
// It will return creds it can deduce and return empty value for others.
func (module *module) GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Credentials, error) {
func (module *module) GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.Credentials, error) {
// get license to get the deployment details
license, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
@@ -119,7 +120,7 @@ func (module *module) GetConnectionArtifact(ctx context.Context, account *cloudi
return cloudProviderModule.GetConnectionArtifact(ctx, account, req)
}
func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -133,7 +134,7 @@ func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, account
return cloudintegrationtypes.NewAccountFromStorable(storableAccount)
}
func (module *module) GetConnectedAccount(ctx context.Context, orgID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
func (module *module) GetConnectedAccount(ctx context.Context, orgID, accountID valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -148,7 +149,7 @@ func (module *module) GetConnectedAccount(ctx context.Context, orgID, accountID
}
// ListAccounts return only agent connected accounts.
func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.Account, error) {
func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) ([]*cloudintegrationtypes.Account, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -162,7 +163,7 @@ func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provi
return cloudintegrationtypes.NewAccountsFromStorables(storableAccounts)
}
func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, req *cloudintegrationtypes.AgentCheckInRequest) (*cloudintegrationtypes.AgentCheckInResponse, error) {
func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType, req *cloudintegrationtypes.AgentCheckInRequest) (*cloudintegrationtypes.AgentCheckInResponse, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -248,7 +249,7 @@ func (module *module) UpdateAccount(ctx context.Context, account *cloudintegrati
return module.store.UpdateAccount(ctx, storableAccount)
}
func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cptypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -257,7 +258,7 @@ func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID,
return module.store.RemoveAccount(ctx, orgID, accountID, provider)
}
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -300,7 +301,7 @@ func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUI
return resp, nil
}
func (module *module) GetService(ctx context.Context, orgID valuer.UUID, serviceID cloudintegrationtypes.ServiceID, provider cloudintegrationtypes.CloudProviderType, cloudIntegrationID valuer.UUID) (*cloudintegrationtypes.Service, error) {
func (module *module) GetService(ctx context.Context, orgID valuer.UUID, serviceID cptypes.ServiceID, provider cptypes.CloudProviderType, cloudIntegrationID valuer.UUID) (*cloudintegrationtypes.Service, error) {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -336,7 +337,7 @@ func (module *module) GetService(ctx context.Context, orgID valuer.UUID, service
return cloudintegrationtypes.NewService(*serviceDefinition, integrationService), nil
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cptypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -360,7 +361,7 @@ func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, serv
return module.store.CreateService(ctx, cloudintegrationtypes.NewStorableCloudIntegrationService(service, string(configJSON)))
}
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, integrationService *cloudintegrationtypes.CloudIntegrationService, provider cptypes.CloudProviderType) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -424,7 +425,7 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
stats := make(map[string]any)
// get connected accounts for AWS
awsAccountsCount, err := module.store.CountConnectedAccounts(ctx, orgID, cloudintegrationtypes.CloudProviderTypeAWS)
awsAccountsCount, err := module.store.CountConnectedAccounts(ctx, orgID, cptypes.CloudProviderTypeAWS)
if err == nil {
stats["cloudintegration.aws.connectedaccounts.count"] = awsAccountsCount
}
@@ -436,15 +437,15 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
func (module *module) getCloudProvider(provider cloudintegrationtypes.CloudProviderType) (cloudintegration.CloudProviderModule, error) {
func (module *module) getCloudProvider(provider cptypes.CloudProviderType) (cloudintegration.CloudProviderModule, error) {
if cloudProviderModule, ok := module.cloudProvidersMap[provider]; ok {
return cloudProviderModule, nil
}
return nil, errors.NewInvalidInputf(cloudintegrationtypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
func (module *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
func (module *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (string, error) {
keyName := cloudintegrationtypes.NewIngestionKeyName(provider)
result, err := module.gateway.SearchIngestionKeysByName(ctx, orgID, keyName, 1, 10)
@@ -465,7 +466,7 @@ func (module *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.
return createdIngestionKey.Value, nil
}
func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (string, error) {
domain := module.serviceAccount.Config().Email.Domain
serviceAccount := serviceaccounttypes.NewServiceAccount("integration", domain, serviceaccounttypes.ServiceAccountStatusActive, orgID)
serviceAccount, err := module.serviceAccount.GetOrCreate(ctx, orgID, serviceAccount)

View File

@@ -262,20 +262,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil

View File

@@ -66,8 +66,6 @@ module.exports = {
rules: {
// Asset migration — base-path safety
'rulesdir/no-unsupported-asset-pattern': 'error',
// Base-path safety — window.open and origin-concat patterns; upgrade to error coming PR
'rulesdir/no-raw-absolute-path': 'warn',
// Code quality rules
'prefer-const': 'error', // Enforces const for variables never reassigned

2
frontend/.gitignore vendored
View File

@@ -28,4 +28,4 @@ e2e/test-plan/saved-views/
e2e/test-plan/service-map/
e2e/test-plan/services/
e2e/test-plan/traces/
e2e/test-plan/user-preferences/
e2e/test-plan/user-preferences/

View File

@@ -1,103 +0,0 @@
'use strict';
/**
* ESLint rule: no-raw-absolute-path
*
* Catches two patterns that break at runtime when the app is served from a
* sub-path (e.g. /signoz/):
*
* 1. window.open(path, '_blank')
* → use openInNewTab(path) which calls withBasePath internally
*
* 2. window.location.origin + path / `${window.location.origin}${path}`
* → use getAbsoluteUrl(path)
*
* External URLs (first arg starts with "http") are explicitly allowed.
*/
function isOriginAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'origin' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isExternalUrl(node) {
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value.startsWith('http://') || node.value.startsWith('https://');
}
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
const raw = node.quasis[0].value.raw;
return raw.startsWith('http://') || raw.startsWith('https://');
}
return false;
}
// window.open(withBasePath(x)) and window.open(getAbsoluteUrl(x)) are already safe.
function isSafeHelperCall(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(node.callee.name === 'withBasePath' || node.callee.name === 'getAbsoluteUrl')
);
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow raw window.open and origin-concatenation patterns that miss the runtime base path',
category: 'Base Path Safety',
},
schema: [],
messages: {
windowOpen:
'Use openInNewTab(path) instead of window.open(path, "_blank") — openInNewTab prepends the base path automatically.',
originConcat:
'Use getAbsoluteUrl(path) instead of window.location.origin + path — getAbsoluteUrl prepends the base path automatically.',
},
},
create(context) {
return {
// window.open(path, ...) — allow only external first-arg URLs
CallExpression(node) {
const { callee, arguments: args } = node;
if (
callee.type !== 'MemberExpression' ||
callee.object.type !== 'Identifier' ||
callee.object.name !== 'window' ||
callee.property.name !== 'open'
)
return;
if (args.length < 1) return;
if (isExternalUrl(args[0])) return;
if (isSafeHelperCall(args[0])) return;
context.report({ node, messageId: 'windowOpen' });
},
// window.location.origin + path
BinaryExpression(node) {
if (node.operator !== '+') return;
if (isOriginAccess(node.left) || isOriginAccess(node.right)) {
context.report({ node, messageId: 'originConcat' });
}
},
// `${window.location.origin}${path}`
TemplateLiteral(node) {
if (node.expressions.some(isOriginAccess)) {
context.report({ node, messageId: 'originConcat' });
}
},
};
},
};

View File

@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="[[.BaseHref]]" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
@@ -60,7 +59,7 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="favicon.ico" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
</head>
<body data-theme="default">
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -114,7 +113,7 @@
})(document, 'script');
}
</script>
<link rel="stylesheet" href="css/uPlot.min.css" />
<link rel="stylesheet" href="/css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -2,7 +2,6 @@ import { initReactI18next } from 'react-i18next';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { getBasePath } from 'utils/basePath';
import cacheBursting from '../../i18n-translations-hash.json';
@@ -25,7 +24,7 @@ i18n
const ns = namespace[0];
const pathkey = `/${language}/${ns}`;
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
return `/locales/${language}/${namespace}.json?h=${hash}`;
},
},
react: {

View File

@@ -1,6 +1,5 @@
import {
interceptorRejected,
interceptorsRequestBasePath,
interceptorsRequestResponse,
interceptorsResponse,
} from 'api';
@@ -18,7 +17,6 @@ export const GeneratedAPIInstance = <T>(
return generatedAPIAxiosInstance({ ...config }).then(({ data }) => data);
};
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
generatedAPIAxiosInstance.interceptors.response.use(
interceptorsResponse,

View File

@@ -11,7 +11,6 @@ import axios, {
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
@@ -68,42 +67,6 @@ export const interceptorsRequestResponse = (
return value;
};
// Prepends the runtime base path to outgoing requests so API calls work under
// a URL prefix (e.g. /signoz/api/v1/…). No-op for root deployments and dev
// (dev baseURL is a full http:// URL, not an absolute path).
export const interceptorsRequestBasePath = (
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const basePath = getBasePath();
if (basePath === '/') {
return value;
}
if (value.baseURL?.startsWith('/')) {
// Relative baseURL: '/api/v1/' → '/signoz/api/v1/'
if (!value.baseURL.startsWith(basePath)) {
value.baseURL = basePath + value.baseURL.slice(1);
}
} else if (value.baseURL?.startsWith('http')) {
// Absolute baseURL (e.g. VITE_FRONTEND_API_ENDPOINT set for dev/testing):
// 'https://host/api/v1/' → 'https://host/signoz/api/v1/'
const url = new URL(value.baseURL);
if (!url.pathname.startsWith(basePath)) {
url.pathname = basePath + url.pathname.slice(1);
value.baseURL = url.toString();
}
} else if (
!value.baseURL &&
value.url?.startsWith('/') &&
!value.url.startsWith(basePath)
) {
// Generated instance: baseURL is '' in prod, path is in url
value.url = basePath + value.url.slice(1);
}
return value;
};
export const interceptorRejected = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => {
@@ -170,7 +133,6 @@ const instance = axios.create({
});
instance.interceptors.request.use(interceptorsRequestResponse);
instance.interceptors.request.use(interceptorsRequestBasePath);
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
export const AxiosAlertManagerInstance = axios.create({
@@ -185,7 +147,6 @@ ApiV2Instance.interceptors.response.use(
interceptorRejected,
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
// axios V3
export const ApiV3Instance = axios.create({
@@ -197,7 +158,6 @@ ApiV3Instance.interceptors.response.use(
interceptorRejected,
);
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V4
@@ -210,7 +170,6 @@ ApiV4Instance.interceptors.response.use(
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V5
@@ -223,7 +182,6 @@ ApiV5Instance.interceptors.response.use(
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios Base
@@ -236,7 +194,6 @@ LogEventAxiosInstance.interceptors.response.use(
interceptorRejectedBase,
);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
//
AxiosAlertManagerInstance.interceptors.response.use(
@@ -244,7 +201,6 @@ AxiosAlertManagerInstance.interceptors.response.use(
interceptorRejected,
);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
export { apiV1 };
export default instance;

View File

@@ -12,7 +12,6 @@ import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { withBasePath } from 'utils/basePath';
export interface NavigateToExplorerProps {
filters: TagFilterItem[];
@@ -134,7 +133,7 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
},
[
prepareQuery,

View File

@@ -1,7 +1,6 @@
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { cloneDeep, isEqual } from 'lodash-es';
import { withBasePath } from 'utils/basePath';
interface NavigateOptions {
replace?: boolean;
@@ -131,7 +130,7 @@ export const useSafeNavigate = (
typeof to === 'string'
? to
: `${to.pathname || location.pathname}${to.search || ''}`;
window.open(withBasePath(targetPath), '_blank');
window.open(targetPath, '_blank');
return;
}

View File

@@ -1,4 +1,3 @@
import { createBrowserHistory } from 'history';
import { getBasePath } from 'utils/basePath';
export default createBrowserHistory({ basename: getBasePath() });
export default createBrowserHistory();

View File

@@ -4,7 +4,6 @@ import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Home, LifeBuoy } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils';
import { withBasePath } from 'utils/basePath';
import cloudUrl from '@/assets/Images/cloud.svg';
@@ -12,8 +11,8 @@ import './ErrorBoundaryFallback.styles.scss';
function ErrorBoundaryFallback(): JSX.Element {
const handleReload = (): void => {
// Hard reload resets Sentry.ErrorBoundary state; withBasePath preserves any /signoz/ prefix.
window.location.href = withBasePath(ROUTES.HOME);
// Go to home page
window.location.href = ROUTES.HOME;
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();

View File

@@ -1,118 +0,0 @@
/**
* basePath is memoized at module init, so each describe block isolates the
* module with a fresh DOM state using jest.isolateModules + require.
*/
type BasePath = typeof import('../basePath');
function loadModule(href?: string): BasePath {
if (href !== undefined) {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.appendChild(base);
}
let mod!: BasePath;
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
mod = require('../basePath');
});
return mod;
}
afterEach(() => {
document.head.querySelectorAll('base').forEach((el) => el.remove());
});
describe('at basePath="/"', () => {
let m: BasePath;
beforeEach(() => {
m = loadModule('/');
});
it('getBasePath returns "/"', () => {
expect(m.getBasePath()).toBe('/');
});
it('withBasePath is a no-op for any internal path', () => {
expect(m.withBasePath('/logs')).toBe('/logs');
expect(m.withBasePath('/logs/explorer')).toBe('/logs/explorer');
});
it('withBasePath passes through external URLs', () => {
expect(m.withBasePath('https://example.com/foo')).toBe(
'https://example.com/foo',
);
});
it('getAbsoluteUrl returns origin + path', () => {
expect(m.getAbsoluteUrl('/logs')).toBe(`${window.location.origin}/logs`);
});
it('getBaseUrl returns bare origin', () => {
expect(m.getBaseUrl()).toBe(window.location.origin);
});
});
describe('at basePath="/signoz/"', () => {
let m: BasePath;
beforeEach(() => {
m = loadModule('/signoz/');
});
it('getBasePath returns "/signoz/"', () => {
expect(m.getBasePath()).toBe('/signoz/');
});
it('withBasePath prepends the prefix', () => {
expect(m.withBasePath('/logs')).toBe('/signoz/logs');
expect(m.withBasePath('/logs/explorer')).toBe('/signoz/logs/explorer');
});
it('withBasePath is idempotent — safe to call twice', () => {
expect(m.withBasePath('/signoz/logs')).toBe('/signoz/logs');
});
it('withBasePath is idempotent when path equals the prefix without trailing slash', () => {
expect(m.withBasePath('/signoz')).toBe('/signoz');
});
it('withBasePath passes through external URLs', () => {
expect(m.withBasePath('https://example.com/foo')).toBe(
'https://example.com/foo',
);
});
it('getAbsoluteUrl returns origin + prefixed path', () => {
expect(m.getAbsoluteUrl('/logs')).toBe(
`${window.location.origin}/signoz/logs`,
);
});
it('getBaseUrl returns origin + prefix without trailing slash', () => {
expect(m.getBaseUrl()).toBe(`${window.location.origin}/signoz`);
});
});
describe('no <base> tag', () => {
it('getBasePath falls back to "/"', () => {
const m = loadModule();
expect(m.getBasePath()).toBe('/');
});
});
describe('href without trailing slash', () => {
it('normalises to trailing slash', () => {
const m = loadModule('/signoz');
expect(m.getBasePath()).toBe('/signoz/');
expect(m.withBasePath('/logs')).toBe('/signoz/logs');
});
});
describe('nested prefix "/a/b/prefix/"', () => {
it('withBasePath handles arbitrary depth', () => {
const m = loadModule('/a/b/prefix/');
expect(m.withBasePath('/logs')).toBe('/a/b/prefix/logs');
expect(m.withBasePath('/a/b/prefix/logs')).toBe('/a/b/prefix/logs');
});
});

View File

@@ -1,27 +1,15 @@
import { isModifierKeyPressed } from '../app';
type NavigationModule = typeof import('../navigation');
function loadNavigationModule(href?: string): NavigationModule {
if (href !== undefined) {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.appendChild(base);
}
let mod!: NavigationModule;
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
mod = require('../navigation');
});
return mod;
}
import { openInNewTab } from '../navigation';
describe('navigation utilities', () => {
const originalWindowOpen = window.open;
beforeEach(() => {
window.open = jest.fn();
});
afterEach(() => {
window.open = originalWindowOpen;
document.head.querySelectorAll('base').forEach((el) => el.remove());
});
describe('isModifierKeyPressed', () => {
@@ -68,59 +56,25 @@ describe('navigation utilities', () => {
});
describe('openInNewTab', () => {
describe('at basePath="/"', () => {
let m: NavigationModule;
beforeEach(() => {
window.open = jest.fn();
m = loadNavigationModule('/');
});
it('passes internal path through unchanged', () => {
m.openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
});
it('passes through external URLs unchanged', () => {
m.openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('handles paths with query strings', () => {
m.openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
it('calls window.open with the given path and _blank target', () => {
openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
});
describe('at basePath="/signoz/"', () => {
let m: NavigationModule;
beforeEach(() => {
window.open = jest.fn();
m = loadNavigationModule('/signoz/');
});
it('handles full URLs', () => {
openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('prepends base path to internal paths', () => {
m.openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/signoz/dashboard', '_blank');
});
it('passes through external URLs unchanged', () => {
m.openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('is idempotent — does not double-prefix an already-prefixed path', () => {
m.openInNewTab('/signoz/dashboard');
expect(window.open).toHaveBeenCalledWith('/signoz/dashboard', '_blank');
});
it('handles paths with query strings', () => {
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
});
});

View File

@@ -1,50 +0,0 @@
// Read once at module init — avoids a DOM query on every axios request.
const _basePath: string = ((): string => {
const href = document.querySelector('base')?.getAttribute('href') ?? '/';
return href.endsWith('/') ? href : `${href}/`;
})();
/** Returns the runtime base path — always trailing-slashed. e.g. "/" or "/signoz/" */
export function getBasePath(): string {
return _basePath;
}
/**
* Prepends the base path to an internal absolute path.
* Idempotent and safe to call on any value.
*
* withBasePath('/logs') → '/signoz/logs'
* withBasePath('/signoz/logs') → '/signoz/logs' (already prefixed)
* withBasePath('https://x.com') → 'https://x.com' (external, passthrough)
*/
export function withBasePath(path: string): string {
if (!path.startsWith('/')) {
return path;
}
if (_basePath === '/') {
return path;
}
if (path.startsWith(_basePath) || path === _basePath.slice(0, -1)) {
return path;
}
return _basePath + path.slice(1);
}
/**
* Full absolute URL — for copy-to-clipboard and window.open calls.
* getAbsoluteUrl(ROUTES.LOGS_EXPLORER) → 'https://host/signoz/logs/logs-explorer'
*/
export function getAbsoluteUrl(path: string): string {
return window.location.origin + withBasePath(path);
}
/**
* Origin + base path without trailing slash — for sending to the backend
* as frontendBaseUrl in invite / password-reset email flows.
* getBaseUrl() → 'https://host/signoz'
*/
export function getBaseUrl(): string {
return (
window.location.origin + (_basePath === '/' ? '' : _basePath.slice(0, -1))
);
}

View File

@@ -1,5 +1,6 @@
import { withBasePath } from 'utils/basePath';
/**
* Opens the given path in a new browser tab.
*/
export const openInNewTab = (path: string): void => {
window.open(withBasePath(path), '_blank');
window.open(path, '_blank');
};

View File

@@ -10,18 +10,6 @@ import { createHtmlPlugin } from 'vite-plugin-html';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
import tsconfigPaths from 'vite-tsconfig-paths';
// In dev the Go backend is not involved, so replace the [[.BaseHref]] placeholder
// with "/" so relative assets resolve correctly from the Vite dev server.
function devBasePathPlugin(): Plugin {
return {
name: 'dev-base-path',
apply: 'serve',
transformIndexHtml(html): string {
return html.replaceAll('[[.BaseHref]]', '/');
},
};
}
function rawMarkdownPlugin(): Plugin {
return {
name: 'raw-markdown',
@@ -44,7 +32,6 @@ export default defineConfig(
const plugins = [
tsconfigPaths(),
rawMarkdownPlugin(),
devBasePathPlugin(),
react(),
createHtmlPlugin({
inject: {
@@ -137,7 +124,6 @@ export default defineConfig(
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
},
base: './',
build: {
sourcemap: true,
outDir: 'build',

View File

@@ -2,8 +2,6 @@ package global
import (
"net/url"
"path"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
@@ -39,34 +37,5 @@ func newConfig() factory.Config {
}
func (c Config) Validate() error {
if c.ExternalURL != nil {
if c.ExternalURL.Path != "" && c.ExternalURL.Path != "/" {
if !strings.HasPrefix(c.ExternalURL.Path, "/") {
return errors.NewInvalidInputf(ErrCodeInvalidGlobalConfig, "global::external_url path must start with '/', got %q", c.ExternalURL.Path)
}
}
}
return nil
}
func (c Config) ExternalPath() string {
if c.ExternalURL == nil || c.ExternalURL.Path == "" || c.ExternalURL.Path == "/" {
return ""
}
p := path.Clean("/" + c.ExternalURL.Path)
if p == "/" {
return ""
}
return p
}
func (c Config) ExternalPathTrailing() string {
if p := c.ExternalPath(); p != "" {
return p + "/"
}
return "/"
}

View File

@@ -1,139 +0,0 @@
package global
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExternalPath(t *testing.T) {
testCases := []struct {
name string
config Config
expected string
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
expected: "",
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: ""}},
expected: "",
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/"}},
expected: "",
},
{
name: "SingleSegment",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: "/signoz",
},
{
name: "TrailingSlash",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz/"}},
expected: "/signoz",
},
{
name: "MultiSegment",
config: Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/a/b/c"}},
expected: "/a/b/c",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.config.ExternalPath())
})
}
}
func TestExternalPathTrailing(t *testing.T) {
testCases := []struct {
name string
config Config
expected string
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
expected: "/",
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Path: ""}},
expected: "/",
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Path: "/"}},
expected: "/",
},
{
name: "SingleSegment",
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
expected: "/signoz/",
},
{
name: "MultiSegment",
config: Config{ExternalURL: &url.URL{Path: "/a/b/c"}},
expected: "/a/b/c/",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, tc.config.ExternalPathTrailing())
})
}
}
func TestValidate(t *testing.T) {
testCases := []struct {
name string
config Config
fail bool
}{
{
name: "NilURL",
config: Config{ExternalURL: nil},
fail: false,
},
{
name: "EmptyPath",
config: Config{ExternalURL: &url.URL{Path: ""}},
fail: false,
},
{
name: "RootPath",
config: Config{ExternalURL: &url.URL{Path: "/"}},
fail: false,
},
{
name: "ValidPath",
config: Config{ExternalURL: &url.URL{Path: "/signoz"}},
fail: false,
},
{
name: "NoLeadingSlash",
config: Config{ExternalURL: &url.URL{Path: "signoz"}},
fail: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.config.Validate()
if tc.fail {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}

View File

@@ -6,29 +6,30 @@ import (
"github.com/SigNoz/signoz/pkg/statsreporter"
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider citypes.CloudProviderType) (*citypes.Credentials, error)
GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (*citypes.Credentials, error)
CreateAccount(ctx context.Context, account *citypes.Account) error
// GetAccount returns cloud integration account
GetAccount(ctx context.Context, orgID, accountID valuer.UUID, provider citypes.CloudProviderType) (*citypes.Account, error)
GetAccount(ctx context.Context, orgID, accountID valuer.UUID, provider cptypes.CloudProviderType) (*citypes.Account, error)
// GetConnectedAccount returns the account where agent is connected
GetConnectedAccount(ctx context.Context, orgID, accountID valuer.UUID, provider citypes.CloudProviderType) (*citypes.Account, error)
GetConnectedAccount(ctx context.Context, orgID, accountID valuer.UUID, provider cptypes.CloudProviderType) (*citypes.Account, error)
// ListAccounts lists accounts where agent is connected
ListAccounts(ctx context.Context, orgID valuer.UUID, provider citypes.CloudProviderType) ([]*citypes.Account, error)
ListAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) ([]*citypes.Account, error)
// UpdateAccount updates the cloud integration account for a specific organization.
UpdateAccount(ctx context.Context, account *citypes.Account) error
// DisconnectAccount soft deletes/removes a cloud integration account.
DisconnectAccount(ctx context.Context, orgID, accountID valuer.UUID, provider citypes.CloudProviderType) error
DisconnectAccount(ctx context.Context, orgID, accountID valuer.UUID, provider cptypes.CloudProviderType) error
// GetConnectionArtifact returns cloud provider specific connection information,
// client side handles how this information is shown
@@ -36,20 +37,20 @@ type Module interface {
// ListServicesMetadata returns the list of supported services' metadata for a cloud provider with optional filtering for a specific integration
// This just returns a summary of the service and not the whole service definition.
ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider citypes.CloudProviderType, integrationID valuer.UUID) ([]*citypes.ServiceMetadata, error)
ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType, integrationID valuer.UUID) ([]*citypes.ServiceMetadata, error)
// GetService returns service definition details for a serviceID. This optionally returns the service config
// for integrationID if provided.
GetService(ctx context.Context, orgID valuer.UUID, serviceID citypes.ServiceID, provider citypes.CloudProviderType, integrationID valuer.UUID) (*citypes.Service, error)
GetService(ctx context.Context, orgID valuer.UUID, serviceID cptypes.ServiceID, provider cptypes.CloudProviderType, integrationID valuer.UUID) (*citypes.Service, error)
// CreateService creates a new service for a cloud integration account.
CreateService(ctx context.Context, orgID valuer.UUID, service *citypes.CloudIntegrationService, provider citypes.CloudProviderType) error
CreateService(ctx context.Context, orgID valuer.UUID, service *citypes.CloudIntegrationService, provider cptypes.CloudProviderType) error
// UpdateService updates cloud integration service
UpdateService(ctx context.Context, orgID valuer.UUID, service *citypes.CloudIntegrationService, provider citypes.CloudProviderType) error
UpdateService(ctx context.Context, orgID valuer.UUID, service *citypes.CloudIntegrationService, provider cptypes.CloudProviderType) error
// AgentCheckIn is called by agent to send heartbeat and get latest config in response.
AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider citypes.CloudProviderType, req *citypes.AgentCheckInRequest) (*citypes.AgentCheckInResponse, error)
AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType, req *citypes.AgentCheckInRequest) (*citypes.AgentCheckInResponse, error)
// GetDashboardByID returns dashboard JSON for a given dashboard id.
// this only returns the dashboard when the service (embedded in dashboard id) is enabled
@@ -70,7 +71,7 @@ type CloudProviderModule interface {
ListServiceDefinitions(ctx context.Context) ([]*citypes.ServiceDefinition, error)
// GetServiceDefinition returns the service definition for the given service ID.
GetServiceDefinition(ctx context.Context, serviceID citypes.ServiceID) (*citypes.ServiceDefinition, error)
GetServiceDefinition(ctx context.Context, serviceID cptypes.ServiceID) (*citypes.ServiceDefinition, error)
// BuildIntegrationConfig compiles the provider-specific integration config from the account
// and list of configured services. This is the config returned to the agent on check-in.

View File

@@ -14,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
citypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
)
const definitionsRoot = "fs/definitions"
@@ -29,7 +30,7 @@ func NewServiceDefinitionStore() citypes.ServiceDefinitionStore {
}
// Get reads and hydrates the service definition for the given provider and service ID.
func (s *definitionStore) Get(ctx context.Context, provider citypes.CloudProviderType, serviceID citypes.ServiceID) (*citypes.ServiceDefinition, error) {
func (s *definitionStore) Get(ctx context.Context, provider cptypes.CloudProviderType, serviceID cptypes.ServiceID) (*citypes.ServiceDefinition, error) {
svcDir := path.Join(definitionsRoot, provider.StringValue(), serviceID.StringValue())
def, err := readServiceDefinition(svcDir)
if err != nil {
@@ -39,7 +40,7 @@ func (s *definitionStore) Get(ctx context.Context, provider citypes.CloudProvide
}
// List reads and hydrates all service definitions for the given provider, sorted by ID.
func (s *definitionStore) List(ctx context.Context, provider citypes.CloudProviderType) ([]*citypes.ServiceDefinition, error) {
func (s *definitionStore) List(ctx context.Context, provider cptypes.CloudProviderType) ([]*citypes.ServiceDefinition, error) {
providerDir := path.Join(definitionsRoot, provider.StringValue())
entries, err := fs.ReadDir(definitionFiles, providerDir)
if err != nil {

View File

@@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -34,7 +35,7 @@ func (handler *handler) GetConnectionCredentials(rw http.ResponseWriter, r *http
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -59,7 +60,7 @@ func (handler *handler) CreateAccount(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -104,7 +105,7 @@ func (handler *handler) GetAccount(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -135,7 +136,7 @@ func (handler *handler) ListAccounts(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -160,7 +161,7 @@ func (handler *handler) UpdateAccount(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -214,7 +215,7 @@ func (handler *handler) DisconnectAccount(rw http.ResponseWriter, r *http.Reques
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -245,7 +246,7 @@ func (handler *handler) ListServicesMetadata(rw http.ResponseWriter, r *http.Req
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
@@ -286,13 +287,13 @@ func (handler *handler) GetService(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
}
serviceID, err := cloudintegrationtypes.NewServiceID(provider, mux.Vars(r)["service_id"])
serviceID, err := cptypes.NewServiceID(provider, mux.Vars(r)["service_id"])
if err != nil {
render.Error(rw, err)
return
@@ -332,13 +333,13 @@ func (handler *handler) UpdateService(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return
}
serviceID, err := cloudintegrationtypes.NewServiceID(provider, mux.Vars(r)["service_id"])
serviceID, err := cptypes.NewServiceID(provider, mux.Vars(r)["service_id"])
if err != nil {
render.Error(rw, err)
return
@@ -402,7 +403,7 @@ func (handler *handler) AgentCheckIn(rw http.ResponseWriter, r *http.Request) {
return
}
provider, err := cloudintegrationtypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
provider, err := cptypes.NewCloudProvider(mux.Vars(r)["cloud_provider"])
if err != nil {
render.Error(rw, err)
return

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -16,7 +17,7 @@ func NewModule() cloudintegration.Module {
return &module{}
}
func (module *module) GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Credentials, error) {
func (module *module) GetConnectionCredentials(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.Credentials, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "get connection credentials is not supported")
}
@@ -24,16 +25,16 @@ func (module *module) CreateAccount(ctx context.Context, account *cloudintegrati
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "create account is not supported")
}
func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
func (module *module) GetAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "get account is not supported")
}
// GetConnectedAccount implements [cloudintegration.Module].
func (module *module) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
func (module *module) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.Account, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "get connected account is not supported")
}
func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.Account, error) {
func (module *module) ListAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) ([]*cloudintegrationtypes.Account, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "list accounts is not supported")
}
@@ -41,23 +42,23 @@ func (module *module) UpdateAccount(ctx context.Context, account *cloudintegrati
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "update account is not supported")
}
func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) DisconnectAccount(ctx context.Context, orgID valuer.UUID, accountID valuer.UUID, provider cptypes.CloudProviderType) error {
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "disconnect account is not supported")
}
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) CreateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cptypes.CloudProviderType) error {
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "create service is not supported")
}
func (module *module) GetService(ctx context.Context, orgID valuer.UUID, serviceID cloudintegrationtypes.ServiceID, provider cloudintegrationtypes.CloudProviderType, integrationID valuer.UUID) (*cloudintegrationtypes.Service, error) {
func (module *module) GetService(ctx context.Context, orgID valuer.UUID, serviceID cptypes.ServiceID, provider cptypes.CloudProviderType, integrationID valuer.UUID) (*cloudintegrationtypes.Service, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "get service is not supported")
}
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
func (module *module) ListServicesMetadata(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType, integrationID valuer.UUID) ([]*cloudintegrationtypes.ServiceMetadata, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "list services metadata is not supported")
}
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cloudintegrationtypes.CloudProviderType) error {
func (module *module) UpdateService(ctx context.Context, orgID valuer.UUID, service *cloudintegrationtypes.CloudIntegrationService, provider cptypes.CloudProviderType) error {
return errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "update service is not supported")
}
@@ -65,7 +66,7 @@ func (module *module) GetConnectionArtifact(ctx context.Context, account *cloudi
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "get connection artifact is not supported")
}
func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, req *cloudintegrationtypes.AgentCheckInRequest) (*cloudintegrationtypes.AgentCheckInResponse, error) {
func (module *module) AgentCheckIn(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType, req *cloudintegrationtypes.AgentCheckInRequest) (*cloudintegrationtypes.AgentCheckInResponse, error) {
return nil, errors.New(errors.TypeUnsupported, cloudintegrationtypes.ErrCodeUnsupported, "agent check-in is not supported")
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -17,7 +18,7 @@ func NewStore(sqlStore sqlstore.SQLStore) cloudintegrationtypes.Store {
return &store{store: sqlStore}
}
func (store *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
func (store *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
account := new(cloudintegrationtypes.StorableCloudIntegration)
err := store.
store.
@@ -34,7 +35,7 @@ func (store *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, p
return account, nil
}
func (store *store) GetConnectedAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
func (store *store) GetConnectedAccount(ctx context.Context, orgID, id valuer.UUID, provider cptypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
account := new(cloudintegrationtypes.StorableCloudIntegration)
err := store.
store.
@@ -54,7 +55,7 @@ func (store *store) GetConnectedAccount(ctx context.Context, orgID, id valuer.UU
return account, nil
}
func (store *store) GetConnectedAccountByProviderAccountID(ctx context.Context, orgID valuer.UUID, providerAccountID string, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
func (store *store) GetConnectedAccountByProviderAccountID(ctx context.Context, orgID valuer.UUID, providerAccountID string, provider cptypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
account := new(cloudintegrationtypes.StorableCloudIntegration)
err := store.
store.
@@ -73,7 +74,7 @@ func (store *store) GetConnectedAccountByProviderAccountID(ctx context.Context,
return account, nil
}
func (store *store) ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) {
func (store *store) ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) {
var accounts []*cloudintegrationtypes.StorableCloudIntegration
err := store.
store.
@@ -93,7 +94,7 @@ func (store *store) ListConnectedAccounts(ctx context.Context, orgID valuer.UUID
return accounts, nil
}
func (store *store) CountConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (int, error) {
func (store *store) CountConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (int, error) {
storable := new(cloudintegrationtypes.StorableCloudIntegration)
count, err := store.
store.
@@ -141,7 +142,7 @@ func (store *store) UpdateAccount(ctx context.Context, account *cloudintegration
return err
}
func (store *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
func (store *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cptypes.CloudProviderType) error {
_, err := store.
store.
BunDBCtx(ctx).
@@ -155,7 +156,7 @@ func (store *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, pr
return err
}
func (store *store) GetServiceByServiceID(ctx context.Context, cloudIntegrationID valuer.UUID, serviceID cloudintegrationtypes.ServiceID) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
func (store *store) GetServiceByServiceID(ctx context.Context, cloudIntegrationID valuer.UUID, serviceID cptypes.ServiceID) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
service := new(cloudintegrationtypes.StorableCloudIntegrationService)
err := store.
store.

View File

@@ -587,6 +587,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/query_filter/analyze", am.ViewAccess(aH.QueryParserAPI.AnalyzeQueryFilter)).Methods(http.MethodPost)
}
func Intersection(a, b []int) (c []int) {
m := make(map[int]bool)

View File

@@ -244,20 +244,6 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server,
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil

View File

@@ -88,9 +88,9 @@ func NewCacheProviderFactories() factory.NamedMap[factory.ProviderFactory[cache.
)
}
func NewWebProviderFactories(globalConfig global.Config) factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
func NewWebProviderFactories() factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]] {
return factory.MustNewNamedMap(
routerweb.NewFactory(globalConfig),
routerweb.NewFactory(),
noopweb.NewFactory(),
)
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfmanagertest"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
@@ -35,7 +34,7 @@ func TestNewProviderFactories(t *testing.T) {
})
assert.NotPanics(t, func() {
NewWebProviderFactories(global.Config{})
NewWebProviderFactories()
})
assert.NotPanics(t, func() {

View File

@@ -7,6 +7,8 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes/awstypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -14,12 +16,12 @@ import (
type Account struct {
types.Identifiable
types.TimeAuditable
ProviderAccountID *string `json:"providerAccountId" required:"true" nullable:"true"`
Provider CloudProviderType `json:"provider" required:"true"`
RemovedAt *time.Time `json:"removedAt" required:"true" nullable:"true"`
AgentReport *AgentReport `json:"agentReport" required:"true" nullable:"true"`
OrgID valuer.UUID `json:"orgId" required:"true"`
Config *AccountConfig `json:"config" required:"true" nullable:"false"`
ProviderAccountID *string `json:"providerAccountId" required:"true" nullable:"true"`
Provider cptypes.CloudProviderType `json:"provider" required:"true"`
RemovedAt *time.Time `json:"removedAt" required:"true" nullable:"true"`
AgentReport *AgentReport `json:"agentReport" required:"true" nullable:"true"`
OrgID valuer.UUID `json:"orgId" required:"true"`
Config *AccountConfig `json:"config" required:"true" nullable:"false"`
}
// AgentReport represents heartbeats sent by the agent.
@@ -30,11 +32,7 @@ type AgentReport struct {
type AccountConfig struct {
// required till new providers are added
AWS *AWSAccountConfig `json:"aws" required:"true" nullable:"false"`
}
type AWSAccountConfig struct {
Regions []string `json:"regions" required:"true" nullable:"false"`
AWS *awstypes.AWSAccountConfig `json:"aws" required:"true" nullable:"false"`
}
type PostableAccount struct {
@@ -45,7 +43,7 @@ type PostableAccount struct {
type PostableAccountConfig struct {
// as agent version is common for all providers, we can keep it at top level of this struct
AgentVersion string
Aws *AWSPostableAccountConfig `json:"aws" required:"true" nullable:"false"`
Aws *awstypes.AWSPostableAccountConfig `json:"aws" required:"true" nullable:"false"`
}
type Credentials struct {
@@ -55,11 +53,6 @@ type Credentials struct {
IngestionKey string `json:"ingestionKey" required:"true"`
}
type AWSPostableAccountConfig struct {
DeploymentRegion string `json:"deploymentRegion" required:"true"`
Regions []string `json:"regions" required:"true" nullable:"false"`
}
type GettableAccountWithConnectionArtifact struct {
ID valuer.UUID `json:"id" required:"true"`
ConnectionArtifact *ConnectionArtifact `json:"connectionArtifact" required:"true"`
@@ -67,11 +60,7 @@ type GettableAccountWithConnectionArtifact struct {
type ConnectionArtifact struct {
// required till new providers are added
Aws *AWSConnectionArtifact `json:"aws" required:"true" nullable:"false"`
}
type AWSConnectionArtifact struct {
ConnectionURL string `json:"connectionUrl" required:"true"`
AWS *awstypes.AWSConnectionArtifact `json:"aws" required:"true" nullable:"false"`
}
type GetConnectionArtifactRequest = PostableAccount
@@ -84,7 +73,7 @@ type UpdatableAccount struct {
Config *AccountConfig `json:"config" required:"true" nullable:"false"`
}
func NewAccount(orgID valuer.UUID, provider CloudProviderType, config *AccountConfig) *Account {
func NewAccount(orgID valuer.UUID, provider cptypes.CloudProviderType, config *AccountConfig) *Account {
return &Account{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
@@ -125,8 +114,8 @@ func NewAccountFromStorable(storableAccount *StorableCloudIntegration) (*Account
}
switch storableAccount.Provider {
case CloudProviderTypeAWS:
awsConfig := new(AWSAccountConfig)
case cptypes.CloudProviderTypeAWS:
awsConfig := new(awstypes.AWSAccountConfig)
err := json.Unmarshal([]byte(storableAccount.Config), awsConfig)
if err != nil {
return nil, err
@@ -170,14 +159,14 @@ func NewGettableAccounts(accounts []*Account) *GettableAccounts {
}
}
func NewAccountConfigFromPostable(provider CloudProviderType, config *PostableAccountConfig) (*AccountConfig, error) {
func NewAccountConfigFromPostable(provider cptypes.CloudProviderType, config *PostableAccountConfig) (*AccountConfig, error) {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
if config.Aws == nil {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "AWS config can not be nil for AWS provider")
}
if err := validateAWSRegion(config.Aws.DeploymentRegion); err != nil {
if err := cptypes.NewAWSRegion(config.Aws.DeploymentRegion); err != nil {
return nil, err
}
@@ -186,20 +175,20 @@ func NewAccountConfigFromPostable(provider CloudProviderType, config *PostableAc
}
for _, region := range config.Aws.Regions {
if err := validateAWSRegion(region); err != nil {
if err := cptypes.NewAWSRegion(region); err != nil {
return nil, err
}
}
return &AccountConfig{AWS: &AWSAccountConfig{Regions: config.Aws.Regions}}, nil
return &AccountConfig{AWS: &awstypes.AWSAccountConfig{Regions: config.Aws.Regions}}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
}
func NewAccountConfigFromUpdatable(provider CloudProviderType, config *UpdatableAccount) (*AccountConfig, error) {
func NewAccountConfigFromUpdatable(provider cptypes.CloudProviderType, config *UpdatableAccount) (*AccountConfig, error) {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
if config.Config.AWS == nil {
return nil, errors.NewInvalidInputf(ErrCodeInvalidInput, "AWS config can not be nil for AWS provider")
}
@@ -209,14 +198,14 @@ func NewAccountConfigFromUpdatable(provider CloudProviderType, config *Updatable
}
for _, region := range config.Config.AWS.Regions {
if err := validateAWSRegion(region); err != nil {
if err := cptypes.NewAWSRegion(region); err != nil {
return nil, err
}
}
return &AccountConfig{AWS: &AWSAccountConfig{Regions: config.Config.AWS.Regions}}, nil
return &AccountConfig{AWS: &awstypes.AWSAccountConfig{Regions: config.Config.AWS.Regions}}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
}
@@ -235,7 +224,7 @@ func GetSigNozAPIURLFromDeployment(deployment *zeustypes.GettableDeployment) (st
return fmt.Sprintf("https://%s.%s", deployment.Name, deployment.Cluster.Region.DNS), nil
}
func (account *Account) Update(provider CloudProviderType, config *AccountConfig) error {
func (account *Account) Update(provider cptypes.CloudProviderType, config *AccountConfig) error {
account.Config = config
account.UpdatedAt = time.Now()
return nil
@@ -303,3 +292,7 @@ func (config *AccountConfig) ToJSON() ([]byte, error) {
return nil, errors.NewInternalf(errors.CodeInternal, "no provider account config found")
}
func NewIngestionKeyName(provider cptypes.CloudProviderType) string {
return fmt.Sprintf("%s-integration", provider.StringValue())
}

View File

@@ -5,6 +5,8 @@ import (
"time"
"github.com/SigNoz/signoz/pkg/errors"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes/awstypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -43,24 +45,19 @@ type GettableAgentCheckIn struct {
// IntegrationConfig older integration config struct for backward compatibility,
// this will be eventually removed once agents are updated to use new struct.
type IntegrationConfig struct {
EnabledRegions []string `json:"enabled_regions" required:"true" nullable:"false"` // backward compatible
Telemetry *OldAWSCollectionStrategy `json:"telemetry" required:"true" nullable:"false"` // backward compatible
EnabledRegions []string `json:"enabled_regions" required:"true" nullable:"false"` // backward compatible
Telemetry *awstypes.OldAWSCollectionStrategy `json:"telemetry" required:"true" nullable:"false"` // backward compatible
}
type ProviderIntegrationConfig struct {
AWS *AWSIntegrationConfig `json:"aws" required:"true" nullable:"false"`
}
type AWSIntegrationConfig struct {
EnabledRegions []string `json:"enabledRegions" required:"true" nullable:"false"`
TelemetryCollectionStrategy *AWSTelemetryCollectionStrategy `json:"telemetryCollectionStrategy" required:"true" nullable:"false"`
AWS *awstypes.AWSIntegrationConfig `json:"aws" required:"true" nullable:"false"`
}
// NewGettableAgentCheckIn constructs a backward-compatible response from an AgentCheckInResponse.
// It populates the old snake_case fields (account_id, cloud_account_id, integration_config, removed_at)
// from the new camelCase fields so older agents continue to work unchanged.
// The provider parameter controls which provider-specific block is mapped into the legacy integration_config.
func NewGettableAgentCheckIn(provider CloudProviderType, resp *AgentCheckInResponse) *GettableAgentCheckIn {
func NewGettableAgentCheckIn(provider cptypes.CloudProviderType, resp *AgentCheckInResponse) *GettableAgentCheckIn {
gettable := &GettableAgentCheckIn{
AccountID: resp.CloudIntegrationID,
CloudAccountID: resp.ProviderAccountID,
@@ -69,7 +66,7 @@ func NewGettableAgentCheckIn(provider CloudProviderType, resp *AgentCheckInRespo
}
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
gettable.OlderIntegrationConfig = awsOlderIntegrationConfig(resp.IntegrationConfig)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/uptrace/bun"
"github.com/SigNoz/signoz/pkg/types"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -32,12 +33,12 @@ type StorableCloudIntegration struct {
types.Identifiable
types.TimeAuditable
Provider CloudProviderType `bun:"provider,type:text"`
Config string `bun:"config,type:text"` // Config is provider-specific data in JSON string format
AccountID *string `bun:"account_id,type:text"`
LastAgentReport *StorableAgentReport `bun:"last_agent_report,type:text"`
RemovedAt *time.Time `bun:"removed_at,type:timestamp,nullzero"`
OrgID valuer.UUID `bun:"org_id,type:text"`
Provider cptypes.CloudProviderType `bun:"provider,type:text"`
Config string `bun:"config,type:text"` // Config is provider-specific data in JSON string format
AccountID *string `bun:"account_id,type:text"`
LastAgentReport *StorableAgentReport `bun:"last_agent_report,type:text"`
RemovedAt *time.Time `bun:"removed_at,type:timestamp,nullzero"`
OrgID valuer.UUID `bun:"org_id,type:text"`
}
// StorableAgentReport represents the last heartbeat and arbitrary data sent by the agent
@@ -53,9 +54,9 @@ type StorableCloudIntegrationService struct {
types.Identifiable
types.TimeAuditable
Type ServiceID `bun:"type,type:text,notnull"` // Keeping Type field name as is, but it is a service id
Config string `bun:"config,type:text"` // Config is cloud provider's service specific data in JSON string format
CloudIntegrationID valuer.UUID `bun:"cloud_integration_id,type:text"`
Type cptypes.ServiceID `bun:"type,type:text,notnull"` // Keeping Type field name as is, but it is a service id
Config string `bun:"config,type:text"` // Config is cloud provider's service specific data in JSON string format
CloudIntegrationID valuer.UUID `bun:"cloud_integration_id,type:text"`
}
// Following Service config types are only internally used to store service config in DB and use JSON snake case keys for backward compatibility.
@@ -153,9 +154,9 @@ func (account *StorableCloudIntegration) Update(providerAccountID *string, agent
}
// following StorableServiceConfig related functions are helper functions to convert between JSON string and ServiceConfig domain struct.
func newStorableServiceConfig(provider CloudProviderType, serviceID ServiceID, serviceConfig *ServiceConfig, supportedSignals *SupportedSignals) (*StorableServiceConfig, error) {
func newStorableServiceConfig(provider cptypes.CloudProviderType, serviceID cptypes.ServiceID, serviceConfig *ServiceConfig, supportedSignals *SupportedSignals) (*StorableServiceConfig, error) {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
storableAWSServiceConfig := new(StorableAWSServiceConfig)
if supportedSignals.Logs {
@@ -167,7 +168,7 @@ func newStorableServiceConfig(provider CloudProviderType, serviceID ServiceID, s
Enabled: serviceConfig.AWS.Logs.Enabled,
}
if serviceID == AWSServiceS3Sync {
if serviceID == cptypes.AWSServiceS3Sync {
if serviceConfig.AWS.Logs.S3Buckets == nil {
return nil, errors.NewInvalidInputf(ErrCodeCloudIntegrationInvalidConfig, "s3 buckets config is required for AWS S3 Sync service")
}
@@ -188,13 +189,13 @@ func newStorableServiceConfig(provider CloudProviderType, serviceID ServiceID, s
return &StorableServiceConfig{AWS: storableAWSServiceConfig}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
}
func newStorableServiceConfigFromJSON(provider CloudProviderType, jsonStr string) (*StorableServiceConfig, error) {
func newStorableServiceConfigFromJSON(provider cptypes.CloudProviderType, jsonStr string) (*StorableServiceConfig, error) {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
awsConfig := new(StorableAWSServiceConfig)
err := json.Unmarshal([]byte(jsonStr), awsConfig)
if err != nil {
@@ -202,13 +203,13 @@ func newStorableServiceConfigFromJSON(provider CloudProviderType, jsonStr string
}
return &StorableServiceConfig{AWS: awsConfig}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
}
func (config *StorableServiceConfig) toJSON(provider CloudProviderType) ([]byte, error) {
func (config *StorableServiceConfig) toJSON(provider cptypes.CloudProviderType) ([]byte, error) {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
jsonBytes, err := json.Marshal(config.AWS)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "couldn't serialize AWS service config to JSON")
@@ -216,6 +217,6 @@ func (config *StorableServiceConfig) toJSON(provider CloudProviderType) ([]byte,
return jsonBytes, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
}

View File

@@ -0,0 +1,117 @@
package awstypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
CloudFormationQuickCreateBaseURL = valuer.NewString("https://%s.console.aws.amazon.com/cloudformation/home")
AgentCloudFormationTemplateS3Path = valuer.NewString("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json")
AgentCloudFormationBaseStackName = valuer.NewString("signoz-integration")
)
type AWSPostableAccountConfig struct {
DeploymentRegion string `json:"deploymentRegion" required:"true"`
Regions []string `json:"regions" required:"true" nullable:"false"`
}
type AWSConnectionArtifact struct {
ConnectionURL string `json:"connectionUrl" required:"true"`
}
type AWSAccountConfig struct {
Regions []string `json:"regions" required:"true" nullable:"false"`
}
// OldAWSCollectionStrategy is the backward-compatible snake_case form of AWSCollectionStrategy,
// used in the legacy integration_config response field for older agents.
type OldAWSCollectionStrategy struct {
Provider string `json:"provider"`
Metrics *OldAWSMetricsStrategy `json:"aws_metrics,omitempty"`
Logs *OldAWSLogsStrategy `json:"aws_logs,omitempty"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
// OldAWSMetricsStrategy is the snake_case form of AWSMetricsStrategy for older agents.
type OldAWSMetricsStrategy struct {
StreamFilters []struct {
Namespace string `json:"Namespace"`
MetricNames []string `json:"MetricNames,omitempty"`
} `json:"cloudwatch_metric_stream_filters"`
}
// OldAWSLogsStrategy is the snake_case form of AWSLogsStrategy for older agents.
type OldAWSLogsStrategy struct {
Subscriptions []struct {
LogGroupNamePrefix string `json:"log_group_name_prefix"`
FilterPattern string `json:"filter_pattern"`
} `json:"cloudwatch_logs_subscriptions"`
}
type AWSIntegrationConfig struct {
EnabledRegions []string `json:"enabledRegions" required:"true" nullable:"false"`
TelemetryCollectionStrategy *AWSTelemetryCollectionStrategy `json:"telemetryCollectionStrategy" required:"true" nullable:"false"`
}
// AWSTelemetryCollectionStrategy represents signal collection strategy for AWS services.
type AWSTelemetryCollectionStrategy struct {
Metrics *AWSMetricsCollectionStrategy `json:"metrics,omitempty" required:"false" nullable:"false"`
Logs *AWSLogsCollectionStrategy `json:"logs,omitempty" required:"false" nullable:"false"`
S3Buckets map[string][]string `json:"s3Buckets,omitempty" required:"false"` // Only available in S3 Sync Service Type in AWS
}
// AWSMetricsCollectionStrategy represents metrics collection strategy for AWS services.
type AWSMetricsCollectionStrategy struct {
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
StreamFilters []*AWSCloudWatchMetricStreamFilter `json:"streamFilters" required:"true" nullable:"false"`
}
type AWSCloudWatchMetricStreamFilter struct {
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
Namespace string `json:"namespace" required:"true"`
MetricNames []string `json:"metricNames,omitempty" required:"false" nullable:"false"`
}
// AWSLogsCollectionStrategy represents logs collection strategy for AWS services.
type AWSLogsCollectionStrategy struct {
Subscriptions []*AWSCloudWatchLogsSubscription `json:"subscriptions" required:"true" nullable:"false"`
}
type AWSCloudWatchLogsSubscription struct {
// subscribe to all logs groups with specified prefix.
// eg: `/aws/rds/`
LogGroupNamePrefix string `json:"logGroupNamePrefix" required:"true"`
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
// "" implies no filtering is required
FilterPattern string `json:"filterPattern" required:"true"`
}
type AWSServiceConfig struct {
Logs *AWSServiceLogsConfig `json:"logs"`
Metrics *AWSServiceMetricsConfig `json:"metrics"`
}
// AWSServiceLogsConfig is AWS specific logs config for a service
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
type AWSServiceLogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3Buckets,omitempty"`
}
type AWSServiceMetricsConfig struct {
Enabled bool `json:"enabled"`
}
func NewConnectionArtifact(connectionURL string) *AWSConnectionArtifact {
return &AWSConnectionArtifact{
ConnectionURL: connectionURL,
}
}
func NewIntegrationConfig(enabledRegions []string, telemetryCollectionStrategy *AWSTelemetryCollectionStrategy) *AWSIntegrationConfig {
return &AWSIntegrationConfig{
EnabledRegions: enabledRegions,
TelemetryCollectionStrategy: telemetryCollectionStrategy,
}
}

View File

@@ -1,13 +1,10 @@
package cloudintegrationtypes
package cloudprovidertypes
import (
"fmt"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
// CloudProviderType type alias.
type CloudProviderType struct{ valuer.String }
var (
@@ -15,16 +12,9 @@ var (
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
// errors.
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("cloud_integration_invalid_cloud_provider")
CloudFormationQuickCreateBaseURL = valuer.NewString("https://%s.console.aws.amazon.com/cloudformation/home")
AgentCloudFormationTemplateS3Path = valuer.NewString("https://signoz-integrations.s3.us-east-1.amazonaws.com/aws-quickcreate-template-%s.json")
AgentCloudFormationBaseStackName = valuer.NewString("signoz-integration")
)
// NewCloudProvider returns a new CloudProviderType from a string.
// It validates the input and returns an error if the input is not valid cloud provider.
func NewCloudProvider(provider string) (CloudProviderType, error) {
switch provider {
case CloudProviderTypeAWS.StringValue():
@@ -35,7 +25,3 @@ func NewCloudProvider(provider string) (CloudProviderType, error) {
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
}
}
func NewIngestionKeyName(provider CloudProviderType) string {
return fmt.Sprintf("%s-integration", provider.StringValue())
}

View File

@@ -0,0 +1,75 @@
package cloudprovidertypes
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
type CloudProviderRegion struct{ valuer.String }
var ErrCodeInvalidServiceID = errors.MustNewCode("invalid_service_id")
var (
// AWS regions.
AWSRegionAFSouth1 = CloudProviderRegion{valuer.NewString("af-south-1")} // Africa (Cape Town).
AWSRegionAPEast1 = CloudProviderRegion{valuer.NewString("ap-east-1")} // Asia Pacific (Hong Kong).
AWSRegionAPNortheast1 = CloudProviderRegion{valuer.NewString("ap-northeast-1")} // Asia Pacific (Tokyo).
AWSRegionAPNortheast2 = CloudProviderRegion{valuer.NewString("ap-northeast-2")} // Asia Pacific (Seoul).
AWSRegionAPNortheast3 = CloudProviderRegion{valuer.NewString("ap-northeast-3")} // Asia Pacific (Osaka).
AWSRegionAPSouth1 = CloudProviderRegion{valuer.NewString("ap-south-1")} // Asia Pacific (Mumbai).
AWSRegionAPSouth2 = CloudProviderRegion{valuer.NewString("ap-south-2")} // Asia Pacific (Hyderabad).
AWSRegionAPSoutheast1 = CloudProviderRegion{valuer.NewString("ap-southeast-1")} // Asia Pacific (Singapore).
AWSRegionAPSoutheast2 = CloudProviderRegion{valuer.NewString("ap-southeast-2")} // Asia Pacific (Sydney).
AWSRegionAPSoutheast3 = CloudProviderRegion{valuer.NewString("ap-southeast-3")} // Asia Pacific (Jakarta).
AWSRegionAPSoutheast4 = CloudProviderRegion{valuer.NewString("ap-southeast-4")} // Asia Pacific (Melbourne).
AWSRegionCACentral1 = CloudProviderRegion{valuer.NewString("ca-central-1")} // Canada (Central).
AWSRegionCAWest1 = CloudProviderRegion{valuer.NewString("ca-west-1")} // Canada West (Calgary).
AWSRegionEUCentral1 = CloudProviderRegion{valuer.NewString("eu-central-1")} // Europe (Frankfurt).
AWSRegionEUCentral2 = CloudProviderRegion{valuer.NewString("eu-central-2")} // Europe (Zurich).
AWSRegionEUNorth1 = CloudProviderRegion{valuer.NewString("eu-north-1")} // Europe (Stockholm).
AWSRegionEUSouth1 = CloudProviderRegion{valuer.NewString("eu-south-1")} // Europe (Milan).
AWSRegionEUSouth2 = CloudProviderRegion{valuer.NewString("eu-south-2")} // Europe (Spain).
AWSRegionEUWest1 = CloudProviderRegion{valuer.NewString("eu-west-1")} // Europe (Ireland).
AWSRegionEUWest2 = CloudProviderRegion{valuer.NewString("eu-west-2")} // Europe (London).
AWSRegionEUWest3 = CloudProviderRegion{valuer.NewString("eu-west-3")} // Europe (Paris).
AWSRegionILCentral1 = CloudProviderRegion{valuer.NewString("il-central-1")} // Israel (Tel Aviv).
AWSRegionMECentral1 = CloudProviderRegion{valuer.NewString("me-central-1")} // Middle East (UAE).
AWSRegionMESouth1 = CloudProviderRegion{valuer.NewString("me-south-1")} // Middle East (Bahrain).
AWSRegionSAEast1 = CloudProviderRegion{valuer.NewString("sa-east-1")} // South America (Sao Paulo).
AWSRegionUSEast1 = CloudProviderRegion{valuer.NewString("us-east-1")} // US East (N. Virginia).
AWSRegionUSEast2 = CloudProviderRegion{valuer.NewString("us-east-2")} // US East (Ohio).
AWSRegionUSWest1 = CloudProviderRegion{valuer.NewString("us-west-1")} // US West (N. California).
AWSRegionUSWest2 = CloudProviderRegion{valuer.NewString("us-west-2")} // US West (Oregon).
)
func Enum() []any {
return []any{
AWSRegionAFSouth1, AWSRegionAPEast1, AWSRegionAPNortheast1, AWSRegionAPNortheast2, AWSRegionAPNortheast3,
AWSRegionAPSouth1, AWSRegionAPSouth2, AWSRegionAPSoutheast1, AWSRegionAPSoutheast2, AWSRegionAPSoutheast3,
AWSRegionAPSoutheast4, AWSRegionCACentral1, AWSRegionCAWest1, AWSRegionEUCentral1, AWSRegionEUCentral2, AWSRegionEUNorth1,
AWSRegionEUSouth1, AWSRegionEUSouth2, AWSRegionEUWest1, AWSRegionEUWest2, AWSRegionEUWest3,
AWSRegionILCentral1, AWSRegionMECentral1, AWSRegionMESouth1, AWSRegionSAEast1, AWSRegionUSEast1, AWSRegionUSEast2,
AWSRegionUSWest1, AWSRegionUSWest2,
}
}
var SupportedRegions = map[CloudProviderType][]CloudProviderRegion{
CloudProviderTypeAWS: {
AWSRegionAFSouth1, AWSRegionAPEast1, AWSRegionAPNortheast1, AWSRegionAPNortheast2, AWSRegionAPNortheast3,
AWSRegionAPSouth1, AWSRegionAPSouth2, AWSRegionAPSoutheast1, AWSRegionAPSoutheast2, AWSRegionAPSoutheast3,
AWSRegionAPSoutheast4, AWSRegionCACentral1, AWSRegionCAWest1, AWSRegionEUCentral1, AWSRegionEUCentral2, AWSRegionEUNorth1,
AWSRegionEUSouth1, AWSRegionEUSouth2, AWSRegionEUWest1, AWSRegionEUWest2, AWSRegionEUWest3,
AWSRegionILCentral1, AWSRegionMECentral1, AWSRegionMESouth1, AWSRegionSAEast1, AWSRegionUSEast1, AWSRegionUSEast2,
AWSRegionUSWest1, AWSRegionUSWest2,
},
}
func NewAWSRegion(region string) error {
for _, r := range SupportedRegions[CloudProviderTypeAWS] {
if r.StringValue() == region {
return nil
}
}
return errors.NewInvalidInputf(ErrCodeInvalidCloudRegion, "invalid AWS region: %s", region)
}

View File

@@ -1,4 +1,4 @@
package cloudintegrationtypes
package cloudprovidertypes
import (
"github.com/SigNoz/signoz/pkg/errors"
@@ -8,6 +8,8 @@ import (
type ServiceID struct{ valuer.String }
var (
ErrCodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
AWSServiceALB = ServiceID{valuer.NewString("alb")}
AWSServiceAPIGateway = ServiceID{valuer.NewString("api-gateway")}
AWSServiceDynamoDB = ServiceID{valuer.NewString("dynamodb")}
@@ -60,16 +62,11 @@ var SupportedServices = map[CloudProviderType][]ServiceID{
},
}
// NewServiceID returns a new ServiceID from a string, validated against the supported services for the given cloud provider.
func NewServiceID(provider CloudProviderType, service string) (ServiceID, error) {
services, ok := SupportedServices[provider]
if !ok {
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID, "no services defined for cloud provider: %s", provider)
}
for _, s := range services {
for _, s := range SupportedServices[provider] {
if s.StringValue() == service {
return s, nil
}
}
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID, "invalid service id %q for cloud provider %s", service, provider)
return ServiceID{}, errors.NewInvalidInputf(ErrCodeInvalidServiceID, "invalid service id %q for %s cloud provider", service, provider.StringValue())
}

View File

@@ -1,44 +1,5 @@
package cloudintegrationtypes
import (
"github.com/SigNoz/signoz/pkg/errors"
)
var ErrCodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
// List of all valid cloud regions on Amazon Web Services.
var ValidAWSRegions = map[string]struct{}{
"af-south-1": {}, // Africa (Cape Town).
"ap-east-1": {}, // Asia Pacific (Hong Kong).
"ap-northeast-1": {}, // Asia Pacific (Tokyo).
"ap-northeast-2": {}, // Asia Pacific (Seoul).
"ap-northeast-3": {}, // Asia Pacific (Osaka).
"ap-south-1": {}, // Asia Pacific (Mumbai).
"ap-south-2": {}, // Asia Pacific (Hyderabad).
"ap-southeast-1": {}, // Asia Pacific (Singapore).
"ap-southeast-2": {}, // Asia Pacific (Sydney).
"ap-southeast-3": {}, // Asia Pacific (Jakarta).
"ap-southeast-4": {}, // Asia Pacific (Melbourne).
"ca-central-1": {}, // Canada (Central).
"ca-west-1": {}, // Canada West (Calgary).
"eu-central-1": {}, // Europe (Frankfurt).
"eu-central-2": {}, // Europe (Zurich).
"eu-north-1": {}, // Europe (Stockholm).
"eu-south-1": {}, // Europe (Milan).
"eu-south-2": {}, // Europe (Spain).
"eu-west-1": {}, // Europe (Ireland).
"eu-west-2": {}, // Europe (London).
"eu-west-3": {}, // Europe (Paris).
"il-central-1": {}, // Israel (Tel Aviv).
"me-central-1": {}, // Middle East (UAE).
"me-south-1": {}, // Middle East (Bahrain).
"sa-east-1": {}, // South America (Sao Paulo).
"us-east-1": {}, // US East (N. Virginia).
"us-east-2": {}, // US East (Ohio).
"us-west-1": {}, // US West (N. California).
"us-west-2": {}, // US West (Oregon).
}
// List of all valid cloud regions for Microsoft Azure.
var ValidAzureRegions = map[string]struct{}{
"australiacentral": {}, // Australia Central
@@ -98,11 +59,3 @@ var ValidAzureRegions = map[string]struct{}{
"westus2": {}, // West US 2
"westus3": {}, // West US 3
}
func validateAWSRegion(region string) error {
_, ok := ValidAWSRegions[region]
if !ok {
return errors.NewInvalidInputf(ErrCodeInvalidCloudRegion, "invalid AWS region: %s", region)
}
return nil
}

View File

@@ -8,39 +8,23 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes/awstypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
var ErrCodeInvalidServiceID = errors.MustNewCode("invalid_service_id")
type CloudIntegrationService struct {
types.Identifiable
types.TimeAuditable
Type ServiceID `json:"type"`
Config *ServiceConfig `json:"config"`
CloudIntegrationID valuer.UUID `json:"cloudIntegrationId"`
Type cptypes.ServiceID `json:"type"`
Config *ServiceConfig `json:"config"`
CloudIntegrationID valuer.UUID `json:"cloudIntegrationId"`
}
type ServiceConfig struct {
// required till new providers are added
AWS *AWSServiceConfig `json:"aws" required:"true" nullable:"false"`
}
type AWSServiceConfig struct {
Logs *AWSServiceLogsConfig `json:"logs"`
Metrics *AWSServiceMetricsConfig `json:"metrics"`
}
// AWSServiceLogsConfig is AWS specific logs config for a service
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
type AWSServiceLogsConfig struct {
Enabled bool `json:"enabled"`
S3Buckets map[string][]string `json:"s3Buckets,omitempty"`
}
type AWSServiceMetricsConfig struct {
Enabled bool `json:"enabled"`
AWS *awstypes.AWSServiceConfig `json:"aws" required:"true" nullable:"false"`
}
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
@@ -106,7 +90,7 @@ type DataCollected struct {
// TelemetryCollectionStrategy is cloud provider specific configuration for signal collection,
// this is used by agent to understand the nitty-gritty for collecting telemetry for the cloud provider.
type TelemetryCollectionStrategy struct {
AWS *AWSTelemetryCollectionStrategy `json:"aws" required:"true" nullable:"false"`
AWS *awstypes.AWSTelemetryCollectionStrategy `json:"aws" required:"true" nullable:"false"`
}
// Assets represents the collection of dashboards.
@@ -130,65 +114,6 @@ type CollectedMetric struct {
Description string `json:"description"`
}
// OldAWSCollectionStrategy is the backward-compatible snake_case form of AWSCollectionStrategy,
// used in the legacy integration_config response field for older agents.
type OldAWSCollectionStrategy struct {
Provider string `json:"provider"`
Metrics *OldAWSMetricsStrategy `json:"aws_metrics,omitempty"`
Logs *OldAWSLogsStrategy `json:"aws_logs,omitempty"`
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
}
// OldAWSMetricsStrategy is the snake_case form of AWSMetricsStrategy for older agents.
type OldAWSMetricsStrategy struct {
StreamFilters []struct {
Namespace string `json:"Namespace"`
MetricNames []string `json:"MetricNames,omitempty"`
} `json:"cloudwatch_metric_stream_filters"`
}
// OldAWSLogsStrategy is the snake_case form of AWSLogsStrategy for older agents.
type OldAWSLogsStrategy struct {
Subscriptions []struct {
LogGroupNamePrefix string `json:"log_group_name_prefix"`
FilterPattern string `json:"filter_pattern"`
} `json:"cloudwatch_logs_subscriptions"`
}
// AWSTelemetryCollectionStrategy represents signal collection strategy for AWS services.
type AWSTelemetryCollectionStrategy struct {
Metrics *AWSMetricsCollectionStrategy `json:"metrics,omitempty" required:"false" nullable:"false"`
Logs *AWSLogsCollectionStrategy `json:"logs,omitempty" required:"false" nullable:"false"`
S3Buckets map[string][]string `json:"s3Buckets,omitempty" required:"false"` // Only available in S3 Sync Service Type in AWS
}
// AWSMetricsCollectionStrategy represents metrics collection strategy for AWS services.
type AWSMetricsCollectionStrategy struct {
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
StreamFilters []*AWSCloudWatchMetricStreamFilter `json:"streamFilters" required:"true" nullable:"false"`
}
type AWSCloudWatchMetricStreamFilter struct {
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
Namespace string `json:"namespace" required:"true"`
MetricNames []string `json:"metricNames,omitempty" required:"false" nullable:"false"`
}
// AWSLogsCollectionStrategy represents logs collection strategy for AWS services.
type AWSLogsCollectionStrategy struct {
Subscriptions []*AWSCloudWatchLogsSubscription `json:"subscriptions" required:"true" nullable:"false"`
}
type AWSCloudWatchLogsSubscription struct {
// subscribe to all logs groups with specified prefix.
// eg: `/aws/rds/`
LogGroupNamePrefix string `json:"logGroupNamePrefix" required:"true"`
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
// "" implies no filtering is required
FilterPattern string `json:"filterPattern" required:"true"`
}
// Dashboard represents a dashboard definition for cloud integration.
// This is used to show available pre-made dashboards for a service,
// hence has additional fields like id, title and description.
@@ -199,7 +124,7 @@ type Dashboard struct {
Definition dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
}
func NewCloudIntegrationService(serviceID ServiceID, cloudIntegrationID valuer.UUID, config *ServiceConfig) *CloudIntegrationService {
func NewCloudIntegrationService(serviceID cptypes.ServiceID, cloudIntegrationID valuer.UUID, config *ServiceConfig) *CloudIntegrationService {
return &CloudIntegrationService{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
@@ -244,52 +169,52 @@ func NewGettableServicesMetadata(services []*ServiceMetadata) *GettableServicesM
}
}
func NewServiceConfigFromJSON(provider CloudProviderType, jsonString string) (*ServiceConfig, error) {
func NewServiceConfigFromJSON(provider cptypes.CloudProviderType, jsonString string) (*ServiceConfig, error) {
storableServiceConfig, err := newStorableServiceConfigFromJSON(provider, jsonString)
if err != nil {
return nil, err
}
switch provider {
case CloudProviderTypeAWS:
awsServiceConfig := new(AWSServiceConfig)
case cptypes.CloudProviderTypeAWS:
awsServiceConfig := new(awstypes.AWSServiceConfig)
if storableServiceConfig.AWS.Logs != nil {
awsServiceConfig.Logs = &AWSServiceLogsConfig{
awsServiceConfig.Logs = &awstypes.AWSServiceLogsConfig{
Enabled: storableServiceConfig.AWS.Logs.Enabled,
S3Buckets: storableServiceConfig.AWS.Logs.S3Buckets,
}
}
if storableServiceConfig.AWS.Metrics != nil {
awsServiceConfig.Metrics = &AWSServiceMetricsConfig{
awsServiceConfig.Metrics = &awstypes.AWSServiceMetricsConfig{
Enabled: storableServiceConfig.AWS.Metrics.Enabled,
}
}
return &ServiceConfig{AWS: awsServiceConfig}, nil
default:
return nil, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return nil, errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
}
// Update sets the service config.
func (service *CloudIntegrationService) Update(provider CloudProviderType, serviceID ServiceID, config *ServiceConfig) error {
func (service *CloudIntegrationService) Update(provider cptypes.CloudProviderType, serviceID cptypes.ServiceID, config *ServiceConfig) error {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
if config.AWS == nil {
return errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "AWS config is required for AWS service")
return errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "AWS config is required for AWS service")
}
if serviceID == AWSServiceS3Sync {
if serviceID == cptypes.AWSServiceS3Sync {
if config.AWS.Logs == nil || config.AWS.Logs.S3Buckets == nil {
return errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "AWS S3 Sync service requires S3 bucket configuration for logs")
return errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "AWS S3 Sync service requires S3 bucket configuration for logs")
}
}
// other validations happen in newStorableServiceConfig
default:
return errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
return errors.NewInvalidInputf(cptypes.ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider.StringValue())
}
service.Config = config
@@ -299,9 +224,9 @@ func (service *CloudIntegrationService) Update(provider CloudProviderType, servi
// IsServiceEnabled returns true if the service has at least one signal (logs or metrics) enabled
// for the given cloud provider.
func (config *ServiceConfig) IsServiceEnabled(provider CloudProviderType) bool {
func (config *ServiceConfig) IsServiceEnabled(provider cptypes.CloudProviderType) bool {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
logsEnabled := config.AWS.Logs != nil && config.AWS.Logs.Enabled
metricsEnabled := config.AWS.Metrics != nil && config.AWS.Metrics.Enabled
return logsEnabled || metricsEnabled
@@ -312,9 +237,9 @@ func (config *ServiceConfig) IsServiceEnabled(provider CloudProviderType) bool {
// IsMetricsEnabled returns true if metrics are explicitly enabled for the given cloud provider.
// Used to gate dashboard availability — dashboards are only shown when metrics are enabled.
func (config *ServiceConfig) IsMetricsEnabled(provider CloudProviderType) bool {
func (config *ServiceConfig) IsMetricsEnabled(provider cptypes.CloudProviderType) bool {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
return config.AWS.Metrics != nil && config.AWS.Metrics.Enabled
default:
return false
@@ -322,16 +247,16 @@ func (config *ServiceConfig) IsMetricsEnabled(provider CloudProviderType) bool {
}
// IsLogsEnabled returns true if logs are explicitly enabled for the given cloud provider.
func (config *ServiceConfig) IsLogsEnabled(provider CloudProviderType) bool {
func (config *ServiceConfig) IsLogsEnabled(provider cptypes.CloudProviderType) bool {
switch provider {
case CloudProviderTypeAWS:
case cptypes.CloudProviderTypeAWS:
return config.AWS.Logs != nil && config.AWS.Logs.Enabled
default:
return false
}
}
func (config *ServiceConfig) ToJSON(provider CloudProviderType, serviceID ServiceID, supportedSignals *SupportedSignals) ([]byte, error) {
func (config *ServiceConfig) ToJSON(provider cptypes.CloudProviderType, serviceID cptypes.ServiceID, supportedSignals *SupportedSignals) ([]byte, error) {
storableServiceConfig, err := newStorableServiceConfig(provider, serviceID, config, supportedSignals)
if err != nil {
return nil, err
@@ -360,20 +285,20 @@ func (updatableService *UpdatableService) UnmarshalJSON(data []byte) error {
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcID, dashboardID string) string {
func GetCloudIntegrationDashboardID(cloudProvider cptypes.CloudProviderType, svcID, dashboardID string) string {
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider.StringValue(), svcID, dashboardID)
}
// ParseCloudIntegrationDashboardID parses a dashboard id generated by GetCloudIntegrationDashboardID
// into its constituent parts (cloudProvider, serviceID, dashboardID).
func ParseCloudIntegrationDashboardID(id string) (CloudProviderType, string, string, error) {
func ParseCloudIntegrationDashboardID(id string) (cptypes.CloudProviderType, string, string, error) {
parts := strings.SplitN(id, "--", 4)
if len(parts) != 4 || parts[0] != "cloud-integration" {
return CloudProviderType{}, "", "", errors.New(errors.TypeNotFound, ErrCodeCloudIntegrationNotFound, "invalid cloud integration dashboard id")
return cptypes.CloudProviderType{}, "", "", errors.New(errors.TypeNotFound, ErrCodeCloudIntegrationNotFound, "invalid cloud integration dashboard id")
}
provider, err := NewCloudProvider(parts[1])
provider, err := cptypes.NewCloudProvider(parts[1])
if err != nil {
return CloudProviderType{}, "", "", err
return cptypes.CloudProviderType{}, "", "", err
}
return provider, parts[2], parts[3], nil
}
@@ -382,7 +307,7 @@ func ParseCloudIntegrationDashboardID(id string) (CloudProviderType, string, str
func GetDashboardsFromAssets(
svcID string,
orgID valuer.UUID,
cloudProvider CloudProviderType,
cloudProvider cptypes.CloudProviderType,
createdAt time.Time,
assets Assets,
) []*dashboardtypes.Dashboard {
@@ -426,14 +351,14 @@ func awsOlderIntegrationConfig(cfg *ProviderIntegrationConfig) *IntegrationConfi
}
// Older agents expect a "provider" field and fully snake_case keys inside telemetry.
oldTelemetry := &OldAWSCollectionStrategy{
Provider: CloudProviderTypeAWS.StringValue(),
oldTelemetry := &awstypes.OldAWSCollectionStrategy{
Provider: cptypes.CloudProviderTypeAWS.StringValue(),
S3Buckets: awsCfg.TelemetryCollectionStrategy.S3Buckets,
}
if awsCfg.TelemetryCollectionStrategy.Metrics != nil {
// Convert camelCase cloudwatchMetricStreamFilters → snake_case cloudwatch_metric_stream_filters
oldMetrics := &OldAWSMetricsStrategy{}
oldMetrics := &awstypes.OldAWSMetricsStrategy{}
for _, f := range awsCfg.TelemetryCollectionStrategy.Metrics.StreamFilters {
oldMetrics.StreamFilters = append(oldMetrics.StreamFilters, struct {
Namespace string `json:"Namespace"`
@@ -445,7 +370,7 @@ func awsOlderIntegrationConfig(cfg *ProviderIntegrationConfig) *IntegrationConfi
if awsCfg.TelemetryCollectionStrategy.Logs != nil {
// Convert camelCase cloudwatchLogsSubscriptions → snake_case cloudwatch_logs_subscriptions
oldLogs := &OldAWSLogsStrategy{}
oldLogs := &awstypes.OldAWSLogsStrategy{}
for _, s := range awsCfg.TelemetryCollectionStrategy.Logs.Subscriptions {
oldLogs.Subscriptions = append(oldLogs.Subscriptions, struct {
LogGroupNamePrefix string `json:"log_group_name_prefix"`

View File

@@ -3,24 +3,25 @@ package cloudintegrationtypes
import (
"context"
cptypes "github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes/cloudprovidertypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
// GetAccountByID returns a cloud integration account by id
GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cptypes.CloudProviderType) (*StorableCloudIntegration, error)
// GetConnectedAccount for a given provider
GetConnectedAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
GetConnectedAccount(ctx context.Context, orgID, id valuer.UUID, provider cptypes.CloudProviderType) (*StorableCloudIntegration, error)
// GetConnectedAccountByProviderAccountID returns the connected cloud integration account for a given provider account id
GetConnectedAccountByProviderAccountID(ctx context.Context, orgID valuer.UUID, providerAccountID string, provider CloudProviderType) (*StorableCloudIntegration, error)
GetConnectedAccountByProviderAccountID(ctx context.Context, orgID valuer.UUID, providerAccountID string, provider cptypes.CloudProviderType) (*StorableCloudIntegration, error)
// ListConnectedAccounts returns all the cloud integration accounts for the org and cloud provider
ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error)
ListConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) ([]*StorableCloudIntegration, error)
// CountConnectedAccounts returns the count of connected accounts for the org and cloud provider
CountConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) (int, error)
CountConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cptypes.CloudProviderType) (int, error)
// CreateAccount creates a new cloud integration account
CreateAccount(ctx context.Context, account *StorableCloudIntegration) error
@@ -29,12 +30,12 @@ type Store interface {
UpdateAccount(ctx context.Context, account *StorableCloudIntegration) error
// RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field
RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) error
RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cptypes.CloudProviderType) error
// cloud_integration_service related methods
// GetServiceByServiceID returns the cloud integration service for the given cloud integration id and service id
GetServiceByServiceID(ctx context.Context, cloudIntegrationID valuer.UUID, serviceID ServiceID) (*StorableCloudIntegrationService, error)
GetServiceByServiceID(ctx context.Context, cloudIntegrationID valuer.UUID, serviceID cptypes.ServiceID) (*StorableCloudIntegrationService, error)
// ListServices returns all the cloud integration services for the given cloud integration id
ListServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error)
@@ -49,6 +50,6 @@ type Store interface {
}
type ServiceDefinitionStore interface {
List(ctx context.Context, provider CloudProviderType) ([]*ServiceDefinition, error)
Get(ctx context.Context, provider CloudProviderType, serviceID ServiceID) (*ServiceDefinition, error)
List(ctx context.Context, provider cptypes.CloudProviderType) ([]*ServiceDefinition, error)
Get(ctx context.Context, provider cptypes.CloudProviderType, serviceID cptypes.ServiceID) (*ServiceDefinition, error)
}

View File

@@ -8,11 +8,10 @@ import (
type Config struct {
// Whether the web package is enabled.
Enabled bool `mapstructure:"enabled"`
// The name of the index file to serve.
Index string `mapstructure:"index"`
// The directory from which to serve the web files.
// The prefix to serve the files from
Prefix string `mapstructure:"prefix"`
// The directory containing the static build files. The root of this directory should
// have an index.html file.
Directory string `mapstructure:"directory"`
}
@@ -23,7 +22,7 @@ func NewConfigFactory() factory.ConfigFactory {
func newConfig() factory.Config {
return &Config{
Enabled: true,
Index: "index.html",
Prefix: "/",
Directory: "/etc/signoz/web",
}
}

View File

@@ -12,6 +12,7 @@ import (
)
func TestNewWithEnvProvider(t *testing.T) {
t.Setenv("SIGNOZ_WEB_PREFIX", "/web")
t.Setenv("SIGNOZ_WEB_ENABLED", "false")
conf, err := config.New(
@@ -36,7 +37,7 @@ func TestNewWithEnvProvider(t *testing.T) {
expected := &Config{
Enabled: false,
Index: def.Index,
Prefix: "/web",
Directory: def.Directory,
}

View File

@@ -8,55 +8,56 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/web"
"github.com/gorilla/mux"
)
const (
indexFileName string = "index.html"
)
type provider struct {
config web.Config
indexContents []byte
fileHandler http.Handler
config web.Config
}
func NewFactory(globalConfig global.Config) factory.ProviderFactory[web.Web, web.Config] {
return factory.NewProviderFactory(factory.MustNewName("router"), func(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
return New(ctx, settings, config, globalConfig)
})
func NewFactory() factory.ProviderFactory[web.Web, web.Config] {
return factory.NewProviderFactory(factory.MustNewName("router"), New)
}
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config, globalConfig global.Config) (web.Web, error) {
func New(ctx context.Context, settings factory.ProviderSettings, config web.Config) (web.Web, error) {
fi, err := os.Stat(config.Directory)
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access web directory")
}
if !fi.IsDir() {
ok := fi.IsDir()
if !ok {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "web directory is not a directory")
}
indexPath := filepath.Join(config.Directory, config.Index)
raw, err := os.ReadFile(indexPath)
fi, err = os.Stat(filepath.Join(config.Directory, indexFileName))
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot read %q in web directory", config.Index)
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot access %q in web directory", indexFileName)
}
logger := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/web/routerweb").Logger()
indexContents := web.NewIndex(ctx, logger, config.Index, raw, web.TemplateData{BaseHref: globalConfig.ExternalPathTrailing()})
if os.IsNotExist(err) || fi.IsDir() {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "%q does not exist", indexFileName)
}
return &provider{
config: config,
indexContents: indexContents,
fileHandler: http.FileServer(http.Dir(config.Directory)),
config: config,
}, nil
}
func (provider *provider) AddToRouter(router *mux.Router) error {
cache := middleware.NewCache(0)
err := router.PathPrefix("/").
err := router.PathPrefix(provider.config.Prefix).
Handler(
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
http.StripPrefix(
provider.config.Prefix,
cache.Wrap(http.HandlerFunc(provider.ServeHTTP)),
),
).GetError()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "unable to add web to router")
@@ -74,7 +75,7 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if err != nil {
// if the file doesn't exist, serve index.html
if os.IsNotExist(err) {
provider.serveIndex(rw)
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
return
}
@@ -86,15 +87,10 @@ func (provider *provider) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if fi.IsDir() {
// path is a directory, serve index.html
provider.serveIndex(rw)
http.ServeFile(rw, req, filepath.Join(provider.config.Directory, indexFileName))
return
}
// otherwise, use http.FileServer to serve the static file
provider.fileHandler.ServeHTTP(rw, req)
}
func (provider *provider) serveIndex(rw http.ResponseWriter) {
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = rw.Write(provider.indexContents)
http.FileServer(http.Dir(provider.config.Directory)).ServeHTTP(rw, req)
}

View File

@@ -5,113 +5,45 @@ import (
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/SigNoz/signoz/pkg/factory/factorytest"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/web"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func startServer(t *testing.T, config web.Config, globalConfig global.Config) string {
t.Helper()
func TestServeHttpWithoutPrefix(t *testing.T) {
t.Parallel()
fi, err := os.Open(filepath.Join("testdata", indexFileName))
require.NoError(t, err)
web, err := New(context.Background(), factorytest.NewSettings(), config, globalConfig)
expected, err := io.ReadAll(fi)
require.NoError(t, err)
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/", Directory: filepath.Join("testdata")})
require.NoError(t, err)
router := mux.NewRouter()
require.NoError(t, web.AddToRouter(router))
err = web.AddToRouter(router)
require.NoError(t, err)
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
server := &http.Server{Handler: router}
go func() { _ = server.Serve(listener) }()
t.Cleanup(func() { _ = server.Close() })
return "http://" + listener.Addr().String()
}
func httpGet(t *testing.T, url string) string {
t.Helper()
res, err := http.DefaultClient.Get(url)
require.NoError(t, err)
defer func() { _ = res.Body.Close() }()
body, err := io.ReadAll(res.Body)
require.NoError(t, err)
return string(body)
}
func TestServeTemplatedIndex(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
path string
globalConfig global.Config
expected string
}{
{
name: "RootBaseHrefAtRoot",
path: "/",
globalConfig: global.Config{},
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "RootBaseHrefAtNonExistentPath",
path: "/does-not-exist",
globalConfig: global.Config{},
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "RootBaseHrefAtDirectory",
path: "/assets",
globalConfig: global.Config{},
expected: `<html><head><base href="/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "SubPathBaseHrefAtRoot",
path: "/",
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "SubPathBaseHrefAtNonExistentPath",
path: "/does-not-exist",
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
},
{
name: "SubPathBaseHrefAtDirectory",
path: "/assets",
globalConfig: global.Config{ExternalURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/signoz"}},
expected: `<html><head><base href="/signoz/" /></head><body>Welcome to test data!!!</body></html>`,
},
server := &http.Server{
Handler: router,
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, testCase.globalConfig)
assert.Equal(t, testCase.expected, strings.TrimSuffix(httpGet(t, base+testCase.path), "\n"))
})
}
}
func TestServeNoTemplateIndex(t *testing.T) {
t.Parallel()
expected, err := os.ReadFile(filepath.Join("testdata", "no_template.html"))
require.NoError(t, err)
go func() {
_ = server.Serve(listener)
}()
defer func() {
_ = server.Close()
}()
testCases := []struct {
name string
@@ -122,7 +54,11 @@ func TestServeNoTemplateIndex(t *testing.T) {
path: "/",
},
{
name: "NonExistentPath",
name: "Index",
path: "/" + indexFileName,
},
{
name: "DoesNotExist",
path: "/does-not-exist",
},
{
@@ -131,55 +67,104 @@ func TestServeNoTemplateIndex(t *testing.T) {
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
base := startServer(t, web.Config{Index: "no_template.html", Directory: "testdata"}, global.Config{})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path)
require.NoError(t, err)
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
defer func() {
_ = res.Body.Close()
}()
actual, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, expected, actual)
})
}
}
func TestServeInvalidTemplateIndex(t *testing.T) {
func TestServeHttpWithPrefix(t *testing.T) {
t.Parallel()
expected, err := os.ReadFile(filepath.Join("testdata", "invalid_template.html"))
fi, err := os.Open(filepath.Join("testdata", indexFileName))
require.NoError(t, err)
expected, err := io.ReadAll(fi)
require.NoError(t, err)
web, err := New(context.Background(), factorytest.NewSettings(), web.Config{Prefix: "/web", Directory: filepath.Join("testdata")})
require.NoError(t, err)
router := mux.NewRouter()
err = web.AddToRouter(router)
require.NoError(t, err)
listener, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
server := &http.Server{
Handler: router,
}
go func() {
_ = server.Serve(listener)
}()
defer func() {
_ = server.Close()
}()
testCases := []struct {
name string
path string
name string
path string
found bool
}{
{
name: "Root",
path: "/",
name: "Root",
path: "/web",
found: true,
},
{
name: "NonExistentPath",
path: "/does-not-exist",
name: "Index",
path: "/web/" + indexFileName,
found: true,
},
{
name: "Directory",
path: "/assets",
name: "FileDoesNotExist",
path: "/web/does-not-exist",
found: true,
},
{
name: "Directory",
path: "/web/assets",
found: true,
},
{
name: "DoesNotExist",
path: "/does-not-exist",
found: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
base := startServer(t, web.Config{Index: "invalid_template.html", Directory: "testdata"}, global.Config{ExternalURL: &url.URL{Path: "/signoz"}})
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := http.DefaultClient.Get("http://" + listener.Addr().String() + tc.path)
require.NoError(t, err)
defer func() {
_ = res.Body.Close()
}()
if tc.found {
actual, err := io.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, expected, actual)
} else {
assert.Equal(t, http.StatusNotFound, res.StatusCode)
}
assert.Equal(t, string(expected), httpGet(t, base+testCase.path))
})
}
}
func TestServeStaticFilesUnchanged(t *testing.T) {
t.Parallel()
expected, err := os.ReadFile(filepath.Join("testdata", "assets", "style.css"))
require.NoError(t, err)
base := startServer(t, web.Config{Index: "valid_template.html", Directory: "testdata"}, global.Config{ExternalURL: &url.URL{Path: "/signoz"}})
assert.Equal(t, string(expected), httpGet(t, base+"/assets/style.css"))
}

View File

@@ -0,0 +1,3 @@
#root {
background-color: red;
}

View File

@@ -1 +0,0 @@
body { color: red; }

1
pkg/web/routerweb/testdata/index.html vendored Normal file
View File

@@ -0,0 +1 @@
<h1>Welcome to test data!!!</h1>

View File

@@ -1 +0,0 @@
<html><head><base href="[[." /></head><body>Bad template</body></html>

View File

@@ -1 +0,0 @@
<html><head></head><body>No template here</body></html>

View File

@@ -1 +0,0 @@
<html><head><base href="[[.BaseHref]]" /></head><body>Welcome to test data!!!</body></html>

View File

@@ -1,42 +0,0 @@
package web
import (
"bytes"
"context"
"log/slog"
"text/template"
"github.com/SigNoz/signoz/pkg/errors"
)
// Field names map to the HTML attributes they populate in the template:
// - BaseHref → <base href="[[.BaseHref]]" />
type TemplateData struct {
BaseHref string
}
// If the template cannot be parsed or executed, the raw bytes are
// returned unchanged and the error is logged.
func NewIndex(ctx context.Context, logger *slog.Logger, name string, raw []byte, data TemplateData) []byte {
result, err := NewIndexE(name, raw, data)
if err != nil {
logger.ErrorContext(ctx, "cannot render index template, serving raw file", slog.String("name", name), errors.Attr(err))
return raw
}
return result
}
func NewIndexE(name string, raw []byte, data TemplateData) ([]byte, error) {
tmpl, err := template.New(name).Delims("[[", "]]").Parse(string(raw))
if err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot parse %q as template", name)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return nil, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "cannot execute template for %q", name)
}
return buf.Bytes(), nil
}