Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update qryn+gigapipe docs #1594

Merged
merged 14 commits into from
Oct 17, 2024
148 changes: 107 additions & 41 deletions common/config/qryn.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,87 +9,136 @@ import (
)

const (
qrynHost = "QRYN_URL"
qrynAPIKey = "QRYN_API_KEY"
qrynAPISecret = "${QRYN_API_SECRET}"
qrynHost = "QRYN_URL"
qrynAPIKey = "QRYN_API_KEY"
qrynAddExporterName = "QRYN_ADD_EXPORTER_NAME"
resourceToTelemetryConversion = "QRYN_RESOURCE_TO_TELEMETRY_CONVERSION"
qrynSecretsOptional = "__QRYN_SECRETS_OPTIONAL__"
qrynPasswordFieldName = "__QRYN_PASSWORD_FIELD_NAME__"
)

type qrynConf struct {
host string
key string
addExporterName bool
resourceToTelemetryConversion bool
secretsOptional bool
passwordFieldName string
}

type Qryn struct{}

func (g *Qryn) DestType() common.DestinationType {
return common.QrynDestinationType
}

func (g *Qryn) ModifyConfig(dest ExporterConfigurer, currentConfig *Config) error {
if !g.requiredVarsExists(dest) {
return errors.New("Qryn config is missing required variables")
}
apiKey, apiSecret := g.authData(dest)
if apiKey == "" || apiSecret == "" {
return errors.New("Qryn API key or secret not set")
conf := g.getConfigs(dest)
err := g.checkConfigs(&conf)
if err != nil {
return err
}

baseURL, err := parseURL(dest.GetConfig()[qrynHost], apiKey, apiSecret)
passwordPlaceholder := "${QRYN_API_SECRET}"
if conf.passwordFieldName != "" {
passwordPlaceholder = "${" + conf.passwordFieldName + "}"
}
baseURL, err := parseURL(dest.GetConfig()[qrynHost], conf.key, passwordPlaceholder)
if err != nil {
return errors.New("Qryn API host is not a valid")
return errors.New("API host is not a valid")
lmangani marked this conversation as resolved.
Show resolved Hide resolved
}

if isMetricsEnabled(dest) {
rwExporterName := "prometheusremotewrite/qryn-" + dest.GetID()
currentConfig.Exporters[rwExporterName] = GenericMap{
"endpoint": fmt.Sprintf("%s/api/v1/prom/remote/write", baseURL),
"resource_to_telemetry_conversion": GenericMap{
"enabled": dest.GetConfig()[resourceToTelemetryConversion] == "Yes",
},
}
metricsPipelineName := "metrics/qryn-" + dest.GetID()
currentConfig.Service.Pipelines[metricsPipelineName] = Pipeline{
ppl := Pipeline{
Exporters: []string{rwExporterName},
}
g.maybeAddExporterName(
&conf,
currentConfig,
"resource/qryn-metrics-name-"+dest.GetID(),
"odigos-qryn-metrics",
&ppl,
)
currentConfig.Service.Pipelines[metricsPipelineName] = ppl

}

otlpHttpExporterName := ""
otlpHttpExporter := GenericMap{}
if isTracingEnabled(dest) {
exporterName := "otlp/qryn-" + dest.GetID()
currentConfig.Exporters[exporterName] = GenericMap{
"endpoint": fmt.Sprintf("%s/tempo/spans", baseURL),
}
otlpHttpExporterName = "otlphttp/qryn-" + dest.GetID()
otlpHttpExporter["traces_endpoint"] = fmt.Sprintf("%s/v1/traces", baseURL)
otlpHttpExporter["encoding"] = "proto"
otlpHttpExporter["compression"] = "none"
tracesPipelineName := "traces/qryn-" + dest.GetID()
currentConfig.Service.Pipelines[tracesPipelineName] = Pipeline{
Exporters: []string{exporterName},
ppl := Pipeline{
Exporters: []string{otlpHttpExporterName},
}
g.maybeAddExporterName(
&conf,
currentConfig,
"resource/qryn-traces-name-"+dest.GetID(),
"odigos-qryn-traces",
&ppl,
)
currentConfig.Service.Pipelines[tracesPipelineName] = ppl

}

if isLoggingEnabled(dest) {
lokiExporterName := "loki/qryn-" + dest.GetID()
currentConfig.Exporters[lokiExporterName] = GenericMap{
"endpoint": fmt.Sprintf("%s/loki/api/v1/push", baseURL),
"labels": GenericMap{
"attributes": GenericMap{
"k8s.container.name": "k8s_container_name",
"k8s.pod.name": "k8s_pod_name",
"k8s.namespace.name": "k8s_namespace_name",
},
},
}
otlpHttpExporterName = "otlphttp/qryn-" + dest.GetID()
otlpHttpExporter["logs_endpoint"] = fmt.Sprintf("%s/v1/logs", baseURL)
logsPipelineName := "logs/qryn-" + dest.GetID()
currentConfig.Service.Pipelines[logsPipelineName] = Pipeline{
Exporters: []string{lokiExporterName},
otlpHttpExporter["encoding"] = "proto"
otlpHttpExporter["compression"] = "none"
ppl := Pipeline{
Exporters: []string{otlpHttpExporterName},
}
g.maybeAddExporterName(
&conf,
currentConfig,
"resource/qryn-logs-name-"+dest.GetID(),
"odigos-qryn-logs",
&ppl,
)
currentConfig.Service.Pipelines[logsPipelineName] = ppl

}

if otlpHttpExporterName != "" {
currentConfig.Exporters[otlpHttpExporterName] = otlpHttpExporter
}

return nil
}

func (g *Qryn) requiredVarsExists(dest ExporterConfigurer) bool {
if _, ok := dest.GetConfig()[qrynHost]; !ok {
return false
func (g *Qryn) getConfigs(dest ExporterConfigurer) qrynConf {
return qrynConf{
host: dest.GetConfig()[qrynHost],
key: dest.GetConfig()[qrynAPIKey],
addExporterName: dest.GetConfig()[qrynAddExporterName] == "Yes",
resourceToTelemetryConversion: dest.GetConfig()[resourceToTelemetryConversion] == "Yes",
secretsOptional: dest.GetConfig()[qrynSecretsOptional] == "1",
blumamir marked this conversation as resolved.
Show resolved Hide resolved
passwordFieldName: dest.GetConfig()[qrynPasswordFieldName],
}
return true
}

func (g *Qryn) authData(dest ExporterConfigurer) (string, string) {
var key string
if k, ok := dest.GetConfig()[qrynAPIKey]; ok {
key = k
func (g *Qryn) checkConfigs(conf *qrynConf) error {
if conf.host == "" {
return errors.New("missing URL")
}
return key, qrynAPISecret
if !conf.secretsOptional && conf.key == "" {
return errors.New("missing API key")
}
return nil
}

func parseURL(rawURL, apiKey, apiSecret string) (string, error) {
Expand All @@ -101,5 +150,22 @@ func parseURL(rawURL, apiKey, apiSecret string) (string, error) {
return parseURL(fmt.Sprintf("https://%s", rawURL), apiKey, apiSecret)
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider trimming rawURL white spaces, as it will make url.Parse(rawURL) return with error.

	noWhiteSpaces := strings.TrimSpace(rawUrl)
	parsedUrl, err := url.Parse(noWhiteSpaces)

return fmt.Sprintf("https://%s:%s@%s", apiKey, apiSecret, u.Host), nil
return fmt.Sprintf("%s://%s:%s@%s", u.Scheme, apiKey, apiSecret, u.Host), nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important: if we encode the secret into the url this way, it will show up as plain text in the config map which can be undesirable for some users due to security concerns.

You can use the basicauthextension to implement this authentication, while keeping the password as env variable. see otlphttp for example code on how to use it

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: after the latest commit, the value is no longer plain text in the configmap which is great.

If you like, you can still consider using the basic auto extension, or resolve this thread.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we just take the u.Host, we will silently drop any u.Path that the user might have set. I guess in most cases this is fine, but in clusters with internal routing this might break the export.

I am also fine with keeping it as is if you prefer and address this issue if someone bumps into it.

An alternative can be to modify the u object itself, and then use u.String() to make it a safe url string in idomatic way

}

func (g *Qryn) maybeAddExporterName(conf *qrynConf, currentConfig *Config, processorName string, name string,
Copy link
Collaborator

@blumamir blumamir Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on the usage for this exporter name?

I see it adds an attribute "qryn_exporter" to all telemetry signals with values odigos-qryn-logs, odigos-qryn-traces, odigos-qryn-metrics.

Is this added to trace in qryn where the data came from?

Is qryn_exporter a common attribute in qryn ecosystem?

pipeline *Pipeline) {
if !conf.addExporterName {
return
}
currentConfig.Processors[processorName] = GenericMap{
"attributes": []GenericMap{
{
"action": "upsert",
"key": "qryn_exporter",
"value": name,
},
},
}
pipeline.Processors = append(pipeline.Processors, processorName)
}
39 changes: 39 additions & 0 deletions common/config/qryn_oss.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package config

import (
"github.com/odigos-io/odigos/common"
)

const (
qrynOssHost = "QRYN_OSS_URL"
qrynOssUsername = "QRYN_OSS_USERNAME"
qrynOssresourceToTelemetryConversion = "QRYN_OSS_RESOURCE_TO_TELEMETRY_CONVERSION"
qrynOssAddExporterName = "QRYN_OSS_ADD_EXPORTER_NAME"
)

type QrynOSS struct {
*Qryn
}

type QrynOssDest struct {
ExporterConfigurer
}

func (d QrynOssDest) GetConfig() map[string]string {
conf := d.ExporterConfigurer.GetConfig()
conf[qrynHost] = conf[qrynOssHost]
conf[qrynAPIKey] = conf[qrynOssUsername]
conf[resourceToTelemetryConversion] = conf[qrynOssresourceToTelemetryConversion]
conf[qrynSecretsOptional] = "1"
conf[qrynAddExporterName] = conf[qrynOssAddExporterName]
conf[qrynPasswordFieldName] = "QRYN_OSS_PASSWORD"
return conf
}

func (g *QrynOSS) DestType() common.DestinationType {
return common.QrynOSSDestinationType
}

func (g *QrynOSS) ModifyConfig(dest ExporterConfigurer, currentConfig *Config) error {
return g.Qryn.ModifyConfig(QrynOssDest{dest}, currentConfig)
}
66 changes: 33 additions & 33 deletions common/config/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var availableConfigers = []Configer{
&Tempo{}, &Loki{}, &Jaeger{}, &GenericOTLP{}, &OTLPHttp{}, &Elasticsearch{}, &Quickwit{}, &Signoz{}, &Qryn{},
&OpsVerse{}, &Splunk{}, &Lightstep{}, &GoogleCloud{}, &GoogleCloudStorage{}, &Sentry{}, &AzureBlobStorage{},
&AWSS3{}, &Dynatrace{}, &Chronosphere{}, &ElasticAPM{}, &Axiom{}, &SumoLogic{}, &Coralogix{}, &Clickhouse{},
&Causely{}, &Uptrace{}, &Debug{},
&Causely{}, &Uptrace{}, &Debug{}, &QrynOSS{},
}

type Configer interface {
Expand Down Expand Up @@ -144,46 +144,46 @@ func CalculateWithBase(currentConfig *Config, prefixProcessors []string, dests [
// In addition it returns prefix processors that should be added to beginning of each pipeline.
func getBasicConfig(memoryLimiterConfig GenericMap) (*Config, []string) {
return &Config{
Receivers: GenericMap{
"otlp": GenericMap{
"protocols": GenericMap{
"grpc": GenericMap{
// setting it to a large value to avoid dropping batches.
"max_recv_msg_size_mib": 128 * 1024 * 1024,
"endpoint": "0.0.0.0:4317",
},
// Node collectors send in gRPC, so this is probably not needed
"http": GenericMap{
"endpoint": "0.0.0.0:4318",
Receivers: GenericMap{
"otlp": GenericMap{
"protocols": GenericMap{
"grpc": GenericMap{
// setting it to a large value to avoid dropping batches.
"max_recv_msg_size_mib": 128 * 1024 * 1024,
"endpoint": "0.0.0.0:4317",
},
// Node collectors send in gRPC, so this is probably not needed
"http": GenericMap{
"endpoint": "0.0.0.0:4318",
},
},
},
},
},
Processors: GenericMap{
memoryLimiterProcessorName: memoryLimiterConfig,
"resource/odigos-version": GenericMap{
"attributes": []GenericMap{
{
"key": "odigos.version",
"value": "${ODIGOS_VERSION}",
"action": "upsert",
Processors: GenericMap{
memoryLimiterProcessorName: memoryLimiterConfig,
"resource/odigos-version": GenericMap{
"attributes": []GenericMap{
{
"key": "odigos.version",
"value": "${ODIGOS_VERSION}",
"action": "upsert",
},
},
},
},
},
Extensions: GenericMap{
"health_check": GenericMap{
"endpoint": "0.0.0.0:13133",
Extensions: GenericMap{
"health_check": GenericMap{
"endpoint": "0.0.0.0:13133",
},
},
Exporters: map[string]interface{}{},
Connectors: map[string]interface{}{},
Service: Service{
Pipelines: map[string]Pipeline{},
Extensions: []string{"health_check"},
},
},
Exporters: map[string]interface{}{},
Connectors: map[string]interface{}{},
Service: Service{
Pipelines: map[string]Pipeline{},
Extensions: []string{"health_check"},
},
},
[]string{memoryLimiterProcessorName, "resource/odigos-version"}
[]string{memoryLimiterProcessorName, "resource/odigos-version"}
}

func LoadConfigers() (map[common.DestinationType]Configer, error) {
Expand Down
1 change: 1 addition & 0 deletions common/dests.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
OtlpHttpDestinationType DestinationType = "otlphttp"
PrometheusDestinationType DestinationType = "prometheus"
QrynDestinationType DestinationType = "qryn"
QrynOSSDestinationType DestinationType = "qryn-oss"
QuickwitDestinationType DestinationType = "quickwit"
SentryDestinationType DestinationType = "sentry"
SignozDestinationType DestinationType = "signoz"
Expand Down
23 changes: 20 additions & 3 deletions destinations/data/qryn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ apiVersion: internal.odigos.io/v1beta1
kind: Destination
metadata:
type: qryn
displayName: qryn
displayName: Gigapipe
category: managed
spec:
image: qryn.svg
image: gigapipe.svg
signals:
traces:
supported: true
Expand All @@ -27,10 +27,27 @@ spec:
componentProps:
type: password
required: true
secret: true
- name: QRYN_URL
displayName: API Url
componentType: input
componentProps:
type: text
required: true
- name: QRYN_RESOURCE_TO_TELEMETRY_CONVERSION
displayName: Convert container attributes to labels
componentType: dropdown
componentProps:
values:
- Yes
- No
required: false
initialValue: Yes
- name: QRYN_ADD_EXPORTER_NAME
displayName: Add exporter name to labels
componentType: dropdown
componentProps:
values:
- Yes
- No
required: false
initialValue: Yes
Loading
Loading