ownCloud Infinite Scale (oCIS) Tracing and RUM with OpenObserve

  • #ocis
  • #opentelemetry
  • #openobserve
  • #rum
  • #observability

OpenObserve trace waterfall view with an oCIS RUM session replay panel open alongside it

ownCloud Infinite Scale is a modern replacement for ownCloud, the legacy PHP file server.

Welcome to oCIS, the modern file-sync and share platform, which is based on our knowledge and experience with the PHP based ownCloud server.

It is a complete redesign focused on micro services, queueing, and a complete code-rewrite in Go. Simply put, it’s a modern alternative to PHP file servers that has quite a bit of niceties such as tracing.

oCIS supports k8s as well, see the ocis-charts repo. For this example we will be using Docker.

Moving Parts:

ToolPurpose
oCIS Dockermodern file system written in Go
nginxproxy to support JS injection needed for RUM
otel-collectorgrabs gRPC otel events from oCIS, exports them to OpenObserve collector endpoints
OpenObservemodern Rust observability stack in a single binary

All of this is done in Docker.

OpenObserve: what is this?

OpenObserve was a semi-recent GitHub project find of mine. It is a really cool project that aims to offer up to 50x cost savings compared to Elasticsearch, Datadog, and other similar observability tooling. It is built in Rust, centered around Apache DataFusion, OTEL, Apache Parquet, and object storage.

Links:

It has a moderately sized community on GitHub with over 20K stars as of the time of this writing. They also have a very competitively priced hosted option as well. For this walk-through we are using the community edition and are self-hosting it. For the self-hosted pricing there is no cost associated with it except for the storage and compute which is an insane deal in 2026.

Setup

My Docker Compose is as following:

services:
  openobserve:
    image: openobserve/openobserve:latest
    platform: linux/arm64
    container_name: openobserve
    restart: unless-stopped
    environment:
      ZO_ROOT_USER_EMAIL: ${ZO_ROOT_USER_EMAIL}
      ZO_ROOT_USER_PASSWORD: ${ZO_ROOT_USER_PASSWORD}
      ZO_CORS_ALLOWED_ORIGINS: https://${OCIS_HOSTNAME}:${NGINX_HTTPS_PORT}
    ports:
      - "5080:5080"
    volumes:
      - openobserve_data:/data

  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    platform: linux/arm64
    container_name: otel-collector
    restart: unless-stopped
    depends_on:
      - openobserve
    volumes:
      - ./otel-collector/config.yaml:/etc/otelcol-contrib/config.yaml:ro
    command: ["--config=/etc/otelcol-contrib/config.yaml"]

  ocis:
    image: owncloud/ocis:latest
    platform: linux/arm64
    container_name: ocis
    restart: unless-stopped
    depends_on:
      - otel-collector
    entrypoint:
      - /bin/sh
      - -c
      - "ocis init --insecure yes 2>/dev/null; exec ocis server"
    environment:
      OCIS_URL: https://${OCIS_HOSTNAME}:${NGINX_HTTPS_PORT}
      PROXY_HTTP_ADDR: 0.0.0.0:9200
      PROXY_TLS: "false"
      OCIS_INSECURE: "true"
      PROXY_ENABLE_BASIC_AUTH: "true"
      ADMIN_PASSWORD: ${OCIS_ADMIN_PASSWORD}
      OCIS_TRACING_ENABLED: "true"
      OCIS_TRACING_TYPE: otlp
      OCIS_TRACING_ENDPOINT: otel-collector:4317
      OCIS_LOG_LEVEL: info
    volumes:
      - ocis_config:/etc/ocis
      - ocis_data:/var/lib/ocis

  nginx:
    image: nginx:alpine
    container_name: ocis-nginx
    restart: unless-stopped
    depends_on:
      - ocis
    ports:
      - "${NGINX_HTTPS_PORT}:${NGINX_HTTPS_PORT}"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certs:/etc/nginx/certs:ro
      - ./nginx/rum-assets:/etc/nginx/rum-assets:ro
    networks:
      default:
        aliases:
          - ${OCIS_HOSTNAME}

volumes:
  openobserve_data:
  ocis_config:
  ocis_data:

OTEL Collector Config

