Kubesense

External Service Logs & Metrics

Push logs and Prometheus metrics into KubeSense from services running outside the cluster, using the aggregator and metrics-scraper load balancers.

Overview

Managed SaaS platforms, third-party APIs, and applications running on VMs sit outside your Kubernetes cluster, so no KubeSense agent can be installed next to them. These services instead push their telemetry to an endpoint you provide, or expose it for you to pull and forward.

KubeSense exposes two ingestion surfaces for this. Both are ordinary Kubernetes Services, so you publish them by turning them into internal load balancers (an NLB on AWS):

DataComponentUnder the hoodDefault port
LogsaggregatorVector30055 OTLP HTTP · 30054 file log
Metrics (Prometheus format)metrics-scrapervmagent30060

Which log path should I use?: Prefer OTLP when the sending side can speak OpenTelemetry. It carries structured resource attributes, so logs are correctly attributed to a cluster, service, and environment. Use the file log path when the source can only POST plain JSON — it is simpler, but the field mapping is fixed and flatter.


Part 1 — Expose the endpoints

Aggregator (logs)

Set the aggregator service to LoadBalancer and add your cloud's annotations:

aggregator:
  service:
    type: LoadBalancer
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
      service.beta.kubernetes.io/aws-load-balancer-internal: "true"
      service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
      service.beta.kubernetes.io/aws-load-balancer-subnets: subnet-aaa, subnet-bbb, subnet-ccc
      service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
      service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled="true"
  sources:
    otel:
      enabled: true       # OTLP logs  — off by default
    file_logs:
      enabled: true       # JSON HTTP  — off by default

The listener ports stay in the 30000 range: The aggregator Service defines port from the global.nodePort.* values regardless of service type. Switching to LoadBalancer does not move ingestion onto 80/443 — the NLB listens on 30052, 30054, 30055, 30056, 30059, and 8686. Send traffic to http://<nlb-dns>:30055, not http://<nlb-dns>.If you need conventional ports, use aggregator.additionalservice, which publishes a second Service with freely chosen port numbers.

Metrics-scraper (metrics)

The metrics-scraper Service is disabled by default and must be switched on as well as exposed:

metrics-scraper:
  service:
    enabled: true         # false by default
    type: LoadBalancer
    servicePort: 30060
    annotations:
      service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
      service.beta.kubernetes.io/aws-load-balancer-internal: "true"
      service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
      service.beta.kubernetes.io/aws-load-balancer-subnets: subnet-aaa, subnet-bbb, subnet-ccc

Apply with:

helm upgrade kubesense kubesense/kubesense \
  -f values.yaml --namespace kubesense

Then collect the two hostnames:

kubectl -n kubesense get svc kubesense-aggregator kubesense-metrics-scraper \
  -o custom-columns=NAME:.metadata.name,LB:.status.loadBalancer.ingress[0].hostname

Keep these load balancers private: The aggregator Service also publishes the Vector admin API on port 8686, which is unauthenticated. Always set aws-load-balancer-internal: "true", and restrict the source range:aggregator: service: loadBalancerSourceRanges: - 10.0.0.0/8 # your VPC, a peered VPC, or the vendor's egress CIDRsNeither ingestion endpoint performs authentication. Reach them over private connectivity — VPC peering, PrivateLink, or Transit Gateway — rather than the public internet. If a vendor can only reach a public address, terminate it on a proxy that enforces authentication and forwards to the NLB.


Part 2 — Pushing logs

Enable aggregator.sources.otel and send OTLP to the NLB:

ProtocolEndpoint
OTLP/HTTPhttp://<aggregator-nlb>:30055/v1/logs
OTLP/gRPC<aggregator-nlb>:30056

For a service outside Kubernetes, the aggregator reads these resource attributes to attribute the log:

Resource attributeBecomesRequired
kubesense.env_typeenv_typeYes — set to a value other than k8s or lambda
kubesense.clusterclusterYes — groups the data in the UI
service.nameworkload, instance, container_nameYes
service.namespacenamespaceRecommended

Severity comes from the OTLP severity_text field; when it is empty, KubeSense falls back to regex-matching the message body for a level.

A collector forwarding logs from an external service would be configured like this:

processors:
  resource/kubesense:
    attributes:
      - { key: kubesense.env_type, value: external,           action: upsert }
      - { key: kubesense.cluster,  value: prod-observability,  action: upsert }
      - { key: service.name,       value: payments-api,        action: upsert }
      - { key: service.namespace,  value: billing,             action: upsert }

exporters:
  otlphttp/kubesense:
    logs_endpoint: http://<aggregator-nlb>:30055/v1/logs
    tls: { insecure: true }

service:
  pipelines:
    logs:
      processors: [resource/kubesense]
      exporters: [otlphttp/kubesense]

Do not send OTLP logs from inside the cluster: Port 30055 is for external sources only. In-cluster pod logs are already collected by the logsensor, and sending them again over OTLP produces duplicates.

Option B — File log (plain JSON POST)

Enable aggregator.sources.file_logs and POST JSON to http://<aggregator-nlb>:30054. The body may be a single JSON object or an array of objects, in which case each element becomes one log record.

The aggregator maps a fixed set of keys:

