From 0128cdb9543068f5da2df818ba35542c84920296 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 26 May 2026 15:26:50 +0000 Subject: [PATCH 1/2] docs: add Helm install path to enterprise Analytics guide Restructures enterprise/analytics.mdx into a single page with tabbed 'Replicated (VM Install)' vs 'Helm (Kubernetes Install)' sections for the two steps where the install paths actually diverge: - 'Enable Analytics' (KOTS checkbox vs site-values.yaml laminar: block plus env.LMNR_BASE_URL/LMNR_FORCE_HTTP keys, with options for pre-existing TLS secrets and cert-manager) - 'Deploy' (Admin Console redeploy vs helm upgrade + kubectl rollout) - 'Wire the API key into the install' (KOTS field + Save config vs env.LMNR_PROJECT_API_KEY + helm upgrade) The Laminar-side workflow (Keycloak login, create project, create ingest-only key, view traces) is shared between paths and stays outside the tabs. Helm-tab content is distilled from the OpenHands-Cloud chart values, the All-Hands-AI/deploy production reference, and the upstream lmnr-helm CONFIGURATION.md, with a Tip pointing AWS users at appServer.loadBalancer. Also: - enterprise/k8s-install/index.mdx: add Analytics card to Guides - enterprise/quick-start.mdx: add analytics.app. row to the 'Reasons for Requirements' table so the SAN is included at install time and customers don't have to re-issue certs later (complements PR #523 which added the row to the DNS table and preflight script) Co-authored-by: openhands --- enterprise/analytics.mdx | 246 ++++++++++++++++++++++++++++--- enterprise/k8s-install/index.mdx | 4 + enterprise/quick-start.mdx | 1 + 3 files changed, 228 insertions(+), 23 deletions(-) diff --git a/enterprise/analytics.mdx b/enterprise/analytics.mdx index e1b84608..66dc3566 100644 --- a/enterprise/analytics.mdx +++ b/enterprise/analytics.mdx @@ -11,6 +11,16 @@ You'll opt into Analytics and configure conversations to automatically send trac This guide is for users who want to explore analytics on their OpenHands Enterprise conversations. +It covers both supported install paths: + +- **Replicated (VM install)** -- if you followed the [Quick Start](/enterprise/quick-start) and + manage OpenHands through the Replicated Admin Console. +- **Helm (Kubernetes install)** -- if you deployed the `openhands` Helm chart into your own + Kubernetes cluster (see [Kubernetes Installation](/enterprise/k8s-install/index)). + +Most of the workflow (creating a Laminar project, creating an API key, viewing traces) is +the same on both paths. The two install-specific steps are tabbed below. + ### Why Laminar? [Laminar](https://laminar.sh/) is an open source observability platform for AI agents like OpenHands. @@ -29,31 +39,182 @@ For more information on evaluating skills, see [Evaluating Agent Skills](https:/ ### Prerequisites -Before you begin, make sure you completed the [Quick Start guide](/enterprise/quick-start). +Before you begin, make sure you have completed the install for your path: -## Enable Analytics +- **Replicated**: [Quick Start guide](/enterprise/quick-start) +- **Helm**: [Kubernetes Installation guide](/enterprise/k8s-install/index) -You should see an **Analytics Configuration** section on the application configuration page. +You will also need: -Check the **Enable Analytics** box to have the installer set up and configure Laminar for analytics. +- DNS records (and a TLS certificate covering the SAN) for `analytics.app.`. + On Replicated, this is included in the [Quick Start DNS table](/enterprise/quick-start#dns-and-tls-setup). + On Helm, you choose the hostname yourself in `site-values.yaml`. +- An ingress controller already running in the cluster (Replicated installs ship Traefik; Helm + installs typically use Traefik as well -- see the [chart README](https://github.com/OpenHands/OpenHands-Cloud/blob/main/charts/openhands/README.md)). -![Configure Analytics](./images/laminar-configure-analytics.png) +## Enable Analytics + + + + On the application configuration page in the Admin Console, find the + **Analytics Configuration** section. + + Check the **Enable Analytics** box. The installer will set up Laminar and template the + required hostnames and Keycloak wiring for you. + + ![Configure Analytics](./images/laminar-configure-analytics.png) + + + + Add the `laminar` block to your `site-values.yaml` and set the top-level `env.LMNR_*` + keys so the application sends traces to Laminar. + + Replace `example.com` with your base domain, and replace `traefik` with the name of + your ingress controller's IngressClass. + + ```yaml + # site-values.yaml + + env: + # Where the application sends traces. Must match laminar.appServer.ingress.hostname below. + LMNR_BASE_URL: "https://laminar-api.app.example.com" + # Use OTLP/HTTP (rather than gRPC) for trace ingestion. + LMNR_FORCE_HTTP: "true" + # LMNR_PROJECT_API_KEY is set in a later step, after you create an ingest-only key + # in the Laminar UI. + + laminar: + enabled: true + global: + # Sets provider-specific defaults; not auto-detected. Use "gcp" or "aws". + cloudProvider: "gcp" + frontend: + ingress: + enabled: true + hostname: "analytics.app.example.com" # REQUIRED + className: "traefik" # your ingress controller's IngressClass + externalDns: + enabled: false # true if external-dns manages your DNS + tls: + enabled: true + clusterIssuer: "" # see TLS options below + secretName: "laminar-frontend-tls" + env: + # Must match laminar.frontend.ingress.hostname above. + nextauthUrl: "https://analytics.app.example.com" + nextPublicUrl: "https://analytics.app.example.com" + extraEnv: + # Wires the Laminar UI to your existing Keycloak realm so users can sign in + # with the same identity provider they use for OpenHands. + - name: AUTH_KEYCLOAK_ID + valueFrom: + secretKeyRef: + name: keycloak-realm + key: client-id + - name: AUTH_KEYCLOAK_SECRET + valueFrom: + secretKeyRef: + name: keycloak-realm + key: client-secret + - name: AUTH_KEYCLOAK_ISSUER + value: "https://auth.app.example.com/realms/allhands" + appServer: + ingress: + hostname: "laminar-api.app.example.com" # REQUIRED -- matches LMNR_BASE_URL + className: "traefik" + externalDns: + enabled: false + tls: + enabled: true + clusterIssuer: "" + secretName: "laminar-app-server-tls" + ``` + + + **AWS clusters** can swap `laminar.appServer.ingress` for an L4 Network Load Balancer: + + ```yaml + laminar: + appServer: + loadBalancer: + enabled: true + hostname: "laminar-api.app.example.com" + ``` + + This is the upstream chart's recommended pattern on AWS. See the [lmnr-helm + configuration guide](https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md) + for details. + + + ### TLS options + + The `laminar.frontend.ingress.tls` and `laminar.appServer.ingress.tls` blocks above + work with either pattern: + + - **Pre-existing TLS secret** (recommended if your DNS and certs are managed externally): + leave `clusterIssuer: ""` and create the secrets yourself. Concatenate the certificate + and CA bundle into a full chain first: + + ```bash + cat cert.pem ca-bundle.pem > fullchain.pem + + kubectl create secret tls laminar-frontend-tls \ + -n openhands --cert=fullchain.pem --key=private-key.pem + + kubectl create secret tls laminar-app-server-tls \ + -n openhands --cert=fullchain.pem --key=private-key.pem + ``` + + - **cert-manager with Let's Encrypt**: set `clusterIssuer: "letsencrypt"` (or the name + of any other `ClusterIssuer` in your cluster). The hostnames must be publicly + DNS-resolvable so Let's Encrypt can complete the HTTP-01 challenge. + + See the [lmnr-helm configuration guide](https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md) + for additional DNS and TLS variants, including manual DNS and pre-existing ACM certificates. + + ### Apply the change + + ```bash + helm upgrade --install openhands \ + --namespace openhands \ + oci://ghcr.io/all-hands-ai/helm-charts/openhands \ + -f site-values.yaml + ``` + + ## Deploy -OpenHands will begin deploying. You can expect the deployment status to transition from -**Missing** to **Unavailable** to **Ready**. This typically takes 10-15 minutes. + + + OpenHands will begin deploying. You can expect the deployment status to transition from + **Missing** to **Unavailable** to **Ready**. This typically takes 10-15 minutes. + + ![Deployment in progress](./images/laminar-deploy-in-progress.png) + + Click **Details** next to the deployment status to monitor individual resources. Resources + shown in orange are still deploying -- wait until all resources are ready. -![Deployment in progress](./images/laminar-deploy-in-progress.png) + ![Deployment status details](./images/laminar-deployment-status-details.png) + -Click **Details** next to the deployment status to monitor individual resources. Resources -shown in orange are still deploying -- wait until all resources are ready. + + Watch the Laminar pods come up in your cluster: -![Deployment status details](./images/laminar-deployment-status-details.png) + ```bash + kubectl get pods -n openhands -l app.kubernetes.io/instance=openhands -w + ``` + + You should see pods for `laminar-frontend`, `laminar-app-server`, `laminar-clickhouse`, + `laminar-postgres`, `laminar-rabbitmq`, `laminar-redis`, and the Quickwit components. + Wait until all pods are `Running` and ready before continuing. + + ## Access Laminar UI -Once the deployment status shows **Ready**, navigate to `https://analytics.app.`. +Once the deployment is **Ready**, navigate to `https://analytics.app.` +(or the `laminar.frontend.ingress.hostname` you configured for the Helm install). Click the **Continue with Keycloak** button: @@ -67,29 +228,68 @@ Once a project has been created, Laminar is ready to listen for traces. ![Laminar Listen Traces](./images/laminar-listen-traces.png) -## Create an ingest only API Key +## Create an ingest-only API Key -Important: Always use ingest API keys when deploying. + + Always use **ingest-only** API keys for the OpenHands integration. Ingest-only keys can + only write traces -- they cannot be used to read data, so they are safe to embed in + configuration. + -Create a key with the right permissions. Ingest only keys are recommended as they only have write access to write traces. They cannot be used to read data. +Create a key with ingest-only permissions: ![Configure Laminar Ingest Only Key](./images/laminar-ingest-only-key.png) -## Set Laminar Project API Key to enable automatic conversation traces +## Wire the API key into the install + + + + Paste the ingest-only key into the **Laminar Project API Key** field in the Admin Console + configuration: + + ![Configure Laminar Project API Key](./images/laminar-configure-key.png) + + Click **Save config**, then deploy the change: + + ![Laminar Deploy Again](./images/laminar-deploy-again.png) + + Wait for the deployment to complete. + + + + Set the ingest-only key in your `site-values.yaml` under the top-level `env` block: -Set the ingest only key as the Laminar Project API Key in the Admin Console configuration: + ```yaml + # site-values.yaml -![Configure Laminar Project API Key](./images/laminar-configure-key.png) + env: + LMNR_BASE_URL: "https://laminar-api.app.example.com" + LMNR_FORCE_HTTP: "true" + LMNR_PROJECT_API_KEY: "" + ``` -Click **Save config**. + + For a production install, store the key in a Kubernetes Secret and reference it from + your values file or via `--set-string` at install time, rather than committing it to + source control. + -## Deploy Updated Configuration + Apply the change: -Deploy the config change after setting the Laminar Project API Key in the Admin Console. + ```bash + helm upgrade --install openhands \ + --namespace openhands \ + oci://ghcr.io/all-hands-ai/helm-charts/openhands \ + -f site-values.yaml + ``` -![Laminar Deploy Again](./images/laminar-deploy-again.png) + Wait for the rollout to complete: -Wait for the deployment to complete. + ```bash + kubectl rollout status deploy/openhands -n openhands + ``` + + ## Start a conversation diff --git a/enterprise/k8s-install/index.mdx b/enterprise/k8s-install/index.mdx index 50b7fdd3..1cc08ef2 100644 --- a/enterprise/k8s-install/index.mdx +++ b/enterprise/k8s-install/index.mdx @@ -58,6 +58,10 @@ OpenHands Enterprise consists of several components deployed as Kubernetes workl Configure memory, CPU, and storage for optimal performance. + + Enable Laminar for LLM observability and tracing. See the **Helm (Kubernetes Install)** tab on each step. + + ## Request Access Kubernetes-based installation is currently available to select customers on request. diff --git a/enterprise/quick-start.mdx b/enterprise/quick-start.mdx index cf686e4b..bf1bcf1b 100644 --- a/enterprise/quick-start.mdx +++ b/enterprise/quick-start.mdx @@ -235,6 +235,7 @@ If any check fails, stop and resolve before continuing: | `30000/TCP` inbound | Replicated/KOTS Admin Console for install and configuration | | `80/TCP` inbound | HTTP entrypoint used for ingress/redirect behavior | | `*.runtime.` DNS + cert SAN | Runtime sandboxes are addressed by dynamic runtime-specific hostnames | +| `analytics.app.` DNS + cert SAN | Hosts the Laminar UI when [Analytics](/enterprise/analytics) is enabled. Include the SAN at install time so you do not need to re-issue the certificate later. | | `replicated.app`, `proxy.replicated.com` | Replicated control-plane/license/install paths | | `images.r9...`, `charts.r9...`, `updates.r9...`, `install.r9...` | Vendor distribution image/chart/update/install endpoints | | `traefik.github.io` | Embedded cluster ingress chart repository | From b86bf7f487e735b8c7fc6f602bef5db1ec0ec113 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 26 May 2026 16:28:32 +0000 Subject: [PATCH 2/2] docs(analytics): make in-cluster ingest the default Helm path The previous draft mirrored the All-Hands SaaS production values, which expose an external Laminar app-server ingress at laminar-api.app... . That's necessary for SaaS because the runtime fleet can span clusters, but it's over-specified for typical single-cluster Helm customers, who have no current multi-cluster installs. Update the Helm tab so the primary path uses the in-cluster Laminar Service for trace ingestion -- LMNR_BASE_URL=http://laminar-app-server-service, LMNR_FORCE_HTTP=true, LMNR_HTTP_PORT=8000 -- matching what the Replicated install does. This means customers only need ONE new DNS record and TLS SAN (the user-facing analytics.app. hostname) instead of two. Move the laminar.appServer.ingress / loadBalancer configuration into an labelled 'Advanced: expose the app-server externally (multi-cluster only)' so it's discoverable but doesn't pollute the default path. Also update the 'Wire the API key' Helm code block to reflect the new LMNR_BASE_URL value. Co-authored-by: openhands --- enterprise/analytics.mdx | 102 +++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/enterprise/analytics.mdx b/enterprise/analytics.mdx index 66dc3566..c2653147 100644 --- a/enterprise/analytics.mdx +++ b/enterprise/analytics.mdx @@ -69,6 +69,11 @@ You will also need: Add the `laminar` block to your `site-values.yaml` and set the top-level `env.LMNR_*` keys so the application sends traces to Laminar. + This guide assumes a single-cluster install where OpenHands runtimes and Laminar run + in the same Kubernetes cluster. Traces are sent to the in-cluster Laminar Service, + so you only need **one** new DNS record and TLS SAN -- the user-facing Laminar UI + hostname (e.g. `analytics.app.`). + Replace `example.com` with your base domain, and replace `traefik` with the name of your ingress controller's IngressClass. @@ -76,10 +81,11 @@ You will also need: # site-values.yaml env: - # Where the application sends traces. Must match laminar.appServer.ingress.hostname below. - LMNR_BASE_URL: "https://laminar-api.app.example.com" - # Use OTLP/HTTP (rather than gRPC) for trace ingestion. + # The application sends traces to the in-cluster Laminar Service. No external + # hostname is required for ingestion. + LMNR_BASE_URL: "http://laminar-app-server-service" LMNR_FORCE_HTTP: "true" + LMNR_HTTP_PORT: "8000" # LMNR_PROJECT_API_KEY is set in a later step, after you create an ingest-only key # in the Laminar UI. @@ -91,7 +97,7 @@ You will also need: frontend: ingress: enabled: true - hostname: "analytics.app.example.com" # REQUIRED + hostname: "analytics.app.example.com" # REQUIRED -- the Laminar UI hostname className: "traefik" # your ingress controller's IngressClass externalDns: enabled: false # true if external-dns manages your DNS @@ -118,41 +124,14 @@ You will also need: key: client-secret - name: AUTH_KEYCLOAK_ISSUER value: "https://auth.app.example.com/realms/allhands" - appServer: - ingress: - hostname: "laminar-api.app.example.com" # REQUIRED -- matches LMNR_BASE_URL - className: "traefik" - externalDns: - enabled: false - tls: - enabled: true - clusterIssuer: "" - secretName: "laminar-app-server-tls" ``` - - **AWS clusters** can swap `laminar.appServer.ingress` for an L4 Network Load Balancer: + ### TLS options for the frontend ingress - ```yaml - laminar: - appServer: - loadBalancer: - enabled: true - hostname: "laminar-api.app.example.com" - ``` - - This is the upstream chart's recommended pattern on AWS. See the [lmnr-helm - configuration guide](https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md) - for details. - - - ### TLS options - - The `laminar.frontend.ingress.tls` and `laminar.appServer.ingress.tls` blocks above - work with either pattern: + The `laminar.frontend.ingress.tls` block above works with either pattern: - **Pre-existing TLS secret** (recommended if your DNS and certs are managed externally): - leave `clusterIssuer: ""` and create the secrets yourself. Concatenate the certificate + leave `clusterIssuer: ""` and create the secret yourself. Concatenate the certificate and CA bundle into a full chain first: ```bash @@ -160,18 +139,12 @@ You will also need: kubectl create secret tls laminar-frontend-tls \ -n openhands --cert=fullchain.pem --key=private-key.pem - - kubectl create secret tls laminar-app-server-tls \ - -n openhands --cert=fullchain.pem --key=private-key.pem ``` - **cert-manager with Let's Encrypt**: set `clusterIssuer: "letsencrypt"` (or the name - of any other `ClusterIssuer` in your cluster). The hostnames must be publicly + of any other `ClusterIssuer` in your cluster). The hostname must be publicly DNS-resolvable so Let's Encrypt can complete the HTTP-01 challenge. - See the [lmnr-helm configuration guide](https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md) - for additional DNS and TLS variants, including manual DNS and pre-existing ACM certificates. - ### Apply the change ```bash @@ -180,6 +153,50 @@ You will also need: oci://ghcr.io/all-hands-ai/helm-charts/openhands \ -f site-values.yaml ``` + + + Skip this section unless OpenHands runtimes will be sending traces from **outside** + the cluster where Laminar runs. Almost all installs are single-cluster and should + use the in-cluster ingest configuration above. + + For multi-cluster setups, expose the Laminar app-server through your ingress (or, on + AWS, an L4 Network Load Balancer) and point `LMNR_BASE_URL` at the external hostname. + This adds a second DNS record and TLS SAN (e.g. `laminar-api.app.`). + + ```yaml + # site-values.yaml + + env: + LMNR_BASE_URL: "https://laminar-api.app.example.com" + LMNR_FORCE_HTTP: "true" + # Omit LMNR_HTTP_PORT -- the port comes from the URL. + + laminar: + appServer: + ingress: + hostname: "laminar-api.app.example.com" + className: "traefik" + externalDns: + enabled: false + tls: + enabled: true + clusterIssuer: "" + secretName: "laminar-app-server-tls" + ``` + + **AWS clusters** can swap `laminar.appServer.ingress` for an L4 Network Load Balancer: + + ```yaml + laminar: + appServer: + loadBalancer: + enabled: true + hostname: "laminar-api.app.example.com" + ``` + + See the [lmnr-helm configuration guide](https://github.com/lmnr-ai/lmnr-helm/blob/main/CONFIGURATION.md) + for additional DNS and TLS variants, including manual DNS and pre-existing ACM certificates. + @@ -263,8 +280,9 @@ Create a key with ingest-only permissions: # site-values.yaml env: - LMNR_BASE_URL: "https://laminar-api.app.example.com" + LMNR_BASE_URL: "http://laminar-app-server-service" LMNR_FORCE_HTTP: "true" + LMNR_HTTP_PORT: "8000" LMNR_PROJECT_API_KEY: "" ```