The reason for the OTEL collector to begin with is that oCIS just supports outputting OTEL traces to gRPC and has no way of adding an auth header which OpenObserve requires. In their official docs they want you to use Jaeger which is another cloud native tracing framework built on OTEL. In this case we just use the OTEL collector to forward to OpenObserve. Here is the config sample:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}

exporters:
  otlphttp/openobserve:
    endpoint: http://openobserve:5080/api/default
    headers:
      Authorization: "Basic cGFzc3dvcmQxMjMK"

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/openobserve]

oCIS Config

This block of config was already pasted above in the Docker config but I’ll paste it again for verbosity. This website is pretty authoritative regarding the config options and is well maintained upon releases. The main points below are just the tracking being enabled, pointed at the svc dns of the otel-collector in the Docker stack, and that http(s) has been disabled and to trust the proxy. I found that info log setting was verbose enough for me.

environment:
      OCIS_URL: https://${OCIS_HOSTNAME}:${NGINX_HTTPS_PORT}
      PROXY_HTTP_ADDR: 0.0.0.0:9200
      PROXY_TLS: "false"
      OCIS_INSECURE: "true"
      PROXY_ENABLE_BASIC_AUTH: "true"
      ADMIN_PASSWORD: ${OCIS_ADMIN_PASSWORD}
      OCIS_TRACING_ENABLED: "true"
      OCIS_TRACING_TYPE: otlp
      OCIS_TRACING_ENDPOINT: otel-collector:4317
      OCIS_LOG_LEVEL: info

nginx Config

Okay this is where it gets a little schizo. Basically the RUM (real user monitoring) feature in OpenObserve requires JS to be injected into the served content. Since oCIS is a compiled Go binary, I did not feel like re-compiling it to include the needed JS files, so I injected it at the proxy level, in this case nginx. There are some CSP items in here as well, those need to be adjusted accordingly per domain.

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 8443 ssl;
    server_name ocis.local;

    ssl_certificate     /etc/nginx/certs/ocis.crt;
    ssl_certificate_key /etc/nginx/certs/ocis.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 0;

    # Self-hosted RUM SDK bundles, served same-origin so they satisfy ocis's script-src 'self' CSP.
    location /rum-assets/ {
        alias /etc/nginx/rum-assets/;
        add_header Cache-Control "no-cache";
    }

    # Proxy RUM ingestion same-origin/HTTPS: the SDK would otherwise call OpenObserve directly over
    # plain http://localhost:5080, which Safari (unlike Chrome) blocks as mixed content on an https page.
    location /rum/ {
        proxy_pass http://openobserve:5080/rum/;
        proxy_set_header Host $http_host;
    }

    location / {
        proxy_pass http://ocis:9200;
        proxy_http_version 1.1;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # sub_filter can't rewrite compressed bodies, so ask ocis for uncompressed HTML.
        proxy_set_header Accept-Encoding "";

        # ocis's CSP has no worker-src, so Worker creation falls back to child-src 'self' (no blob:).
        # The RUM SDK's session-replay recorder spins up a compression worker from a blob: URL, which
        # that fallback blocks; add worker-src explicitly so it's the only change to ocis's policy.
        proxy_hide_header Content-Security-Policy;
        add_header Content-Security-Policy "child-src 'self'; connect-src 'self' blob: https://raw.githubusercontent.com/owncloud/awesome-ocis/; default-src 'none'; font-src 'self'; frame-ancestors 'self'; frame-src 'self' blob: https://embed.diagrams.net/; img-src 'self' data: blob: https://raw.githubusercontent.com/owncloud/awesome-ocis/; manifest-src 'self'; media-src 'self'; object-src 'self' blob:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:" always;

        sub_filter_once on;
        sub_filter_types text/html;
        sub_filter '</head>' '<script src="/rum-assets/openobserve-rum.js"></script><script src="/rum-assets/openobserve-logs.js"></script><script>(function(){var options={clientToken:"rumsorBELb4WJnw2jts",applicationId:"ocis-web",site:"ocis.local:8443",service:"ocis-web",env:"development",version:"1.0.0",organizationIdentifier:"default",insecureHTTP:false,apiVersion:"v1"};OO_RUM.init({applicationId:options.applicationId,clientToken:options.clientToken,site:options.site,organizationIdentifier:options.organizationIdentifier,service:options.service,env:options.env,version:options.version,trackResources:true,trackLongTasks:true,trackUserInteractions:true,apiVersion:options.apiVersion,insecureHTTP:options.insecureHTTP,defaultPrivacyLevel:"allow",allowedTracingUrls:[{match:"https://ocis.local:8443",propagatorTypes:["tracecontext"]}],sessionSampleRate:100,sessionReplaySampleRate:100});OO_LOGS.init({clientToken:options.clientToken,site:options.site,organizationIdentifier:options.organizationIdentifier,service:options.service,env:options.env,version:options.version,forwardErrorsToLogs:true,insecureHTTP:options.insecureHTTP,apiVersion:options.apiVersion});OO_RUM.startSessionReplayRecording();})();</script></head>';
    }
}

