Skip to content
17 changes: 16 additions & 1 deletion lib/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const {
View
} = require('@opentelemetry/sdk-metrics')

const { getDynatraceMetadata, getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils')
const { getDynatraceMetadata, getCredsForDTAsUPS, getCredsForCLSAsUPS, getCredsForCaaS, augmentCLCreds, augmentCaaSCreds, _require } = require('../utils')

const _protocol2module = {
grpc: '@opentelemetry/exporter-metrics-otlp-grpc',
Expand Down Expand Up @@ -70,6 +70,21 @@ function _getExporter() {
config.credentials ??= credentials.credentials
}

if (kind.match(/to-caas$/)) {
if (!credentials) credentials = getCredsForCaaS()
if (!credentials) throw new Error('No CaaS credentials found.')
augmentCaaSCreds(credentials)
// Append /v1/metrics to base URL (OTLP exporter expects full URL when config.url is provided)
config.url ??= credentials.baseUrl ? credentials.baseUrl + '/v1/metrics' : credentials.url + '/v1/metrics'
// Pass mTLS agent options if available (OTLP exporter uses 'httpAgentOptions')
if (credentials.httpAgentOptions) {
config.httpAgentOptions = credentials.httpAgentOptions
LOG._info && LOG.info('CaaS metrics exporter config: url=' + config.url + ', httpAgentOptions.cert exists=' + !!config.httpAgentOptions.cert)
} else {
LOG._warn && LOG.warn('CaaS metrics: no httpAgentOptions found in credentials')
}
}

// default to DELTA
config.temporalityPreference ??= AggregationTemporality.DELTA

Expand Down
19 changes: 19 additions & 0 deletions lib/tracing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const {
getDynatraceMetadata,
getCredsForDTAsUPS,
getCredsForCLSAsUPS,
getCredsForCaaS,
augmentCLCreds,
augmentCaaSCreds,
hasDependency,
_require
} = require('../utils')
Expand Down Expand Up @@ -125,6 +127,23 @@ function _getExporter() {
config.credentials ??= credentials.credentials
}

if (kind.match(/to-caas$/)) {
if (!credentials) credentials = getCredsForCaaS()
if (!credentials) throw new Error('No CaaS credentials found')
Comment thread
vkozyura marked this conversation as resolved.
augmentCaaSCreds(credentials)
// Append /v1/traces to base URL (OTLP exporter expects full URL when config.url is provided)
config.url ??= credentials.baseUrl ? credentials.baseUrl + '/v1/traces' : credentials.url + '/v1/traces'
// Pass mTLS agent options if available (OTLP exporter uses 'httpAgentOptions')
if (credentials.httpAgentOptions) {
config.httpAgentOptions = credentials.httpAgentOptions
LOG._info && LOG.info('CaaS exporter config: url=' + config.url + ', httpAgentOptions.cert exists=' + !!config.httpAgentOptions.cert)
LOG._debug && LOG.debug('CaaS exporter full config keys:', Object.keys(config))
} else {
LOG._warn && LOG.warn('CaaS: no httpAgentOptions found in credentials')
}
}

LOG._debug && LOG.debug('Creating exporter with config:', JSON.stringify({ url: config.url, hasHttpAgentOptions: !!config.httpAgentOptions, httpAgentOptionKeys: config.httpAgentOptions ? Object.keys(config.httpAgentOptions) : [] }))
const exporter = new tracingExporterModule[tracingExporter.class](config)
LOG._debug && LOG.debug('Using trace exporter:', exporter)

Expand Down
74 changes: 74 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,77 @@ function getCredsForCLSAsUPS() {
}
}

function getCredsForCaaS() {
if (!process.env.VCAP_SERVICES) return
const vcap = JSON.parse(process.env.VCAP_SERVICES)

// look for caas-service binding
const caas = vcap['caas-service']?.[0]
if (caas) return caas.credentials
}

function getCredsForCertService() {
if (!process.env.VCAP_SERVICES) return
const vcap = JSON.parse(process.env.VCAP_SERVICES)
const certSvc = vcap['certificate-service']?.[0]
if (certSvc) return certSvc.credentials
}

function getCredsForCaaSMtls() {
if (!process.env.VCAP_SERVICES) return
const vcap = JSON.parse(process.env.VCAP_SERVICES)
// Look for user-provided service with name containing 'caas-mtls' or 'caas-cert'
const mtlsCreds = vcap['user-provided']?.find(b => b.name.match(/caas-mtls|caas-cert/i))
if (mtlsCreds) return mtlsCreds.credentials
}

function augmentCaaSCreds(credentials) {
if (credentials._augmented) return
credentials._augmented = true

// check for otlp endpoints
if (!credentials.otlp?.http && !credentials.otlp?.grpc) {
throw new Error('No OTLP endpoints found in CaaS binding. Make sure the CaaS instance is properly configured.')
}

// Store the base URL - path will be added per signal type (traces: /v1/traces, metrics: /v1/metrics)
credentials.baseUrl = credentials.otlp.http
// Also set url for backwards compatibility (without path - exporter will append it)
credentials.url = credentials.otlp.http

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Logic Error: When credentials.otlp.http is falsy but credentials.otlp.grpc exists, credentials.url is set to undefined (the falsy http value), silently misconfiguring the exporter. The URL should fall back to the gRPC endpoint when http is absent.

Suggested change
credentials.url = credentials.otlp.http
credentials.url = credentials.otlp.http ?? credentials.otlp.grpc

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


// Check for mTLS credentials (cert + key) in user-provided service
const mtlsCreds = getCredsForCaaSMtls()
if (mtlsCreds && mtlsCreds.cert && mtlsCreds.key) {
LOG._info && LOG.info('Found CaaS mTLS credentials, configuring HTTPS agent options')
try {
const cert = Buffer.from(mtlsCreds.cert, 'base64').toString('utf-8')
const key = Buffer.from(mtlsCreds.key, 'base64').toString('utf-8')

// Store the mTLS options for the exporter's httpAgentOptions
// The OTLP HTTP exporter will create an https.Agent with these options
credentials.httpAgentOptions = {
cert: cert,
key: key,
keepAlive: true
}

LOG._info && LOG.info('CaaS mTLS credentials configured successfully')
} catch (err) {
LOG._error && LOG.error('Failed to configure CaaS mTLS:', err.message)
}
} else {
// Check for certificate-service binding (manual enrollment needed)
const certServiceCreds = getCredsForCertService()
if (certServiceCreds) {
LOG._warn && LOG.warn('Found certificate-service binding but no caas-mtls-creds.')
LOG._warn && LOG.warn('Run the certificate enrollment script and create caas-mtls-creds service.')
} else {
LOG._warn && LOG.warn('CaaS requires mTLS authentication. No mTLS credentials found.')
LOG._warn && LOG.warn('Bind a caas-mtls-creds user-provided service with cert and key.')
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: augmentCaaSCreds mutates the original credentials object from the VCAP binding (sets _augmented, url) as a side effect. If the credentials object is reused elsewhere, or getCredsForCaaS() is called again (e.g., both tracing and metrics paths), the first call marks _augmented = true and the second call skips re-augmentation - which is fine, but the mutation of a shared object can be surprising. Returning a new augmented object (like augmentCLCreds does) or at minimum being consistent with the other augment pattern is safer.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


function augmentCLCreds(credentials) {
if (credentials._augmented) return
credentials._augmented = true
Expand Down Expand Up @@ -194,7 +265,10 @@ module.exports = {
getDynatraceMetadata,
getCredsForDTAsUPS,
getCredsForCLSAsUPS,
getCredsForCaaS,
getCredsForCertService,
augmentCLCreds,
augmentCaaSCreds,
hasDependency,
_hrnow,
_require
Expand Down
17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,23 @@
"metrics": {
"exporter": "env"
}
},
"telemetry-to-caas": {
"vcap": {
"label": "caas-service"
},
"tracing": {
"exporter": {
"module": "@opentelemetry/exporter-trace-otlp-proto",
"class": "OTLPTraceExporter"
}
},
"metrics": {
"exporter": {
"module": "@opentelemetry/exporter-metrics-otlp-proto",
"class": "OTLPMetricExporter"
}
}
}
}
}
Expand Down