Any other language: OpenTelemetry
Java, Ruby and Rust now have native SDKs, see the Java, Ruby and Rust pages. For languages without one - .NET, PHP - the honest answer is the one those teams already run: their own OpenTelemetry SDK, pointed at e-volv with a preset. (The Java, Ruby and Rust presets below remain valid for teams already on OpenTelemetry.) A preset is one file that sets the OTLP/HTTP exporter at e-volv’s ingest, the project key as the Authorization header, gzip, the resource attributes the product reads, and the exception convention that becomes an error group.
The endpoint for every preset is https://api.e-volv.io/api/public/v1/logs/otlp: traces post to …/logs/otlp/v1/traces, logs to …/logs/otlp/v1/logs. The key is the ingest key of the project’s service (minted on the Observer Projects page, shown once), export it as EVOLVE_LOGS_KEY.
Presets live in the repository under packages/logs-otel-presets/ - copy the one for your language into your service and adjust service.name, deployment.environment and service.release. A conformance script (conformance/check.sh) runs the collector path end to end against a stub ingest.
Java (agent)
Run the OTel Java agent with the preset as its configuration file. The agent records thrown exceptions as span events with the standard exception.* attributes - the ingest maps those to error groups - and the Logback/Log4j2 bridges forward error lines as OTel logs.
# the key and resource attributes ride on env vars: properties files do
# not expand ${VAR}, and the environment overrides the file
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer evk_..."
export OTEL_RESOURCE_ATTRIBUTES="service.name=my-service,deployment.environment=production,service.release=$GIT_SHA"
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.javaagent.configuration-file=otel.properties MyApp.jar
# otel.properties (packages/logs-otel-presets/java/otel.properties)
otel.traces.exporter=otlp
otel.logs.exporter=otlp
otel.exporter.otlp.endpoint=https://api.e-volv.io/api/public/v1/logs/otlp
otel.exporter.otlp.protocol=http/protobuf
otel.exporter.otlp.compression=gzip.NET (auto-instrumentation)
set -a; . ./otel.env; set +a # packages/logs-otel-presets/dotnet/otel.env
EVOLVE_LOGS_KEY=evk_... dotnet run
OTEL_TRACES_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.e-volv.io/api/public/v1/logs/otlp
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_COMPRESSION=gzip
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer ${EVOLVE_LOGS_KEY}"
OTEL_RESOURCE_ATTRIBUTES="service.name=my-service,deployment.environment=production,service.release=${GIT_SHA}"Ruby
# packages/logs-otel-presets/ruby/preset.rb
OpenTelemetry::SDK.configure do |c|
c.service_name = ENV.fetch("OTEL_SERVICE_NAME", "my-service")
c.add_span_processor(
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
OpenTelemetry::Exporter::OTLP::TraceExporter.new(
endpoint: "https://api.e-volv.io/api/public/v1/logs/otlp/v1/traces",
headers: { "Authorization" => "Bearer #{ENV.fetch('EVOLVE_LOGS_KEY')}" },
compression: "gzip"
)
)
)
endPHP
// packages/logs-otel-presets/php/preset.php
$transport = (new OtlpHttpTransportFactory())->create(
'https://api.e-volv.io/api/public/v1/logs/otlp/v1/traces',
'application/x-protobuf',
['Authorization' => 'Bearer ' . getenv('EVOLVE_LOGS_KEY'),
'Content-Encoding' => 'gzip'],
);
$tracerProvider = new TracerProvider(
new BatchSpanProcessor(new SpanExporter($transport)),
new AlwaysOnSampler(),
ResourceInfo::create(Attributes::create([
'service.name' => 'my-service',
'deployment.environment' => 'production',
'service.release' => getenv('GIT_SHA') ?: '',
])),
);Rust
// packages/logs-otel-presets/rust/preset.rs
// features = ["http-proto", "reqwest-client", "gzip-http"]
use opentelemetry_otlp::{Compression, WithExportConfig, WithHttpConfig};
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(otlp_base) // /v1/traces is appended by the exporter
.with_headers(HashMap::from([("Authorization".to_string(), format!("Bearer {key}"))]))
.with_compression(Compression::Gzip)
.build()?;
let provider = TracerProvider::builder()
.with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
.with_resource(Resource::new(vec![
KeyValue::new("service.name", "my-service"),
KeyValue::new("deployment.environment", "production"),
KeyValue::new("service.release", git_sha),
]))
.build();Exceptions become error groups
Whatever the language, record exceptions with the SDK’s standard mechanism - record_exception, recordException, span.record_error(...) - so the span event carries exception.type, exception.message and exception.stacktrace. The ingest reads exactly those attributes; within a minute the error appears under an error group on the project’s Errors page, with the trace graph showing which spans it hit.
Everything else: the collector
For anything the presets do not cover - a metrics pipeline, a protocol we do not ingest natively, a service that already exports to a collector - the collector configuration in the OTLP section of the Observer docs (and docs/OBSERVER-OTEL-COLLECTOR.md in the repository) forwards OTLP to the same ingest endpoints. One collector can carry a whole fleet of languages.
Verifying an install
Trigger one error through the instrumented path, then check the project’s Errors and Traces pages within a minute. Nothing appears: confirm the endpoint ends in /api/public/v1/logs/otlp, that the key belongs to this project, and that a 401 or 402 is not being logged by the exporter.
Back to SDK overview.