You do need a client secret (clientToken) that you can find in the OpenObserve panel in RUM icon on the left bar when you first click into it. This janky snippet of bash did the rendering for me to place it into the nginx config and into the directory it knows how to serve from.

echo "==> Fetching OpenObserve RUM SDK bundles (if missing)"
mkdir -p nginx/rum-assets/chunks
if [[ ! -f nginx/rum-assets/openobserve-rum.js ]]; then
  curl -sL "https://unpkg.com/@openobserve/browser-rum@${RUM_SDK_VERSION}/bundle/openobserve-rum.js" \
    -o nginx/rum-assets/openobserve-rum.js
fi
if [[ ! -f nginx/rum-assets/openobserve-logs.js ]]; then
  curl -sL "https://unpkg.com/@openobserve/browser-logs@${RUM_SDK_VERSION}/bundle/openobserve-logs.js" \
    -o nginx/rum-assets/openobserve-logs.js
fi
# The RUM SDK lazy-loads session-replay recorder chunks from bundle/chunks/ at runtime.
RUM_CHUNKS="profiler-da8b374510b8838a8938-openobserve-rum.js recorder-f4f0f1d89a0eadca1e0f-openobserve-rum.js"
for f in $RUM_CHUNKS; do
  if [[ ! -f "nginx/rum-assets/chunks/$f" ]]; then
    curl -sL "https://unpkg.com/@openobserve/browser-rum@${RUM_SDK_VERSION}/bundle/chunks/$f" \
      -o "nginx/rum-assets/chunks/$f"
  fi
done

echo "==> Rendering nginx config"
sed \
  -e "s/__OCIS_HOSTNAME__/${OCIS_HOSTNAME}/g" \
  -e "s/__NGINX_HTTPS_PORT__/${NGINX_HTTPS_PORT}/g" \
  -e "s/__OCIS_RUM_CLIENT_TOKEN__/${OCIS_RUM_CLIENT_TOKEN}/g" \
  -e "s|__OCIS_RUM_SITE__|${OCIS_RUM_SITE}|g" \
  -e "s/__OCIS_RUM_ORG__/${ZO_ORG}/g" \
  -e "s/__OCIS_RUM_INSECURE__/${OCIS_RUM_INSECURE}/g" \
  -e "s|__OCIS_URL__|https://${OCIS_HOSTNAME}:${NGINX_HTTPS_PORT}|g" \
  nginx/conf.d/ocis.conf.tmpl > nginx/conf.d/ocis.conf

Observing Traces within OpenObserve

The UI feels quite mature within OpenObserve for viewing traces.

OpenObserve traces overview screen

I have some error traces right away since I did not know how CSPs worked, apparently lol.

Here is an example of clicking into a trace during a login check:

Trace waterfall for an oCIS login check, part 1

Trace waterfall for an oCIS login check, part 2

Trace waterfall for an oCIS login check, part 3

They have more nifty views for visualization. Filtering for errors is very easy to do:

Filtering traces for errors in OpenObserve

Their service catalog option is nice as well, allows you to display all the various service providers and consumers:

OpenObserve service catalog for the oCIS stack

Drilling into a service:

Drilling into a single service in the OpenObserve service catalog

Viewing RUM Sessions within OpenObserve

This is where it gets really valuable. Most RUM tools are very costly and do not scale well price-wise when your user base grows, often forcing you to configure your sampling percentage to be very tiny. A solution like this could be inclusive to the requirement of capturing all traces/sessions as needed and then configuring lifecycle policies to retire sessions/traces that do not meet certain criteria.