JSON field you sendBecomes in KubeSense
timestamptimestamp — RFC 3339, e.g. 2026-07-23T10:15:00.000Z
messagebody, the searchable log line
serviceworkload and instance
applicationnamespace
hostnode_name

Records ingested this way are always tagged source = "file" and env_type = "legacy".

curl -X POST http://<aggregator-nlb>:30054 \
  -H 'Content-Type: application/json' \
  -d '[
        {
          "timestamp": "2026-07-23T10:15:00.000Z",
          "service": "payments-api",
          "application": "billing",
          "host": "vm-payments-01",
          "message": "WARN upstream connection reset, retrying in 2s"
        }
      ]'

Extra fields are dropped silently: The ClickHouse sink runs with skip_unknown_fields: true. Any key that does not match a column in the logs table is discarded without an error — so a typo in a field name looks exactly like success. Verify in the Log Explorer that the value actually landed, rather than trusting the 200 OK.


Part 3 — Pushing Prometheus metrics

The metrics-scraper is a vmagent instance. Besides scraping, it accepts pushed metrics on two paths:

PathFormatUse when
/api/v1/writePrometheus remote write (snappy protobuf)The sender is Prometheus, an OTel collector, or another vmagent
/api/v1/import/prometheusPrometheus text expositionYou have a /metrics-style payload to forward as-is

Remote write

Point any Prometheus-compatible sender at the metrics-scraper NLB:

remote_write:
  - url: http://<metrics-scraper-nlb>:30060/api/v1/write

From an OpenTelemetry Collector:

exporters:
  prometheusremotewrite/kubesense:
    endpoint: http://<metrics-scraper-nlb>:30060/api/v1/write
    tls: { insecure: true }
    external_labels:
      cluster: prod-observability

Pushing exposition-format text

If you can already fetch a Prometheus-format payload, forward it unchanged:

curl -s https://<source>/metrics \
  | curl -X POST --data-binary @- \
      'http://<metrics-scraper-nlb>:30060/api/v1/import/prometheus?extra_label=cluster=prod-observability'

The extra_label parameter attaches a label to every series in the request — use it to tag the origin, since pushed metrics carry no cluster attribution of their own.

Alternative — let the metrics-scraper pull instead

Many providers do not push at all. They expose a metrics export endpoint that already returns OpenMetrics/Prometheus format and expects you to poll it, usually behind an API key. When the endpoint is reachable from the cluster, scraping it is simpler than building a push relay — and needs no load balancer at all:

metrics-scraper:
  extraScrapeConfigs:
    - job_name: external-provider
      scrape_interval: 60s
      scheme: https
      metrics_path: /v1/metrics/export
      params:
        resource_id: ["<RESOURCE_ID>"]
      basic_auth:
        username: <API_KEY>
        password: <API_SECRET>
      static_configs:
        - targets: ["metrics.provider.example.com"]

Scrape when you can, push when you must: Check the provider's minimum sampling interval before lowering scrape_interval — polling faster than they publish returns the same data points and burns API quota. Use the push paths above only for sources the cluster cannot reach, such as a Prometheus in a remote network that can make outbound connections but accept none.


Verification

  • kubectl -n kubesense get svc kubesense-aggregator shows an external address
  • The sender can reach the port: nc -vz <aggregator-nlb> 30055
  • Logs appear in the Log Explorer, filtered by the service.name or service value you set
  • The cluster and env_type values are populated, not blank
  • Timestamps reflect event time, not ingest time — a wall of identical timestamps means the timestamp field was not parsed
  • Metrics appear in the Data Explorer; confirm the series name and that your cluster label is attached
  • Check for records the pipeline rejected: kubectl -n kubesense logs -l app=aggregator | grep -i dropped

Troubleshooting

SymptomLikely causeAction
Connection refused / timeoutSource not enabled, so nothing listensSet aggregator.sources.otel.enabled or file_logs.enabled and upgrade
Connection times out from outside the VPCInternal NLB, by designUse private connectivity, or check loadBalancerSourceRanges
200 OK but no logs in the UIField names do not match the mapping; unknown fields are dropped silentlyCompare your payload against the field table above
Logs arrive with blank clusterkubesense.cluster resource attribute not setAdd it in the collector's resource processor
Logs land with the wrong workloadservice.name missing, or kubesense.env_type set to k8sSet env_type to an external value such as external
All timestamps equal ingest timetimestamp not RFC 3339Emit e.g. 2026-07-23T10:15:00.000Z
Duplicate log linesIn-cluster logs sent over OTLP as well as by the logsensorUse 30055 for external sources only
Metrics rejectedWrong pathRemote write is /api/v1/write; exposition text is /api/v1/import/prometheus
Metrics have no cluster labelPushed metrics carry no attributionAdd extra_label= or external_labels

Best practices

  • Keep both load balancers internal and source-range restricted; neither endpoint authenticates callers.
  • Set kubesense.cluster and kubesense.env_type on every external source so the data is filterable and does not blend into in-cluster telemetry.
  • Give each external source a distinct service.name — it becomes the workload in the UI and is the field you will filter on.
  • External sources often need a different retention policy from in-cluster logs — some are low-volume but must be kept for a long time. Set this deliberately in Settings → Data Retention.
  • Watch cardinality after adding a push source. Vendor metrics often carry high-cardinality identifiers; review Settings → Metrics afterwards.