-
Notifications
You must be signed in to change notification settings - Fork 68
feat(tel): implement otel sink #1888
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
base: refactor
Are you sure you want to change the base?
Changes from all commits
e2862d4
14a2603
42bc9df
4b537a7
da70cbb
ead49c9
367dee5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; | ||
| import { resourceFromAttributes } from "@opentelemetry/resources"; | ||
| import type { Logger } from "../logging"; | ||
| import { type MetricSink } from "./types"; | ||
| import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; | ||
| import { type Histogram, type Meter } from "@opentelemetry/api"; | ||
| import type { ResourceAttributes } from "./shapes"; | ||
|
|
||
| export type OtelCollectorSinkConfig = { | ||
| collectorEndpoint: string; | ||
| logger: Logger; | ||
| /** The resource attributes to attach to all metrics.**/ | ||
| resourceAttributes: ResourceAttributes; | ||
| /** The time period between export flushes **/ | ||
| exportIntervalMs?: number; | ||
| /** Describes the maximum time to wait when flushing metrics to the sink.**/ | ||
| flushTimeoutMs?: number; | ||
| /** Describes the maximum time to wait when shutting down the sink.**/ | ||
| shutdownTimeoutMs?: number; | ||
| /** | ||
| * Describes the scope to attach to the given metrics. Defaults to `agentcore-cli` | ||
| * See https://opentelemetry.io/docs/concepts/instrumentation-scope/ for more information. | ||
| **/ | ||
| instrumentationScope?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * An implementation of {@link MetricSink} that sends data to a collector using an otel histogram. | ||
| */ | ||
| export class OtelHistogramSink implements MetricSink { | ||
| private readonly name: string; | ||
| private readonly endpoint: string; | ||
| private meterProvider: MeterProvider; | ||
| private histograms: Map<string, Histogram>; | ||
| private logger: Logger; | ||
|
|
||
| private readonly flushTimeoutMs: number; | ||
| private readonly shutdownTimeoutMs: number; | ||
| private readonly scopedMeter: Meter; | ||
|
|
||
| constructor(config: OtelCollectorSinkConfig) { | ||
| this.endpoint = config.collectorEndpoint; | ||
| this.logger = config.logger.child({ telemetryEndpoint: config.collectorEndpoint }); | ||
| this.name = new.target.name; | ||
|
|
||
| this.flushTimeoutMs = config.flushTimeoutMs ?? 500; | ||
| this.shutdownTimeoutMs = config.shutdownTimeoutMs ?? 500; | ||
|
|
||
| this.meterProvider = new MeterProvider({ | ||
| resource: resourceFromAttributes(config.resourceAttributes), | ||
| readers: [ | ||
| new PeriodicExportingMetricReader({ | ||
| exporter: new OTLPMetricExporter({ | ||
| url: this.endpoint.endsWith("/v1/metrics") | ||
| ? this.endpoint | ||
| : `${this.endpoint}/v1/metrics`, | ||
| }), | ||
| exportIntervalMillis: config.exportIntervalMs ?? 5_000, | ||
| }), | ||
| ], | ||
| }); | ||
| this.scopedMeter = this.meterProvider.getMeter(config.instrumentationScope ?? "agentcore-cli"); | ||
| this.histograms = new Map<string, Histogram>(); | ||
| } | ||
|
|
||
| private getHistogram(metricName: string): Histogram { | ||
| if (!this.histograms.has(metricName)) { | ||
| this.histograms.set(metricName, this.scopedMeter.createHistogram(metricName)); | ||
| } | ||
| return this.histograms.get(metricName)!; | ||
| } | ||
|
|
||
| send(metricName: string, value: number, attributes: Record<string, string | number>): void { | ||
| this.logger | ||
| .child({ metricName, metricValue: value, metricAttributes: attributes }) | ||
| .info(`sending telemetry metric to collector`); | ||
|
|
||
| this.getHistogram(metricName).record(value, attributes); | ||
| } | ||
|
|
||
| async shutdown(): Promise<void> { | ||
| try { | ||
| await this.meterProvider.forceFlush({ timeoutMillis: this.flushTimeoutMs }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. outside this try block, there is https://opentelemetry.io/docs/specs/otel/metrics/sdk/#shutdown can we ensure one export somehow?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. from https://opentelemetry.io/docs/specs/otel/metrics/sdk/#shutdown. I don't see any explicit lines in the protocol linked for |
||
| } catch (e) { | ||
| const error = e instanceof Error ? e : new Error(String(e)); | ||
| this.logger | ||
| .child({ errorName: error.name, errorMessage: error.message }) | ||
| .warn(`failed to flush metrics to ${this.getName()}`); | ||
| // don't let flush failures prevent shutdown | ||
| } | ||
| await this.meterProvider.shutdown({ timeoutMillis: this.shutdownTimeoutMs }); | ||
| } | ||
|
|
||
| getName(): string { | ||
| return this.name; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it seems like a wrong/malformed endpoint makes
getMetricSinks()reject, andshutdown()propagates that rejection, erroring out in the CLI command. Is that understanding correct? can we make it best-effort?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we (the dev team) should be the only ones modifying the endpoint for testing purposes. In which case, I think the ideal behavior is that we reject early.
If a user decides to go into the global config and add an invalid override, I think rejecting is reasonable.