feat: add env example, chart config, and track lib
This commit is contained in:
parent
9bc0e742bc
commit
c7c490bf77
65
.env.example
Normal file
65
.env.example
Normal file
@ -0,0 +1,65 @@
|
||||
# ============================================================
|
||||
# GitDataAI Development Environment Variables
|
||||
# Copy to .env and adjust as needed
|
||||
# ============================================================
|
||||
|
||||
# ── Database ───────────────────────────────────────────────
|
||||
POSTGRES_USER=gitdata
|
||||
POSTGRES_PASSWORD=gitdata123
|
||||
POSTGRES_DB=app
|
||||
|
||||
# ── MinIO (S3-compatible storage) ──────────────────────────
|
||||
MINIO_ROOT_USER=admin
|
||||
MINIO_ROOT_PASSWORD=mysecret123
|
||||
|
||||
# ── Application ────────────────────────────────────────────
|
||||
APP_API_PORT=8080
|
||||
APP_NAME=gitdata
|
||||
APP_DOMAIN_URL=http://localhost
|
||||
APP_SESSION_SECRET=supersecretdevkey123
|
||||
|
||||
# ── Database Connection ────────────────────────────────────
|
||||
APP_DATABASE_URL=postgres://gitdata:gitdata123@localhost:5432/app
|
||||
|
||||
# ── Redis ──────────────────────────────────────────────────
|
||||
APP_REDIS_URLS=redis://localhost:6379
|
||||
|
||||
# ── Qdrant Vector DB ───────────────────────────────────────
|
||||
APP_QDRANT_URL=http://localhost:6333
|
||||
|
||||
# ── NATS ───────────────────────────────────────────────────
|
||||
NATS_URL=nats://localhost:4222
|
||||
|
||||
# ── Storage (S3) ───────────────────────────────────────────
|
||||
APP_STORAGE_BACKEND=s3
|
||||
APP_STORAGE_S3_BUCKET=gitdata
|
||||
APP_STORAGE_S3_REGION=us-east-1
|
||||
APP_STORAGE_S3_ENDPOINT_URL=http://localhost:9000
|
||||
APP_STORAGE_S3_ACCESS_KEY_ID=admin
|
||||
APP_STORAGE_S3_SECRET_ACCESS_KEY=mysecret123
|
||||
APP_STORAGE_S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# ── Git Service (gitpod) ───────────────────────────────────
|
||||
APP_GIT_RPC_ADDR=0.0.0.0
|
||||
APP_GIT_RPC_PORT=5030
|
||||
APP_GIT_HTTP_PORT=5023
|
||||
APP_SSH_PORT=5022
|
||||
|
||||
# ── GitSync Health ─────────────────────────────────────────
|
||||
APP_GITSYNC_HEALTH_PORT=5083
|
||||
|
||||
# ── SMTP (Email) ───────────────────────────────────────────
|
||||
# For development, use MailHog: docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
|
||||
APP_SMTP_HOST=localhost
|
||||
APP_SMTP_PORT=1025
|
||||
APP_SMTP_USERNAME=dev
|
||||
APP_SMTP_PASSWORD=dev
|
||||
APP_SMTP_FROM=Gitdata <noreply@localhost>
|
||||
|
||||
# ── AI / Embed ─────────────────────────────────────────────
|
||||
APP_AI_BASIC_URL=http://localhost:11434/v1
|
||||
APP_AI_API_KEY=ollama
|
||||
APP_EMBED_MODEL_BASE_URL=http://localhost:11434/v1
|
||||
APP_EMBED_MODEL_API_KEY=ollama
|
||||
APP_EMBED_MODEL_NAME=nomic-embed-text
|
||||
APP_EMBED_MODEL_DIMENSIONS=768
|
||||
0
chart/app/.helmignore
Normal file
0
chart/app/.helmignore
Normal file
17
chart/app/Chart.yaml
Normal file
17
chart/app/Chart.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
apiVersion: v2
|
||||
name: gitdataai
|
||||
description: GitDataAI — Git platform with AI-powered code agents
|
||||
|
||||
type: application
|
||||
version: 0.2.0
|
||||
appVersion: "1.0.0"
|
||||
|
||||
keywords:
|
||||
- git
|
||||
- ai
|
||||
- agent
|
||||
- code-review
|
||||
- cicd
|
||||
|
||||
maintainers:
|
||||
- name: gitdataai-team
|
||||
11
chart/app/templates/NOTES.txt
Normal file
11
chart/app/templates/NOTES.txt
Normal file
@ -0,0 +1,11 @@
|
||||
{{- $svcNames := list "gitdata" "gitpod" "gitsync" "email" }}
|
||||
{{- range $svcName := $svcNames }}
|
||||
{{- $svcCfg := index $.Values $svcName }}
|
||||
{{- if and $svcCfg.enabled (not (eq $svcName "email")) }}
|
||||
{{- if eq $svcName "gitpod" }}
|
||||
{{- printf " http://%s-http.%s.svc.cluster.local:%d/" (include "app.serviceFullname" (dict "root" $ "name" $svcName)) (include "app.namespace" $) (int $svcCfg.service.port) }}
|
||||
{{- else }}
|
||||
{{- printf " http://%s.%s.svc.cluster.local:%d/" (include "app.serviceFullname" (dict "root" $ "name" $svcName)) (include "app.namespace" $) (int $svcCfg.service.port) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
125
chart/app/templates/_helpers.tpl
Normal file
125
chart/app/templates/_helpers.tpl
Normal file
@ -0,0 +1,125 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "app.name" -}}
|
||||
{{- default .Chart.Name .Values.global.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "app.fullname" -}}
|
||||
{{- if .Values.global.fullnameOverride }}
|
||||
{{- .Values.global.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.global.nameOverride }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "app.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels.
|
||||
*/}}
|
||||
{{- define "app.labels" -}}
|
||||
helm.sh/chart: {{ include "app.chart" . }}
|
||||
app.kubernetes.io/name: {{ include "app.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Service labels.
|
||||
*/}}
|
||||
{{- define "app.serviceLabels" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $name := .name }}
|
||||
helm.sh/chart: {{ include "app.chart" $root }}
|
||||
app.kubernetes.io/name: {{ $name }}
|
||||
app.kubernetes.io/instance: {{ $root.Release.Name }}
|
||||
app.kubernetes.io/component: {{ $name }}
|
||||
{{- if $root.Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ $root.Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ $root.Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels.
|
||||
*/}}
|
||||
{{- define "app.serviceSelectorLabels" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $name := .name }}
|
||||
app.kubernetes.io/name: {{ $name }}
|
||||
app.kubernetes.io/instance: {{ $root.Release.Name }}
|
||||
app.kubernetes.io/component: {{ $name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Fully qualified service name: <release>-<chart>-<serviceName>
|
||||
*/}}
|
||||
{{- define "app.serviceFullname" -}}
|
||||
{{- $root := .root }}
|
||||
{{- $name := .name }}
|
||||
{{- if $root.Values.global.fullnameOverride }}
|
||||
{{- printf "%s-%s" $root.Values.global.fullnameOverride $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $chartName := default $root.Chart.Name $root.Values.global.nameOverride }}
|
||||
{{- printf "%s-%s-%s" $root.Release.Name $chartName $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Namespace.
|
||||
*/}}
|
||||
{{- define "app.namespace" -}}
|
||||
{{- .Values.global.namespace | default .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
ServiceAccount name.
|
||||
*/}}
|
||||
{{- define "app.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.name }}
|
||||
{{- .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- include "app.fullname" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Docker image reference.
|
||||
*/}}
|
||||
{{- define "app.image" -}}
|
||||
{{- $globalRegistry := .root.Values.global.image.registry }}
|
||||
{{- $registry := .svc.registry | default $globalRegistry }}
|
||||
{{- $name := .svc.name }}
|
||||
{{- $tag := .svc.tag | default .root.Values.global.image.tag | default "latest" }}
|
||||
{{- printf "%s/%s:%s" $registry $name $tag }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Image pull secrets.
|
||||
*/}}
|
||||
{{- define "app.imagePullSecrets" -}}
|
||||
{{- with .Values.global.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Gitpod RPC cluster DNS address.
|
||||
*/}}
|
||||
{{- define "app.gitpodRpcAddr" -}}
|
||||
{{ include "app.serviceFullname" (dict "root" . "name" "gitpod") }}-rpc.{{ include "app.namespace" . }}
|
||||
{{- end }}
|
||||
28
chart/app/templates/configmap.yaml
Normal file
28
chart/app/templates/configmap.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
{{/*
|
||||
Single shared ConfigMap for all services.
|
||||
Merges global.env with service-specific overrides.
|
||||
*/}}
|
||||
{{- $allEnv := deepCopy ($.Values.global.env | default dict) }}
|
||||
{{- /* Auto-fill APP_GIT_RPC_ADDR for gitdata -> gitpod-rpc service */}}
|
||||
{{- if and $.Values.gitdata.enabled (not $.Values.gitdata.env.APP_GIT_RPC_ADDR) }}
|
||||
{{- $_ := set $allEnv "APP_GIT_RPC_ADDR" (include "app.gitpodRpcAddr" $) }}
|
||||
{{- end }}
|
||||
{{- range $svcName, $svc := dict "gitdata" $.Values.gitdata "gitpod" $.Values.gitpod "gitsync" $.Values.gitsync "email" $.Values.email }}
|
||||
{{- if $svc.enabled }}
|
||||
{{- $allEnv = merge $allEnv ($svc.env | default dict) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $allEnv }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "app.fullname" $ }}
|
||||
namespace: {{ $.Values.global.namespace | default $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "app.labels" $ | nindent 4 }}
|
||||
data:
|
||||
{{- range $k, $v := $allEnv }}
|
||||
{{ $k }}: {{ $v | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
381
chart/app/templates/deployment.yaml
Normal file
381
chart/app/templates/deployment.yaml
Normal file
@ -0,0 +1,381 @@
|
||||
{{/*
|
||||
Deployments — One per enabled service.
|
||||
All pods share app-data-pvc mounted at /data.
|
||||
*/}}
|
||||
|
||||
{{/* ============================================================
|
||||
gitdata — Main API server
|
||||
============================================================ */}}
|
||||
{{- if .Values.gitdata.enabled }}
|
||||
{{- $svc := .Values.gitdata }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitdata") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitdata") | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $svc.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitdata") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with $svc.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitdata") | nindent 8 }}
|
||||
spec:
|
||||
{{- include "app.imagePullSecrets" . | nindent 6 }}
|
||||
serviceAccountName: {{ include "app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: {{ include "app.image" (dict "root" . "svc" $svc.image) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "app.fullname" . }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- with $svc.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.startupProbe.httpGet.path }}
|
||||
port: {{ $svc.startupProbe.httpGet.port }}
|
||||
initialDelaySeconds: {{ $svc.startupProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ $svc.startupProbe.periodSeconds }}
|
||||
failureThreshold: {{ $svc.startupProbe.failureThreshold }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.livenessProbe.httpGet.path }}
|
||||
port: {{ $svc.livenessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.livenessProbe.periodSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.readinessProbe.httpGet.path }}
|
||||
port: {{ $svc.readinessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.readinessProbe.periodSeconds }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: app-data-pvc
|
||||
{{- with $svc.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/* ============================================================
|
||||
gitpod — Git protocol server (HTTP + SSH + gRPC)
|
||||
============================================================ */}}
|
||||
{{- if .Values.gitpod.enabled }}
|
||||
{{- $svc := .Values.gitpod }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitpod") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $svc.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitpod") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with $svc.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitpod") | nindent 8 }}
|
||||
spec:
|
||||
{{- include "app.imagePullSecrets" . | nindent 6 }}
|
||||
serviceAccountName: {{ include "app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: {{ include "app.image" (dict "root" . "svc" $svc.image) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
- name: ssh
|
||||
containerPort: 2222
|
||||
protocol: TCP
|
||||
- name: grpc
|
||||
containerPort: 50051
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "app.fullname" . }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- with $svc.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if $svc.sshHostKeySecret }}
|
||||
- name: ssh-host-key
|
||||
mountPath: /etc/ssh
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.startupProbe.httpGet.path }}
|
||||
port: {{ $svc.startupProbe.httpGet.port }}
|
||||
initialDelaySeconds: {{ $svc.startupProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ $svc.startupProbe.periodSeconds }}
|
||||
failureThreshold: {{ $svc.startupProbe.failureThreshold }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.livenessProbe.httpGet.path }}
|
||||
port: {{ $svc.livenessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.livenessProbe.periodSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.readinessProbe.httpGet.path }}
|
||||
port: {{ $svc.readinessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.readinessProbe.periodSeconds }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: app-data-pvc
|
||||
{{- if $svc.sshHostKeySecret }}
|
||||
- name: ssh-host-key
|
||||
secret:
|
||||
secretName: {{ $svc.sshHostKeySecret }}
|
||||
defaultMode: 0600
|
||||
{{- end }}
|
||||
{{- with $svc.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/* ============================================================
|
||||
gitsync — Git sync worker
|
||||
============================================================ */}}
|
||||
{{- if .Values.gitsync.enabled }}
|
||||
{{- $svc := .Values.gitsync }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitsync") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitsync") | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $svc.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitsync") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with $svc.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitsync") | nindent 8 }}
|
||||
spec:
|
||||
{{- include "app.imagePullSecrets" . | nindent 6 }}
|
||||
serviceAccountName: {{ include "app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: {{ include "app.image" (dict "root" . "svc" $svc.image) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy }}
|
||||
ports:
|
||||
- name: health
|
||||
containerPort: 8081
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "app.fullname" . }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- with $svc.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.startupProbe.httpGet.path }}
|
||||
port: {{ $svc.startupProbe.httpGet.port }}
|
||||
initialDelaySeconds: {{ $svc.startupProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ $svc.startupProbe.periodSeconds }}
|
||||
failureThreshold: {{ $svc.startupProbe.failureThreshold }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.livenessProbe.httpGet.path }}
|
||||
port: {{ $svc.livenessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.livenessProbe.periodSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.readinessProbe.httpGet.path }}
|
||||
port: {{ $svc.readinessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.readinessProbe.periodSeconds }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: app-data-pvc
|
||||
{{- with $svc.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/* ============================================================
|
||||
email — Email worker service
|
||||
============================================================ */}}
|
||||
{{- if .Values.email.enabled }}
|
||||
{{- $svc := .Values.email }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "email") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "email") | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ $svc.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "email") | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- with $svc.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "email") | nindent 8 }}
|
||||
spec:
|
||||
{{- include "app.imagePullSecrets" . | nindent 6 }}
|
||||
serviceAccountName: {{ include "app.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: {{ include "app.image" (dict "root" . "svc" $svc.image) }}
|
||||
imagePullPolicy: {{ .Values.global.image.pullPolicy }}
|
||||
ports:
|
||||
- name: health
|
||||
containerPort: 8083
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "app.fullname" . }}
|
||||
resources:
|
||||
{{- toYaml $svc.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml $svc.securityContext | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
{{- with $svc.volumeMounts }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.startupProbe.httpGet.path }}
|
||||
port: {{ $svc.startupProbe.httpGet.port }}
|
||||
initialDelaySeconds: {{ $svc.startupProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ $svc.startupProbe.periodSeconds }}
|
||||
failureThreshold: {{ $svc.startupProbe.failureThreshold }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.livenessProbe.httpGet.path }}
|
||||
port: {{ $svc.livenessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.livenessProbe.periodSeconds }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: {{ $svc.readinessProbe.httpGet.path }}
|
||||
port: {{ $svc.readinessProbe.httpGet.port }}
|
||||
periodSeconds: {{ $svc.readinessProbe.periodSeconds }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: app-data-pvc
|
||||
{{- with $svc.volumes }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with $svc.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
38
chart/app/templates/hpa.yaml
Normal file
38
chart/app/templates/hpa.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
{{- range $svcName := list "gitdata" "gitpod" "gitsync" "email" }}
|
||||
{{- $svcCfg := index $.Values $svcName }}
|
||||
{{- $hpaCfg := index $.Values.autoscaling $svcName }}
|
||||
{{- if and $svcCfg.enabled $hpaCfg.enabled }}
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" $ "name" $svcName) }}
|
||||
namespace: {{ include "app.namespace" $ }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" $ "name" $svcName) | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "app.serviceFullname" (dict "root" $ "name" $svcName) }}
|
||||
minReplicas: {{ $hpaCfg.minReplicas }}
|
||||
maxReplicas: {{ $hpaCfg.maxReplicas }}
|
||||
metrics:
|
||||
{{- if $hpaCfg.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $hpaCfg.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if $hpaCfg.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ $hpaCfg.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
61
chart/app/templates/ingress.yaml
Normal file
61
chart/app/templates/ingress.yaml
Normal file
@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "app.fullname" . }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- if .Values.ingress.api.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.api.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingress.git.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.api.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "app.serviceFullname" (dict "root" $ "name" "gitdata") }}
|
||||
port:
|
||||
number: {{ $.Values.gitdata.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range .Values.ingress.git.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
pathType: {{ .pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "app.serviceFullname" (dict "root" $ "name" "gitpod") }}-http
|
||||
port:
|
||||
number: {{ $.Values.gitpod.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
21
chart/app/templates/pdb.yaml
Normal file
21
chart/app/templates/pdb.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
{{- if .Values.podDisruptionBudget.enabled }}
|
||||
{{- range $svcName := list "gitdata" "gitpod" "gitsync" "email" }}
|
||||
{{- $svcCfg := index $.Values $svcName }}
|
||||
{{- $pdbCfg := index $.Values.podDisruptionBudget $svcName }}
|
||||
{{- if and $svcCfg.enabled $pdbCfg.minAvailable }}
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" $ "name" $svcName) }}
|
||||
namespace: {{ $.Values.global.namespace | default $.Release.Namespace }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" $ "name" $svcName) | nindent 4 }}
|
||||
spec:
|
||||
minAvailable: {{ $pdbCfg.minAvailable }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" $ "name" $svcName) | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
141
chart/app/templates/service.yaml
Normal file
141
chart/app/templates/service.yaml
Normal file
@ -0,0 +1,141 @@
|
||||
{{/*
|
||||
Generate Services for each enabled service.
|
||||
*/}}
|
||||
|
||||
{{- if .Values.gitdata.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitdata") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitdata") | nindent 4 }}
|
||||
{{- with .Values.gitdata.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.gitdata.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.gitdata.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitdata") | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.gitpod.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitpod") }}-http
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
{{- with .Values.gitpod.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.gitpod.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.gitpod.service.port | default 8080 }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitpod") }}-ssh
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
{{- with .Values.gitpod.sshService.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.gitpod.sshService.type }}
|
||||
ports:
|
||||
- port: {{ .Values.gitpod.sshService.port | default 2222 }}
|
||||
targetPort: ssh
|
||||
protocol: TCP
|
||||
name: ssh
|
||||
selector:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitpod") }}-rpc
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
{{- with .Values.gitpod.rpcService.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.gitpod.rpcService.type }}
|
||||
ports:
|
||||
- port: {{ .Values.gitpod.rpcService.port | default 50051 }}
|
||||
targetPort: grpc
|
||||
protocol: TCP
|
||||
name: grpc
|
||||
selector:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitpod") | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.gitsync.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "gitsync") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "gitsync") | nindent 4 }}
|
||||
{{- with .Values.gitsync.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.gitsync.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.gitsync.service.port | default 8081 }}
|
||||
targetPort: health
|
||||
protocol: TCP
|
||||
name: health
|
||||
selector:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "gitsync") | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.email.enabled .Values.email.service.enabled }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" . "name" "email") }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" . "name" "email") | nindent 4 }}
|
||||
{{- with .Values.email.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.email.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.email.service.port | default 8083 }}
|
||||
targetPort: health
|
||||
protocol: TCP
|
||||
name: health
|
||||
selector:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" . "name" "email") | nindent 4 }}
|
||||
{{- end }}
|
||||
13
chart/app/templates/serviceaccount.yaml
Normal file
13
chart/app/templates/serviceaccount.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "app.serviceAccountName" . }}
|
||||
namespace: {{ include "app.namespace" . }}
|
||||
labels:
|
||||
{{- include "app.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
36
chart/app/templates/servicemonitor.yaml
Normal file
36
chart/app/templates/servicemonitor.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
{{- if .Values.serviceMonitor.enabled }}
|
||||
{{- $svcNames := list "gitdata" "gitpod" "gitsync" "email" }}
|
||||
{{- range $svcName := $svcNames }}
|
||||
{{- $svcCfg := index $.Values $svcName }}
|
||||
{{- $monitorCfg := index $.Values.serviceMonitor.services $svcName }}
|
||||
{{- if and $svcCfg.enabled $monitorCfg }}
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "app.serviceFullname" (dict "root" $ "name" $svcName) }}
|
||||
namespace: {{ include "app.namespace" $ }}
|
||||
labels:
|
||||
{{- include "app.serviceLabels" (dict "root" $ "name" $svcName) | nindent 4 }}
|
||||
{{- with $.Values.serviceMonitor.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with $.Values.serviceMonitor.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
endpoints:
|
||||
- interval: {{ $.Values.serviceMonitor.interval }}
|
||||
port: {{ if eq $svcName "gitdata" }}http{{ else if eq $svcName "gitpod" }}http{{ else }}health{{ end }}
|
||||
{{- if eq $svcName "gitdata" }}
|
||||
path: /metrics
|
||||
{{- else }}
|
||||
path: /health
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "app.serviceSelectorLabels" (dict "root" $ "name" $svcName) | nindent 6 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
365
chart/app/values.yaml
Normal file
365
chart/app/values.yaml
Normal file
@ -0,0 +1,365 @@
|
||||
global:
|
||||
image:
|
||||
registry: "harbor.gitdata.me/app"
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "latest"
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
namespace: "gitdataai"
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: ""
|
||||
|
||||
gitdata:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
name: gitdata-gitdata
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
env:
|
||||
APP_API_PORT: "8080"
|
||||
APP_OTEL_SERVICE_NAME: "gitdata-api"
|
||||
APP_GIT_RPC_ADDR: ""
|
||||
APP_GIT_RPC_PORT: "50051"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
annotations: {}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1000m
|
||||
memory: 1Gi
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
port: http
|
||||
periodSeconds: 15
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /metrics
|
||||
port: http
|
||||
periodSeconds: 10
|
||||
|
||||
podAnnotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/metrics"
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
volumes: []
|
||||
volumeMounts: []
|
||||
|
||||
|
||||
gitpod:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
name: gitdata-gitpod
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
env:
|
||||
APP_GIT_HTTP_PORT: "8080"
|
||||
APP_SSH_PORT: "2222"
|
||||
APP_GIT_RPC_ADDR: "0.0.0.0"
|
||||
APP_GIT_RPC_PORT: "50051"
|
||||
APP_OTEL_SERVICE_NAME: "gitpod"
|
||||
APP_SSH_DOMAIN: ""
|
||||
APP_GIT_HTTP_DOMAIN: ""
|
||||
APP_REPOS_ROOT: "/data/repos"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
annotations: {}
|
||||
|
||||
sshService:
|
||||
type: LoadBalancer
|
||||
port: 2222
|
||||
annotations: {}
|
||||
|
||||
rpcService:
|
||||
type: ClusterIP
|
||||
port: 50051
|
||||
annotations: {}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: 2000m
|
||||
memory: 2Gi
|
||||
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 20
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
periodSeconds: 15
|
||||
|
||||
podAnnotations: {}
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
# -- SSH host key secret (mount to /etc/ssh)
|
||||
sshHostKeySecret: ""
|
||||
|
||||
# -- Data volumes (repos storage)
|
||||
volumes: []
|
||||
volumeMounts: []
|
||||
|
||||
|
||||
gitsync:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
name: gitdata-gitsync
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
env:
|
||||
APP_GITSYNC_HEALTH_PORT: "8081"
|
||||
APP_OTEL_SERVICE_NAME: "gitsync"
|
||||
APP_REPOS_ROOT: "/data/repos"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8081
|
||||
annotations: {}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: health
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: health
|
||||
periodSeconds: 30
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: health
|
||||
periodSeconds: 15
|
||||
|
||||
podAnnotations: {}
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
|
||||
|
||||
volumes: []
|
||||
volumeMounts: []
|
||||
|
||||
|
||||
email:
|
||||
enabled: true
|
||||
replicaCount: 1
|
||||
image:
|
||||
name: gitdata-email
|
||||
registry: ""
|
||||
tag: ""
|
||||
|
||||
env:
|
||||
APP_EMAIL_HEALTH_PORT: "8083"
|
||||
APP_OTEL_SERVICE_NAME: "email-service"
|
||||
|
||||
service:
|
||||
enabled: false
|
||||
type: ClusterIP
|
||||
port: 8083
|
||||
annotations: {}
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: health
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: health
|
||||
periodSeconds: 30
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: health
|
||||
periodSeconds: 15
|
||||
|
||||
podAnnotations: {}
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
securityContext:
|
||||
readOnlyRootFilesystem: false
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
volumes: []
|
||||
volumeMounts: []
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "cloudflare-acme-cluster-issuer"
|
||||
api:
|
||||
hosts:
|
||||
- host: dev.gitdata.ai
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- dev.gitdata.ai
|
||||
secretName: dev-gitdata-ai-tls
|
||||
git:
|
||||
hosts:
|
||||
- host: gitdev.gitdata.ai
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- hosts:
|
||||
- gitdev.gitdata.ai
|
||||
secretName: gitdev-gitdata-ai-tls
|
||||
|
||||
serviceMonitor:
|
||||
enabled: false
|
||||
interval: 30s
|
||||
labels: {}
|
||||
annotations: {}
|
||||
services:
|
||||
gitdata: true
|
||||
gitpod: true
|
||||
gitsync: true
|
||||
email: true
|
||||
|
||||
autoscaling:
|
||||
gitdata:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: ""
|
||||
gitpod:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPUUtilizationPercentage: 75
|
||||
targetMemoryUtilizationPercentage: ""
|
||||
gitsync:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 5
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: ""
|
||||
email:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 3
|
||||
targetCPUUtilizationPercentage: 80
|
||||
targetMemoryUtilizationPercentage: ""
|
||||
|
||||
podDisruptionBudget:
|
||||
enabled: false
|
||||
gitdata:
|
||||
minAvailable: 1
|
||||
gitpod:
|
||||
minAvailable: 1
|
||||
gitsync:
|
||||
minAvailable: ""
|
||||
email:
|
||||
minAvailable: ""
|
||||
34
lib/track/Cargo.toml
Normal file
34
lib/track/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "track"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
description.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
documentation.workspace = true
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
name = "track"
|
||||
[dependencies]
|
||||
config = { workspace = true }
|
||||
|
||||
opentelemetry = { version = "0.32.0", features = ["trace", "logs", "metrics"] }
|
||||
opentelemetry_sdk = { version = "0.32.1", features = ["trace", "logs", "metrics", "rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.32.0", features = ["http-proto", "trace", "logs", "metrics", "reqwest-client", "tls-roots"] }
|
||||
opentelemetry-appender-tracing = "0.32.0"
|
||||
opentelemetry-prometheus = "0.32.0"
|
||||
prometheus = "0.13"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter", "json"] }
|
||||
tracing-appender = "0.2"
|
||||
anyhow = { workspace = true }
|
||||
tracing-opentelemetry = "0.33.0"
|
||||
tracing = "0.1.41"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
10
lib/track/lib.rs
Normal file
10
lib/track/lib.rs
Normal file
@ -0,0 +1,10 @@
|
||||
pub mod metrics;
|
||||
mod otel;
|
||||
|
||||
pub use metrics::{MetricsRegistry, record_otel_counter};
|
||||
pub use otel::LgtmGuard as OtelGuard;
|
||||
pub use otel::LgtmGuard;
|
||||
pub use otel::init_lgtm;
|
||||
|
||||
// Re-export prometheus types so downstream crates only need `track`.
|
||||
pub use prometheus::{Counter, CounterVec, Gauge, Histogram, HistogramVec};
|
||||
249
lib/track/metrics.rs
Normal file
249
lib/track/metrics.rs
Normal file
@ -0,0 +1,249 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use opentelemetry::KeyValue;
|
||||
use prometheus::{
|
||||
CounterVec, Encoder, Gauge, HistogramOpts, HistogramVec, Opts, Registry,
|
||||
TextEncoder,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MetricsRegistry {
|
||||
inner: Arc<MetricsRegistryInner>,
|
||||
}
|
||||
|
||||
struct MetricsRegistryInner {
|
||||
registry: Registry,
|
||||
counters: Mutex<HashMap<String, prometheus::Counter>>,
|
||||
histograms: Mutex<HashMap<String, prometheus::Histogram>>,
|
||||
counter_vecs: Mutex<HashMap<String, CounterVec>>,
|
||||
histogram_vecs: Mutex<HashMap<String, HistogramVec>>,
|
||||
gauges: Mutex<HashMap<String, Gauge>>,
|
||||
}
|
||||
|
||||
impl MetricsRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(MetricsRegistryInner {
|
||||
registry: Registry::new(),
|
||||
counters: Mutex::new(HashMap::new()),
|
||||
histograms: Mutex::new(HashMap::new()),
|
||||
counter_vecs: Mutex::new(HashMap::new()),
|
||||
histogram_vecs: Mutex::new(HashMap::new()),
|
||||
gauges: Mutex::new(HashMap::new()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn registry(&self) -> &Registry {
|
||||
&self.inner.registry
|
||||
}
|
||||
|
||||
pub fn register_counter(
|
||||
&self,
|
||||
name: &str,
|
||||
help: &str,
|
||||
) -> prometheus::Result<prometheus::Counter> {
|
||||
let mut counters =
|
||||
self.inner.counters.lock().expect("metrics mutex poisoned");
|
||||
if let Some(counter) = counters.get(name) {
|
||||
return Ok(counter.clone());
|
||||
}
|
||||
|
||||
let counter = prometheus::Counter::new(name, help)?;
|
||||
self.inner.registry.register(Box::new(counter.clone()))?;
|
||||
counters.insert(name.to_string(), counter.clone());
|
||||
Ok(counter)
|
||||
}
|
||||
|
||||
pub fn register_histogram(
|
||||
&self,
|
||||
name: &str,
|
||||
help: &str,
|
||||
buckets: Vec<f64>,
|
||||
) -> prometheus::Result<prometheus::Histogram> {
|
||||
let mut histograms = self
|
||||
.inner
|
||||
.histograms
|
||||
.lock()
|
||||
.expect("metrics mutex poisoned");
|
||||
if let Some(histogram) = histograms.get(name) {
|
||||
return Ok(histogram.clone());
|
||||
}
|
||||
|
||||
let opts = prometheus::HistogramOpts::new(name, help).buckets(buckets);
|
||||
let histogram = prometheus::Histogram::with_opts(opts)?;
|
||||
self.inner.registry.register(Box::new(histogram.clone()))?;
|
||||
histograms.insert(name.to_string(), histogram.clone());
|
||||
Ok(histogram)
|
||||
}
|
||||
|
||||
pub fn register_counter_vec(
|
||||
&self,
|
||||
name: &str,
|
||||
help: &str,
|
||||
labels: &[&str],
|
||||
) -> prometheus::Result<CounterVec> {
|
||||
let mut counter_vecs = self
|
||||
.inner
|
||||
.counter_vecs
|
||||
.lock()
|
||||
.expect("metrics mutex poisoned");
|
||||
if let Some(cv) = counter_vecs.get(name) {
|
||||
return Ok(cv.clone());
|
||||
}
|
||||
|
||||
let opts = Opts::new(name, help);
|
||||
let cv = CounterVec::new(opts, labels)?;
|
||||
self.inner.registry.register(Box::new(cv.clone()))?;
|
||||
counter_vecs.insert(name.to_string(), cv.clone());
|
||||
Ok(cv)
|
||||
}
|
||||
|
||||
pub fn register_histogram_vec(
|
||||
&self,
|
||||
name: &str,
|
||||
help: &str,
|
||||
labels: &[&str],
|
||||
buckets: Vec<f64>,
|
||||
) -> prometheus::Result<HistogramVec> {
|
||||
let mut histogram_vecs = self
|
||||
.inner
|
||||
.histogram_vecs
|
||||
.lock()
|
||||
.expect("metrics mutex poisoned");
|
||||
if let Some(hv) = histogram_vecs.get(name) {
|
||||
return Ok(hv.clone());
|
||||
}
|
||||
|
||||
let opts = HistogramOpts::new(name, help).buckets(buckets);
|
||||
let hv = HistogramVec::new(opts, labels)?;
|
||||
self.inner.registry.register(Box::new(hv.clone()))?;
|
||||
histogram_vecs.insert(name.to_string(), hv.clone());
|
||||
Ok(hv)
|
||||
}
|
||||
|
||||
pub fn register_gauge(
|
||||
&self,
|
||||
name: &str,
|
||||
help: &str,
|
||||
) -> prometheus::Result<prometheus::Gauge> {
|
||||
let mut gauges =
|
||||
self.inner.gauges.lock().expect("metrics mutex poisoned");
|
||||
if let Some(gauge) = gauges.get(name) {
|
||||
return Ok(gauge.clone());
|
||||
}
|
||||
|
||||
let gauge = prometheus::Gauge::new(name, help)?;
|
||||
self.inner.registry.register(Box::new(gauge.clone()))?;
|
||||
gauges.insert(name.to_string(), gauge.clone());
|
||||
Ok(gauge)
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<String, String> {
|
||||
let mut buffer = Vec::new();
|
||||
let encoder = TextEncoder::new();
|
||||
encoder
|
||||
.encode(&self.inner.registry.gather(), &mut buffer)
|
||||
.map_err(|e| format!("failed to encode metrics: {e}"))?;
|
||||
String::from_utf8(buffer).map_err(|e| format!("invalid utf8: {e}"))
|
||||
}
|
||||
|
||||
pub fn gather(&self) -> Vec<prometheus::proto::MetricFamily> {
|
||||
self.inner.registry.gather()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MetricsRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_otel_counter(
|
||||
name: &'static str,
|
||||
value: u64,
|
||||
labels: &[(&'static str, String)],
|
||||
) {
|
||||
if value == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let attrs: Vec<KeyValue> = labels
|
||||
.iter()
|
||||
.map(|(key, value)| KeyValue::new(*key, value.clone()))
|
||||
.collect();
|
||||
|
||||
static COUNTERS: std::sync::OnceLock<
|
||||
std::sync::Mutex<
|
||||
HashMap<&'static str, opentelemetry::metrics::Counter<u64>>,
|
||||
>,
|
||||
> = std::sync::OnceLock::new();
|
||||
|
||||
let counters =
|
||||
COUNTERS.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
|
||||
let mut map = counters.lock().expect("otel counter mutex poisoned");
|
||||
let counter = map
|
||||
.entry(name)
|
||||
.or_insert_with(|| {
|
||||
opentelemetry::global::meter("gitdataai")
|
||||
.u64_counter(name)
|
||||
.build()
|
||||
})
|
||||
.clone();
|
||||
drop(map);
|
||||
counter.add(value, &attrs);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::MetricsRegistry;
|
||||
|
||||
#[test]
|
||||
fn repeated_counter_vec_registration_reuses_metric() {
|
||||
let registry = MetricsRegistry::new();
|
||||
|
||||
let first = registry
|
||||
.register_counter_vec("test_events_total", "Test events", &["kind"])
|
||||
.expect("first registration should succeed");
|
||||
let second = registry
|
||||
.register_counter_vec("test_events_total", "Test events", &["kind"])
|
||||
.expect("second registration should reuse existing metric");
|
||||
|
||||
first.with_label_values(&["a"]).inc();
|
||||
second.with_label_values(&["a"]).inc();
|
||||
|
||||
let encoded = registry.encode().expect("metrics should encode");
|
||||
assert!(encoded.contains("test_events_total{kind=\"a\"} 2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_histogram_vec_registration_reuses_metric() {
|
||||
let registry = MetricsRegistry::new();
|
||||
|
||||
let first = registry
|
||||
.register_histogram_vec(
|
||||
"test_duration_seconds",
|
||||
"Test duration",
|
||||
&["kind"],
|
||||
vec![0.1, 1.0],
|
||||
)
|
||||
.expect("first registration should succeed");
|
||||
let second = registry
|
||||
.register_histogram_vec(
|
||||
"test_duration_seconds",
|
||||
"Test duration",
|
||||
&["kind"],
|
||||
vec![0.1, 1.0],
|
||||
)
|
||||
.expect("second registration should reuse existing metric");
|
||||
|
||||
first.with_label_values(&["a"]).observe(0.2);
|
||||
second.with_label_values(&["a"]).observe(0.3);
|
||||
|
||||
let encoded = registry.encode().expect("metrics should encode");
|
||||
assert!(encoded.contains("test_duration_seconds_count{kind=\"a\"} 2"));
|
||||
}
|
||||
}
|
||||
430
lib/track/otel.rs
Normal file
430
lib/track/otel.rs
Normal file
@ -0,0 +1,430 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use config::AppConfig;
|
||||
use opentelemetry::KeyValue;
|
||||
use opentelemetry::trace::TracerProvider;
|
||||
use opentelemetry_otlp::{WithExportConfig, WithHttpConfig};
|
||||
use opentelemetry_sdk::{
|
||||
Resource, logs::SdkLoggerProvider, metrics::SdkMeterProvider,
|
||||
trace::SdkTracerProvider,
|
||||
};
|
||||
use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt};
|
||||
|
||||
const OTEL_EXPORT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
type WorkerGuard = tracing_appender::non_blocking::WorkerGuard;
|
||||
|
||||
/// `io::Write` adapter that strips the path prefix down to `gitdataai/` for
|
||||
/// cleaner log output.
|
||||
#[derive(Clone)]
|
||||
struct StripWriter<W: io::Write> {
|
||||
inner: W,
|
||||
}
|
||||
|
||||
impl<W: io::Write> StripWriter<W> {
|
||||
fn new(inner: W) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: io::Write> io::Write for StripWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let s = String::from_utf8_lossy(buf);
|
||||
if let Some(pos) = s.rfind("gitdataai/") {
|
||||
let start = pos + "gitdataai/".len();
|
||||
self.inner.write_all(s[start..].as_bytes())?;
|
||||
} else {
|
||||
self.inner.write_all(buf)?;
|
||||
}
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_writer() -> impl for<'a> tracing_subscriber::fmt::MakeWriter<'a> {
|
||||
|| StripWriter::new(io::stdout())
|
||||
}
|
||||
|
||||
pub struct LgtmGuard {
|
||||
tracer_provider: SdkTracerProvider,
|
||||
logger_provider: SdkLoggerProvider,
|
||||
meter_provider: SdkMeterProvider,
|
||||
_file_guard: Option<WorkerGuard>,
|
||||
}
|
||||
|
||||
struct OtlpEndpoints {
|
||||
traces: String,
|
||||
logs: String,
|
||||
metrics: String,
|
||||
}
|
||||
|
||||
impl Drop for LgtmGuard {
|
||||
fn drop(&mut self) {
|
||||
let mut had_error = false;
|
||||
if let Err(err) = self.tracer_provider.shutdown() {
|
||||
had_error = true;
|
||||
eprintln!("failed to shutdown OTel tracer provider: {err}");
|
||||
}
|
||||
if let Err(err) = self.meter_provider.shutdown() {
|
||||
had_error = true;
|
||||
eprintln!("failed to shutdown OTel meter provider: {err}");
|
||||
}
|
||||
if let Err(err) = self.logger_provider.shutdown() {
|
||||
had_error = true;
|
||||
eprintln!("failed to shutdown OTel logger provider: {err}");
|
||||
}
|
||||
drop(self._file_guard.take());
|
||||
if !had_error {
|
||||
eprintln!("OpenTelemetry providers shut down");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_lgtm(config: &AppConfig) -> anyhow::Result<Option<LgtmGuard>> {
|
||||
let filter = EnvFilter::try_new(config.log_level()?)?;
|
||||
let log_format = config.log_format()?;
|
||||
|
||||
if !config.otel_enabled()? {
|
||||
let file_guard = init_fmt_subscriber(filter, &log_format, config)?;
|
||||
if let Some(guard) = file_guard {
|
||||
static FILE_GUARD: std::sync::OnceLock<WorkerGuard> =
|
||||
std::sync::OnceLock::new();
|
||||
let _ = FILE_GUARD.set(guard);
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (endpoint, tracer_provider, logger_provider, meter_provider) =
|
||||
build_lgtm_guard(config)?;
|
||||
let guard = LgtmGuard {
|
||||
tracer_provider: tracer_provider.clone(),
|
||||
logger_provider: logger_provider.clone(),
|
||||
meter_provider: meter_provider.clone(),
|
||||
_file_guard: None,
|
||||
};
|
||||
let file_guard =
|
||||
install_otel_subscriber(filter, &guard, &log_format, config)?;
|
||||
opentelemetry::global::set_meter_provider(meter_provider.clone());
|
||||
tracing::info!(endpoint = %endpoint, "LGTM observability initialized");
|
||||
|
||||
Ok(Some(LgtmGuard {
|
||||
tracer_provider,
|
||||
logger_provider,
|
||||
meter_provider,
|
||||
_file_guard: file_guard,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_lgtm_guard(
|
||||
config: &AppConfig,
|
||||
) -> anyhow::Result<(
|
||||
String,
|
||||
SdkTracerProvider,
|
||||
SdkLoggerProvider,
|
||||
SdkMeterProvider,
|
||||
)> {
|
||||
let endpoint = config.otel_endpoint()?;
|
||||
let endpoints = build_otlp_endpoints(&endpoint)?;
|
||||
let headers = build_headers(config)?;
|
||||
let resource = build_resource(config)?;
|
||||
|
||||
let trace_exporter = build_trace_exporter(&endpoints.traces, &headers)?;
|
||||
let log_exporter = build_log_exporter(&endpoints.logs, &headers)?;
|
||||
let metric_exporter = build_metric_exporter(&endpoints.metrics, headers)?;
|
||||
|
||||
let tracer_provider = SdkTracerProvider::builder()
|
||||
.with_batch_exporter(trace_exporter)
|
||||
.with_resource(resource.clone())
|
||||
.build();
|
||||
let logger_provider = SdkLoggerProvider::builder()
|
||||
.with_batch_exporter(log_exporter)
|
||||
.with_resource(resource.clone())
|
||||
.build();
|
||||
let meter_provider = SdkMeterProvider::builder()
|
||||
.with_periodic_exporter(metric_exporter)
|
||||
.with_resource(resource)
|
||||
.build();
|
||||
|
||||
Ok((endpoint, tracer_provider, logger_provider, meter_provider))
|
||||
}
|
||||
|
||||
fn install_otel_subscriber(
|
||||
filter: EnvFilter,
|
||||
guard: &LgtmGuard,
|
||||
log_format: &str,
|
||||
config: &AppConfig,
|
||||
) -> anyhow::Result<Option<WorkerGuard>> {
|
||||
let tracer = guard.tracer_provider.tracer(env!("CARGO_PKG_NAME"));
|
||||
let otel_log_layer =
|
||||
opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(
|
||||
&guard.logger_provider,
|
||||
);
|
||||
|
||||
if config.log_file_enabled()? {
|
||||
let dir = config
|
||||
.log_file_path()
|
||||
.unwrap_or_else(|_| "./logs".to_string());
|
||||
let file_appender =
|
||||
tracing_appender::rolling::daily(dir, "gitdataai.log");
|
||||
let (non_blocking, file_guard) =
|
||||
tracing_appender::non_blocking(file_appender);
|
||||
let file_writer = {
|
||||
let nb = non_blocking;
|
||||
move || StripWriter::new(nb.clone())
|
||||
};
|
||||
if log_format.eq_ignore_ascii_case("json") {
|
||||
let subscriber = Registry::default()
|
||||
.with(filter)
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.with(otel_log_layer)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(strip_writer()),
|
||||
)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_ansi(false)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(file_writer),
|
||||
);
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
return Ok(Some(file_guard));
|
||||
}
|
||||
let subscriber = Registry::default()
|
||||
.with(filter)
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.with(otel_log_layer)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(strip_writer()),
|
||||
)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(file_writer),
|
||||
);
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
return Ok(Some(file_guard));
|
||||
}
|
||||
|
||||
if log_format.eq_ignore_ascii_case("json") {
|
||||
let subscriber = Registry::default()
|
||||
.with(filter)
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.with(otel_log_layer)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(strip_writer()),
|
||||
);
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let subscriber = Registry::default()
|
||||
.with(filter)
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.with(otel_log_layer)
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(strip_writer()),
|
||||
);
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn build_trace_exporter(
|
||||
endpoint: &str,
|
||||
headers: &HashMap<String, String>,
|
||||
) -> anyhow::Result<opentelemetry_otlp::SpanExporter> {
|
||||
opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_http()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(OTEL_EXPORT_TIMEOUT)
|
||||
.with_headers(headers.clone())
|
||||
.build()
|
||||
.context("failed to build OTLP trace exporter")
|
||||
}
|
||||
|
||||
fn build_log_exporter(
|
||||
endpoint: &str,
|
||||
headers: &HashMap<String, String>,
|
||||
) -> anyhow::Result<opentelemetry_otlp::LogExporter> {
|
||||
opentelemetry_otlp::LogExporter::builder()
|
||||
.with_http()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(OTEL_EXPORT_TIMEOUT)
|
||||
.with_headers(headers.clone())
|
||||
.build()
|
||||
.context("failed to build OTLP log exporter")
|
||||
}
|
||||
|
||||
fn build_metric_exporter(
|
||||
endpoint: &str,
|
||||
headers: HashMap<String, String>,
|
||||
) -> anyhow::Result<opentelemetry_otlp::MetricExporter> {
|
||||
opentelemetry_otlp::MetricExporter::builder()
|
||||
.with_http()
|
||||
.with_endpoint(endpoint)
|
||||
.with_timeout(OTEL_EXPORT_TIMEOUT)
|
||||
.with_headers(headers)
|
||||
.build()
|
||||
.context("failed to build OTLP metric exporter")
|
||||
}
|
||||
|
||||
fn init_fmt_subscriber(
|
||||
filter: EnvFilter,
|
||||
log_format: &str,
|
||||
config: &AppConfig,
|
||||
) -> anyhow::Result<Option<WorkerGuard>> {
|
||||
let file_enabled = config.log_file_enabled()?;
|
||||
|
||||
if log_format.eq_ignore_ascii_case("json") {
|
||||
if file_enabled {
|
||||
let dir = config
|
||||
.log_file_path()
|
||||
.unwrap_or_else(|_| "./logs".to_string());
|
||||
let file_appender =
|
||||
tracing_appender::rolling::daily(dir, "gitdataai.log");
|
||||
let (non_blocking, guard) =
|
||||
tracing_appender::non_blocking(file_appender);
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.json()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer({
|
||||
let nb = non_blocking;
|
||||
move || StripWriter::new(nb.clone())
|
||||
})
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
return Ok(Some(guard));
|
||||
}
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.json()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(strip_writer())
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if file_enabled {
|
||||
let dir = config
|
||||
.log_file_path()
|
||||
.unwrap_or_else(|_| "./logs".to_string());
|
||||
let file_appender =
|
||||
tracing_appender::rolling::daily(dir, "gitdataai.log");
|
||||
let (non_blocking, guard) =
|
||||
tracing_appender::non_blocking(file_appender);
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer({
|
||||
let nb = non_blocking;
|
||||
move || StripWriter::new(nb.clone())
|
||||
})
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
return Ok(Some(guard));
|
||||
}
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(strip_writer())
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("failed to initialize tracing subscriber")?;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn build_resource(config: &AppConfig) -> anyhow::Result<Resource> {
|
||||
Ok(Resource::builder()
|
||||
.with_service_name(config.otel_service_name()?)
|
||||
.with_attributes([KeyValue::new(
|
||||
"service.version",
|
||||
config.otel_service_version()?,
|
||||
)])
|
||||
.build())
|
||||
}
|
||||
|
||||
fn build_headers(
|
||||
config: &AppConfig,
|
||||
) -> anyhow::Result<HashMap<String, String>> {
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(auth) = config.otel_authorization()? {
|
||||
headers.insert("Authorization".to_string(), auth);
|
||||
} else if let Some(token) = config.otel_organization()? {
|
||||
headers.insert(
|
||||
"signoz-access-token".to_string(),
|
||||
format!("ingest:{token}"),
|
||||
);
|
||||
}
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
fn build_otlp_endpoints(endpoint: &str) -> anyhow::Result<OtlpEndpoints> {
|
||||
let base = normalize_otlp_base(endpoint)?;
|
||||
Ok(OtlpEndpoints {
|
||||
traces: format!("{base}/v1/traces"),
|
||||
logs: format!("{base}/v1/logs"),
|
||||
metrics: format!("{base}/v1/metrics"),
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_otlp_base(endpoint: &str) -> anyhow::Result<String> {
|
||||
let endpoint = endpoint.trim().trim_end_matches('/');
|
||||
if endpoint.is_empty() {
|
||||
bail!("APP_OTEL_ENDPOINT must not be empty");
|
||||
}
|
||||
for suffix in ["/v1/traces", "/v1/logs", "/v1/metrics"] {
|
||||
if let Some(base) = endpoint.strip_suffix(suffix) {
|
||||
let base = base.trim_end_matches('/');
|
||||
if base.is_empty() {
|
||||
bail!("APP_OTEL_ENDPOINT base URL must not be empty");
|
||||
}
|
||||
return Ok(base.to_string());
|
||||
}
|
||||
}
|
||||
Ok(endpoint.to_string())
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user