Here is a short demo of me viewing a user (me lol) logging into oCIS and poking around:

The UI is quite intuitive, and shows all my actions within the oCIS UI, and any traces that correlate to clicks/etc. Super nice for triaging a user complaint and seeing the exact flow.

RUM session replay showing traces correlated to a button click

You can see in the above screenshot that when I clicked “create a space” (oCIS version of “drives”) that it automatically correlated all of the traces with that button click and all the internal providers responding to the various micro service requests.

Here you can see all of the errors if any are associated during a RUM capture.

Errors associated with an RUM session capture, part 1

Errors associated with an RUM session capture, part 2

Here is a funny example of OpenObserving thinking that I rage clicked a button…

RUM rage-click detection flagging a button

Adding additional attributes to traces/RUM

I got tired of seeing “Unknown User” so I did a bit of diving into how I could get that field to populate correctly.

By adding this snippet of JS into the nginx config alongside the RUM injection I could obtain the user email by interacting with the Graph API.

var identified = false;
var identifyPoll = setInterval(function() {
  if (identified) return;
  var key = Object.keys(localStorage).find(k => k.indexOf("oc_oAuth.user") === 0);
  if (!key) return;                          // not logged in yet — keep polling
  var stored = JSON.parse(localStorage.getItem(key));
  if (!stored || !stored.access_token) return;
  fetch("/graph/v1.0/me", { headers: { Authorization: "Bearer " + stored.access_token } })
    .then(r => r.ok ? r.json() : null)
    .then(me => {
      if (!me) return;
      identified = true;
      clearInterval(identifyPoll);
      OO_RUM.setUser({ id: me.id, name: me.displayName, email: me.mail });
    });
}, 2000);

This polled until the login identity is established, and then stored the email. This is probably not a great production-grade idea, simply because you are polling and generating errors, and anything holding the email in code is not a great idea. But for this demo, it works.

nginx config diff adding the user-identification script

After adding that, the email appears during actions.

RUM session showing the identified user's email on actions

Errors populate correctly with the user’s email. If your service was geolocation-specific, you could add that metadata as well.

RUM / Tracing Costs in OpenObserve

As the title states, this can be done for literally the cost of an S3 bucket. Combined with a service such as Backblaze B2, this would be essentially free. The compute required to host OpenObserve is very small as it was written in Rust and is very performant. See Bring Your Own Bucket for more.

Storing it on a burstable t4g.large AWS instance would probably suffice for thousands of users, and even more if sampling was enabled. Here is the data from my small test instance:

Data streamWhat it holdsRecord countUncompressedCompressed on diskIndex sizeCompression ratio
default (traces)Backend spans (proxy/graph/gateway/settings/storage)55,804 spans (6,823 distinct traces)38.6 MB2.46 MB0.83 MB~16:1
_rumdataRUM events (views, resources, actions, errors)8,028 events (28 sessions)17.3 MB0.88 MB~20:1
_sessionreplayCompressed session-replay segments208 segments (14 sessions, 1,813 inner events)34.9 MB2.62 MB~13:1
_rumlogErrors forwarded from RUM → Logs56 log lines0.12 MB0.075 MB0.03 MB~1.5:1
trace_list_indexInternal trace-ID index (metadata)55,7116.7 MB0.29 MB0.64 MB
distinct_values_traces_defaultInternal field-cardinality index (metadata)19,1902.3 MB0.07 MB0.23 MB

As you can see the compression is amazing.

Versions used in this example

ComponentImage / packageVersion actually running
ocisowncloud/ocis:latestInfinite Scale 8.0.1 Community (IDP/Konnect 8.0.1)
ocis web frontend(bundled in ocis image)ownCloud Web UI 12.3.2
OpenObserveopenobserve/openobserve:latestv0.91.1
nginxnginx:alpinenginx/1.31.3
OTel Collectorotel/opentelemetry-collector-contrib:latestv0.157.0
RUM SDK (browser)@openobserve/browser-rum0.3.4
RUM logs SDK (browser)@openobserve/browser-logs0.3.4
Docker Engine (host)29.7.1 (client), 29.5.2 (server, via colima)
Docker Compose (host)5.4.0

Thanks for the read!