Skip to content

Commit

Permalink
[GEN-2012]: Add TLS support for Jaeger (v1 & v2) (#2021)
Browse files Browse the repository at this point in the history
This pull request includes several changes to enhance Jaeger
configuration, improve TLS support, and update the frontend for dynamic
field handling. The most important changes are summarized below:

### Jaeger Configuration Enhancements:

*
[`common/config/jaeger.go`](diffhunk://#diff-2390bce22e41af11f8b39ffe6bb9ba74d1285b0bba19612c5c282ddb3d864856L9-R19):
Added new constants `JaegerTlsKey` and `JaegerCaPemKey`, and updated
error messages for better clarity.
*
[`common/config/jaeger.go`](diffhunk://#diff-2390bce22e41af11f8b39ffe6bb9ba74d1285b0bba19612c5c282ddb3d864856R32-R97):
Modified `ModifyConfig` function to support both secure and insecure
connections, and added validation for TLS configuration.
*
[`common/config/utils.go`](diffhunk://#diff-3a30bd3819234a1d17d8a057d80ffc71550dab2fc17687b78b40e5c6f49a5136R73-R132):
Added `parseEncryptedOtlpGrpcUrl` function to handle and validate
encrypted OTLP gRPC URLs.

### Frontend Updates:

*
[`frontend/webapp/containers/main/destinations/destination-drawer/build-card.ts`](diffhunk://#diff-ce1c4d3c06218dab699d84dac4690cc261b1209716055609b79e97ab930e6862L23-R23):
Fixed the display of password fields to always show 11 dots for
consistency.
*
[`frontend/webapp/containers/main/destinations/destination-form-body/dynamic-fields/index.tsx`](diffhunk://#diff-c17889e1ffea148a206f57df5da9f8cc755e7e522a9057a339f6887c01ed5815R26-R27):
Added support for rendering checkbox input type in dynamic fields.
*
[`frontend/webapp/hooks/destinations/useConnectDestinationForm.ts`](diffhunk://#diff-ff55b24fb020911d3ee70fd88fce706bdc4348bfdebe633f7e6967f560218b4eR61):
Updated hook to handle checkbox input type.
*
[`frontend/webapp/utils/constants/string.tsx`](diffhunk://#diff-6b7ecf63c9f57c564b2f2f5d7998fd2cc45aee9e4dfc71aeeeb8361a39ba66cbR15):
Added `CHECKBOX` to `INPUT_TYPES` constants.

### Documentation Updates:

*
[`docs/backends/jaeger.mdx`](diffhunk://#diff-f00a289d3952150edbc0d05149d07088b64968bff41cfd5e1d3aaa29d12760b6L2-L31):
Updated Jaeger configuration documentation to include new TLS-related
fields and improved formatting.
[[1]](diffhunk://#diff-f00a289d3952150edbc0d05149d07088b64968bff41cfd5e1d3aaa29d12760b6L2-L31)
[[2]](diffhunk://#diff-f00a289d3952150edbc0d05149d07088b64968bff41cfd5e1d3aaa29d12760b6R47-R49)

These changes collectively improve the configuration flexibility and
security of Jaeger integrations, while also enhancing the frontend user
experience.
  • Loading branch information
BenElferink authored Dec 19, 2024
1 parent 1b67b01 commit f2af7f4
Show file tree
Hide file tree
Showing 13 changed files with 122 additions and 67 deletions.
2 changes: 1 addition & 1 deletion common/config/genericotlp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (g *GenericOTLP) ModifyConfig(dest ExporterConfigurer, currentConfig *Confi
return errors.New("Generic OTLP gRPC endpoint not specified, gateway will not be configured for otlp")
}

grpcEndpoint, err := parseUnencryptedOtlpGrpcUrl(url)
grpcEndpoint, err := parseOtlpGrpcUrl(url, false)
if err != nil {
return errors.Join(err, errors.New("otlp endpoint invalid, gateway will not be configured for otlp"))
}
Expand Down
68 changes: 46 additions & 22 deletions common/config/jaeger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import (
"github.com/odigos-io/odigos/common"
)

var (
ErrorJaegerTracingDisabled = errors.New("attempting to configure Jaeger tracing, but tracing is disabled")
ErrorJaegerMissingURL = errors.New("missing Jaeger JAEGER_URL config")
ErrorJaegerNoTls = errors.New("jaeger destination only supports non tls connections")
const (
JaegerUrlKey = "JAEGER_URL"
JaegerTlsKey = "JAEGER_TLS_ENABLED"
JaegerCaPemKey = "JAEGER_CA_PEM"
)

const (
JaegerUrlKey = "JAEGER_URL"
var (
ErrorJaegerMissingURL = errors.New("Jaeger is missing a required field (\"JAEGER_URL\"), Jaeger will not be configured")
ErrorJaegerTracingDisabled = errors.New("Jaeger is missing a required field (\"TRACES\"), Jaeger will not be configured")
ErrorJaegerMetricsNotAllowed = errors.New("Jaeger has a forbidden field (\"METRICS\"), Jaeger will not be configured")
ErrorJaegerLogsNotAllowed = errors.New("Jaeger has a forbidden field (\"LOGS\"), Jaeger will not be configured")
)

type Jaeger struct{}
Expand All @@ -26,32 +29,53 @@ func (j *Jaeger) DestType() common.DestinationType {
}

func (j *Jaeger) ModifyConfig(dest ExporterConfigurer, currentConfig *Config) error {
config := dest.GetConfig()
uniqueUri := "jaeger-" + dest.GetID()

if !isTracingEnabled(dest) {
return ErrorJaegerTracingDisabled
}

url, urlExist := dest.GetConfig()[JaegerUrlKey]
if !urlExist {
url, urlExists := config[JaegerUrlKey]
if !urlExists {
return ErrorJaegerMissingURL
}

grpcEndpoint, err := parseUnencryptedOtlpGrpcUrl(url)
tls := dest.GetConfig()[JaegerTlsKey]
tlsEnabled := tls == "true"

endpoint, err := parseOtlpGrpcUrl(url, tlsEnabled)
if err != nil {
return err
}

exporterName := "otlp/jaeger-" + dest.GetID()
currentConfig.Exporters[exporterName] = GenericMap{
"endpoint": grpcEndpoint,
"tls": GenericMap{
"insecure": true,
},
exporterName := "otlp/" + uniqueUri
exporterConfig := GenericMap{
"endpoint": endpoint,
}
tlsConfig := GenericMap{
"insecure": !tlsEnabled,
}
caPem, caExists := dest.GetConfig()[JaegerCaPemKey]
if caExists && caPem != "" {
tlsConfig["ca_pem"] = caPem
}

exporterConfig["tls"] = tlsConfig
currentConfig.Exporters[exporterName] = exporterConfig

if isTracingEnabled(dest) {
tracesPipelineName := "traces/" + uniqueUri
currentConfig.Service.Pipelines[tracesPipelineName] = Pipeline{
Exporters: []string{exporterName},
}
} else {
return ErrorJaegerTracingDisabled
}

if isMetricsEnabled(dest) {
return ErrorJaegerMetricsNotAllowed
}

pipelineName := "traces/jaeger-" + dest.GetID()
currentConfig.Service.Pipelines[pipelineName] = Pipeline{
Exporters: []string{exporterName},
if isLoggingEnabled(dest) {
return ErrorJaegerLogsNotAllowed
}

return nil
}
52 changes: 33 additions & 19 deletions common/config/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,61 @@ import (
"strings"
)

func parseUnencryptedOtlpGrpcUrl(rawURL string) (grpcUrl string, err error) {

func parseOtlpGrpcUrl(rawURL string, encrypted bool) (grpcUrl string, err error) {
rawURL = strings.TrimSpace(rawURL)
urlWithScheme := rawURL

// if no scheme is provided, we default to grpc.
// this is only for the purpose of parsing, we will ignore it later on
// Default scheme based on encryption flag
defaultScheme := "grpc"
if encrypted {
defaultScheme = "grpcs"
}

// Add scheme if not provided
if !strings.Contains(rawURL, "://") {
urlWithScheme = "grpc://" + rawURL
urlWithScheme = defaultScheme + "://" + rawURL
}

parsedUrl, err := url.Parse(urlWithScheme)
if err != nil {
return "", err
}

if parsedUrl.Scheme == "https" || parsedUrl.Scheme == "grpcs" {
return "", fmt.Errorf("grpc endpoint does not support tls")
// Validate scheme based on encryption flag
validSchemes := map[string]struct{}{
"grpc": {},
"http": {},
"grpcs": {},
"https": {},
}

if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "grpc" {
return "", fmt.Errorf("unexpected scheme %s", parsedUrl.Scheme)
if encrypted {
if _, ok := validSchemes[parsedUrl.Scheme]; !ok || (parsedUrl.Scheme != "https" && parsedUrl.Scheme != "grpcs") {
return "", fmt.Errorf("unexpected scheme %s for encrypted gRPC endpoint", parsedUrl.Scheme)
}
} else {
if parsedUrl.Scheme == "https" || parsedUrl.Scheme == "grpcs" {
return "", fmt.Errorf("grpc endpoint does not support TLS")
}
if _, ok := validSchemes[parsedUrl.Scheme]; !ok {
return "", fmt.Errorf("unexpected scheme %s for unencrypted gRPC endpoint", parsedUrl.Scheme)
}
}

// validate no path is provided, as this indicates using improper url (like otlp http with /v1/traces path)
// Validate URL components
if parsedUrl.Path != "" {
return "", fmt.Errorf("unexpected path for grpc endpoint %s", parsedUrl.Path)
return "", fmt.Errorf("unexpected path for gRPC endpoint %s", parsedUrl.Path)
}

// validate no query is provided, as this indicates using improper endpoint
if parsedUrl.RawQuery != "" {
return "", fmt.Errorf("unexpected query for grpc endpoint %s", parsedUrl.RawQuery)
return "", fmt.Errorf("unexpected query for gRPC endpoint %s", parsedUrl.RawQuery)
}

// in grpc endpoint, there is no user or password
if parsedUrl.User != nil {
return "", fmt.Errorf("unexpected user info for grpc endpoint %s", parsedUrl.User)
return "", fmt.Errorf("unexpected user info for gRPC endpoint %s", parsedUrl.User)
}

// we default to port 4317 for otlp grpc.
// if missing we add it.
// Add default port if missing
hostport := parsedUrl.Host
if !urlHostContainsPort(hostport) {
hostport += ":4317"
Expand All @@ -59,10 +73,10 @@ func parseUnencryptedOtlpGrpcUrl(rawURL string) (grpcUrl string, err error) {
}

if host == "" {
return "", fmt.Errorf("missing host in grpc endpoint %s", rawURL)
return "", fmt.Errorf("missing host in gRPC endpoint %s", rawURL)
}

// Check if the host is an IPv6 address and enclose it in square brackets
// Enclose IPv6 addresses in square brackets
if strings.Contains(host, ":") {
host = "[" + host + "]"
}
Expand Down
6 changes: 3 additions & 3 deletions common/config/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,13 +175,13 @@ func TestParseUnencryptedOtlpGrpcUrl(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseUnencryptedOtlpGrpcUrl(tt.args.rawURL)
got, err := parseOtlpGrpcUrl(tt.args.rawURL, false)
if (err != nil) != tt.wantErr {
t.Errorf("parseUnencryptedOtlpGrpcUrl() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("parseOtlpGrpcUrl() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("parseUnencryptedOtlpGrpcUrl() = %v, want %v", got, tt.want)
t.Errorf("parseOtlpGrpcUrl() = %v, want %v", got, tt.want)
}
})
}
Expand Down
13 changes: 13 additions & 0 deletions destinations/data/jaeger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,17 @@ spec:
componentProps:
type: text
required: true
- name: JAEGER_TLS_ENABLED
displayName: Enable TLS (secure connection)
componentType: checkbox
componentProps:
required: false
- name: JAEGER_CA_PEM
displayName: Certificate Authority
componentType: textarea
componentProps:
type: text
required: false
placeholder: '-----BEGIN CERTIFICATE-----'
tooltip: 'When using TLS, provide the CA certificate to verify the server. If empty uses system root CA'
testConnectionSupported: true
32 changes: 15 additions & 17 deletions docs/backends/jaeger.mdx
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
---
title: "Jaeger"
title: 'Jaeger'
---

## Configuring Jaeger Backend

Version v1.35 of Jaeger introduced the ability to receive OpenTelemetry trace data via the OpenTelemetry Protocol (OTLP).
This allows to create a new Jaeger backend by simply specifying the Jaeger OTLP gRPC unencrypted URL.

The endpoint format is `host:port`.
- host is required
- port is optional and defaults to the default OTLP gRPC port `4317`.

- **JAEGER_URL** - Endpoint, the format is `host:port`.
- host is required
- port is optional and defaults to the default OTLP gRPC port `4317`.
- **JAEGER_TLS_ENABLED** - Enable TLS (secure connection). This is optional and defaults to `false`.
- **JAEGER_CA_PEM** - Certificate Authority in PEM format. When using TLS, provide the CA certificate to verify the server. If empty uses system root CA

## Adding a Destination to Odigos

Odigos makes it simple to add and configure destinations, allowing you to select the specific signals [traces/logs/metrics] that you want to send to each destination. There are two primary methods for configuring destinations in Odigos:

1. **Using the UI**
To add a destination via the UI, follow these steps:
- Use the Odigos CLI to access the UI: [Odigos UI](https://docs.odigos.io/cli/odigos_ui)
```bash
odigos ui
```
- In the left sidebar, navigate to the `Destination` page.

- Click `Add New Destination`

- Select `Jaeger` and follow the on-screen instructions.
1. **Using the UI**

Use the [Odigos CLI](https://docs.odigos.io/cli/odigos_ui) to access the UI:

```bash
odigos ui
```

2. **Using kubernetes manifests**

Expand All @@ -48,8 +43,11 @@ metadata:
spec:
data:
JAEGER_URL: <Jaeger OTLP gRPC Endpoint>
# JAEGER_TLS_ENABLED: <Enable TLS (secure connection)>
# JAEGER_CA_PEM: <Certificate Authority>
# Note: The commented fields above are optional.
destinationName: jaeger
signals:
- TRACES
type: jaeger
```
```
3 changes: 2 additions & 1 deletion frontend/endpoints/destination_recognition/jaeger.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package destination_recognition

import (
"fmt"
"strings"

"github.com/odigos-io/odigos/common"
"github.com/odigos-io/odigos/common/config"
k8s "k8s.io/api/core/v1"
"strings"
)

type JaegerDestinationFinder struct{}
Expand Down
3 changes: 2 additions & 1 deletion frontend/endpoints/destinations.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"

"github.com/odigos-io/odigos/frontend/endpoints/destination_recognition"
"github.com/odigos-io/odigos/k8sutils/pkg/env"
"net/http"

"github.com/gin-gonic/gin"
"github.com/odigos-io/odigos/api/odigos/v1alpha1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const buildCard = (destination: ActualDestination, destinationTypeDetails: Desti
const found = destinationTypeDetails?.fields?.find((field) => field.name === key);

const { type } = safeJsonParse(found?.componentProperties, { type: '' });
const secret = type === 'password' ? new Array(value.length).fill('•').join('') : '';
const secret = type === 'password' ? new Array(11).fill('•').join('') : '';

arr.push({
title: found?.displayName || key,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { INPUT_TYPES } from '@/utils';
import { Dropdown, Input, TextArea, InputList, KeyValueInputsList } from '@/reuseable-components';
import { Dropdown, Input, TextArea, InputList, KeyValueInputsList, Checkbox } from '@/reuseable-components';

interface Props {
fields: any[];
Expand All @@ -23,6 +23,8 @@ export const DestinationDynamicFields: React.FC<Props> = ({ fields, onChange, fo
return <KeyValueInputsList key={field.name} {...rest} onChange={(value) => onChange(field.name, JSON.stringify(value))} errorMessage={formErrors[field.name]} />;
case INPUT_TYPES.TEXTAREA:
return <TextArea key={field.name} {...rest} onChange={(e) => onChange(field.name, e.target.value)} errorMessage={formErrors[field.name]} />;
case INPUT_TYPES.CHECKBOX:
return <Checkbox key={field.name} {...rest} initialValue={rest.value == 'true'} onChange={(bool) => onChange(field.name, String(bool))} errorMessage={formErrors[field.name]} />;
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function useConnectDestinationForm() {
};

case INPUT_TYPES.KEY_VALUE_PAIR:
case INPUT_TYPES.CHECKBOX:
return {
name,
componentType,
Expand Down
2 changes: 1 addition & 1 deletion frontend/webapp/hooks/destinations/useDestinationCRUD.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const useDestinationCRUD = (params?: Params) => {
loading: cState.loading || uState.loading || dState.loading,
destinations: data?.computePlatform.destinations || [],

createDestination: (destination: DestinationInput) => createDestination({ variables: { destination } }),
createDestination: (destination: DestinationInput) => createDestination({ variables: { destination: { ...destination, fields: destination.fields.filter(({ value }) => value !== undefined) } } }),
updateDestination: (id: string, destination: DestinationInput) => updateDestination({ variables: { id, destination } }),
deleteDestination: (id: string) => deleteDestination({ variables: { id } }),
};
Expand Down
1 change: 1 addition & 0 deletions frontend/webapp/utils/constants/string.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const INPUT_TYPES = {
MULTI_INPUT: 'multiInput',
KEY_VALUE_PAIR: 'keyValuePairs',
TEXTAREA: 'textarea',
CHECKBOX: 'checkbox',
};

export const ACTION = {
Expand Down

0 comments on commit f2af7f4

Please sign in to comment.