* Copyright (c) 2024 Huawei Technologies Co., Ltd.
* openFuyao is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package v1
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/emicklei/go-restful/v3"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"openfuyao/logging-operator/pkg/server"
"openfuyao/logging-operator/pkg/tools"
)
func New() *Handler {
podClient, configmapClient, scretsClient := newKubernetesAPI("loki")
handler := &Handler{
RequestsMap: make(map[string]*HTTPRequest),
timeUnitsDict: initializeTimeUnits(),
PodClient: podClient,
ConfigmapClient: configmapClient,
SecretsClient: scretsClient,
}
initializeRequests(handler)
return handler
}
func newKubernetesAPI(namespace string) (corev1.PodInterface, corev1.ConfigMapInterface, corev1.SecretInterface) {
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, nil, nil
}
podsClient := clientset.CoreV1().Pods(namespace)
configmapClient := clientset.CoreV1().ConfigMaps(namespace)
scretsClient := clientset.CoreV1().Secrets(namespace)
return podsClient, configmapClient, scretsClient
}
func initializeRequests(h *Handler) {
baseRequests := map[string]string{
"getFileNameList": "/loki/api/v1/label/filename/values",
"filterRequest": "/loki/api/v1/query_range",
"getResourceList": "/loki/api/v1/series",
"getConfigmapList": "/api/kubernetes/api/v1/namespaces/loki/configmaps/loki-promtail",
"getPodList": "/api/kubernetes/api/v1/namespaces/loki/pods",
"getPod": "/api/kubernetes/api/v1/namespaces/loki/pods/",
"patchConfigMap": "/api/kubernetes/api/v1/namespaces/loki/configmaps/loki-promtail",
"getAlertingRules": "/loki/api/v1/rules",
}
for key, path := range baseRequests {
baseURL := os.Getenv("LOKI_HOSTS")
if key == "patchConfigMap" {
h.RequestsMap[key] = &HTTPRequest{
URL: baseURL + path,
Method: "PATCH",
Headers: map[string]string{
ContentType: "application/merge-patch+json",
},
Body: []byte(`{
"data": {
"promtail.yaml": "updated configuration here..."
}
}`),
}
} else {
h.RequestsMap[key] = &HTTPRequest{
URL: baseURL + path,
Method: "GET",
Headers: map[string]string{
ContentType: ContentTypeJSON,
},
}
}
}
}
func sendHTTPRequestResourceOnly(req *HTTPRequest, fullURL string) (map[string]interface{}, error) {
httpReq, err := http.NewRequest(req.Method, fullURL, bytes.NewBuffer(req.Body))
if err != nil {
errorMessage := fmt.Sprintf("failed to create HTTP request: %v", err)
tools.LogError(errorMessage)
return nil, errors.New(errorMessage)
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
errorMessage := fmt.Sprintf("LogError sending request to Loki: %v", err)
tools.LogError(errorMessage)
return nil, errors.New(errorMessage)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
errorMessage := fmt.Sprintf("LogError reading response body: %v", err)
tools.LogError(errorMessage)
return nil, errors.New(errorMessage)
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
errorMessage := fmt.Sprintf("LogError parsing JSON response: %v", err)
tools.LogError(errorMessage)
return nil, errors.New(errorMessage)
}
return result, nil
}
func (h *Handler) getResourceList(request *restful.Request, response *restful.Response) {
req, exists := h.RequestsMap["getResourceList"]
if !exists {
errorMessage := fmt.Sprintf("Request configuration for 'getFileNameList' not found." +
" Please ensure it is defined in the RequestsMap.")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusNotFound, "Resource not found")
}
namespace := server.GetQueryParamOrDefault(request, server.Namespace, "")
pod := server.GetQueryParamOrDefault(request, server.PodName, "")
container := server.GetQueryParamOrDefault(request, server.Container, "")
params := QueryResourcesParams{
Namespace: namespace,
Pod: pod,
Container: container,
}
start, end := calculateQueryTimesUnix("504h")
queryParams := url.Values{}
queryParams.Add("start", start)
queryParams.Add("end", end)
fullURL := fmt.Sprintf("%s?%s", req.URL, queryParams.Encode())
result, err := sendHTTPRequestResourceOnly(req, fullURL)
if err != nil {
handleError(response, "Failed to send http request to loki: ", err)
}
ns, po, co := getList(result, params)
resources := map[string][]string{
"namespaces": ns,
"pods": po,
"containers": co,
}
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: resources,
}
err = response.WriteEntity(apiResponse)
if err != nil {
handleError(response, "Failed to write entity", err)
}
}
func initializeTimeUnits() map[string]string {
return map[string]string{
"5min": "5m",
"15min": "15m",
"30min": "30m",
"1hour": "1h",
"2hour": "2h",
"6hour": "6h",
"12hour": "12h",
"1day": "24h",
"2day": "48h",
"3day": "72h",
"7day": "168h",
}
}
func calculateQueryTimesUnix(duration string) (string, string) {
var startTime, endTime time.Time
d, err := time.ParseDuration(duration)
if err != nil {
errorMessage := fmt.Sprintf("Invalid duration format: %s."+
" Please provide a valid duration string (e.g., '300ms', '1.5h' "+
"or '2h45m'). LogError: %v", duration, err)
tools.LogError(errorMessage)
return "0", "0"
}
now := time.Now()
endTime = now
startTime = now.Add(-d)
fmt.Println("start time: ", startTime.Unix(), "end time: ", endTime.Unix())
return fmt.Sprintf("%d", startTime.Unix()), fmt.Sprintf("%d", endTime.Unix())
}
func (h *Handler) getLokiFileName(request *restful.Request, response *restful.Response) {
req, exists := h.RequestsMap["getFileNameList"]
if !exists {
errorMessage := fmt.Sprintf("Request configuration for 'getFileNameList' not found." +
" Please ensure it is defined in the RequestsMap.")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusNotFound, "Resource not found")
return
}
start, end := calculateQueryTimesUnix("500h")
queryParams := url.Values{}
queryParams.Add("start", start)
queryParams.Add("end", end)
fullURL := fmt.Sprintf("%s?%s", req.URL, queryParams.Encode())
result, err := sendHTTPRequestResourceOnly(req, fullURL)
if err != nil {
handleError(response, "Failed to send http request to loki component", err)
}
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: result,
}
fmt.Println(apiResponse)
err = response.WriteEntity(apiResponse)
fmt.Println(response.StatusCode())
if err != nil {
errorMessage := fmt.Sprintf("Request configuration for 'getFileNameList' not found." +
" Please ensure it is defined in the RequestsMap.")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusNotFound, errorMessage)
}
}
func queryParamBuilder(startTime, endTime, sizes, query string) url.Values {
queryParams := url.Values{}
queryParams.Add("start", startTime)
queryParams.Add("end", endTime)
queryParams.Add("limit", sizes)
queryParams.Add("query", query)
return queryParams
}
func lokiQueryBuilder(params LokiQueryParams) string {
var queryParts []string
if params.Namespace != "" {
queryParts = append(queryParts, fmt.Sprintf(`namespace="%s"`, params.Namespace))
}
if params.Pod != "" {
queryParts = append(queryParts, fmt.Sprintf(`pod="%s"`, params.Pod))
}
if params.Container != "" {
queryParts = append(queryParts, fmt.Sprintf(`container="%s"`, params.Container))
}
if params.Filename != "" {
queryParts = append(queryParts, fmt.Sprintf(`filename="%s"`, params.Filename))
}
baseQuery := "{" + strings.Join(queryParts, ", ") + "}"
if params.Keyword != "" {
baseQuery += fmt.Sprintf(` |= "%s"`, params.Keyword)
}
if params.LogLevel != "" {
regex := logLevelRegex[params.LogLevel]
baseQuery += fmt.Sprintf(` |~ "%s"`, regex)
}
return baseQuery
}
func sendHTTPRequest(req *HTTPRequest, fullURL string) (Response, error) {
httpReq, err := http.NewRequest(req.Method, fullURL, bytes.NewBuffer(req.Body))
var data Response
if err != nil {
errorMessage := fmt.Sprintf("failed to create HTTP request: %v", err)
tools.LogError(errorMessage)
return data, errors.New(errorMessage)
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
errorMessage := fmt.Sprintf("LogError sending request to Loki:")
tools.LogError(errorMessage)
return data, errors.New(errorMessage)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
errorMessage := fmt.Sprintf("LogError reading response body:")
tools.LogError(errorMessage)
return data, errors.New(errorMessage)
}
if err := json.Unmarshal(body, &data); err != nil {
errorMessage := fmt.Sprintf("LogError parsing JSON response:")
tools.LogError(errorMessage)
return data, errors.New(errorMessage)
}
return data, nil
}
func (h *Handler) lokiFilterSearch(request *restful.Request, response *restful.Response) {
filename := server.GetQueryParamOrDefault(request, server.FileName, "")
keyword := server.GetQueryParamOrDefault(request, server.Keyword, "")
loglevel := server.GetQueryParamOrDefault(request, server.Level, "")
params := LokiQueryParams{
Filename: filename,
Keyword: keyword,
LogLevel: loglevel,
}
h.performLokiSearch(request, response, params)
}
func (h *Handler) lokiNamespaceSearch(request *restful.Request, response *restful.Response) {
namespace := request.PathParameter("namespace")
keyword := server.GetQueryParamOrDefault(request, server.Keyword, "")
loglevel := server.GetQueryParamOrDefault(request, server.Level, "")
params := LokiQueryParams{
Namespace: namespace,
Keyword: keyword,
LogLevel: loglevel,
}
h.performLokiSearch(request, response, params)
}
func (h *Handler) lokiPodSearch(request *restful.Request, response *restful.Response) {
namespace := request.PathParameter("namespace")
pod := request.PathParameter("pod")
keyword := server.GetQueryParamOrDefault(request, server.Keyword, "")
loglevel := server.GetQueryParamOrDefault(request, server.Level, "")
params := LokiQueryParams{
Namespace: namespace,
Pod: pod,
Keyword: keyword,
LogLevel: loglevel,
}
h.performLokiSearch(request, response, params)
}
func (h *Handler) lokiContainerSearch(request *restful.Request, response *restful.Response) {
namespace := request.PathParameter("namespace")
pod := request.PathParameter("pod")
container := request.PathParameter("container")
keyword := server.GetQueryParamOrDefault(request, server.Keyword, "")
loglevel := server.GetQueryParamOrDefault(request, server.Level, "")
params := LokiQueryParams{
Namespace: namespace,
Pod: pod,
Container: container,
Keyword: keyword,
LogLevel: loglevel,
}
h.performLokiSearch(request, response, params)
}
func (h *Handler) performLokiSearch(request *restful.Request, response *restful.Response, params LokiQueryParams) {
req, exists := h.RequestsMap["filterRequest"]
if !exists {
response.WriteErrorString(http.StatusNotFound, "Request configuration"+
" for 'filterRequest' not found. Please ensure it is defined in the RequestsMap.")
return
}
timePeriod := server.GetQueryParamOrDefault(request, server.TimePeriod, "")
start := server.GetQueryParamOrDefault(request, server.StartTime, "1712460747")
end := server.GetQueryParamOrDefault(request, server.EndTime, "1712461647")
sizes := server.GetQueryParamOrDefault(request, server.Sizes, "10")
if timePeriod != "" {
duration := h.timeUnitsDict[timePeriod]
start, end = calculateQueryTimesUnix(duration)
}
query := lokiQueryBuilder(params)
queryParams := queryParamBuilder(start, end, sizes, query)
tools.LogInfo("Query: ", query)
fullURL := fmt.Sprintf("%s?%s", req.URL, queryParams.Encode())
result, err := sendHTTPRequest(req, fullURL)
if err != nil {
handleError(response, "Failed to send http request to loki component", err)
return
}
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: result,
}
err = response.WriteEntity(apiResponse)
if err != nil {
handleError(response, "Failed to write entity: %v", err)
}
}
func getList(result map[string]interface{}, params QueryResourcesParams) (namespaces, pods, containers []string) {
nsSet := make(map[string]bool)
podSet := make(map[string]bool)
containerSet := make(map[string]bool)
sources, exists := result["data"].([]interface{})
if !exists {
errorMessage := fmt.Sprintf("Request configuration for 'data' not found." +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
}
for _, item := range sources {
entry, ok := item.(map[string]interface{})
if !ok {
errorMessage := fmt.Sprintf("type assertion failed for item: expected map[string]interface{}, got %T", item)
tools.LogError(errorMessage)
continue
}
namespace, nOk := entry["namespace"].(string)
pod, pOk := entry["pod"].(string)
container, cOk := entry["container"].(string)
if nOk && pOk && cOk {
tools.LogInfo("resource exists")
}
nsCondition := (params.Namespace == "" || (nOk && namespace == params.Namespace))
podCondition := (params.Pod == "" || (pOk && pod == params.Pod))
containerCondition := (params.Container == "" || (cOk && container == params.Container))
if nsCondition && podCondition && containerCondition {
if nOk {
nsSet[namespace] = true
}
if pOk {
podSet[pod] = true
}
if cOk {
containerSet[container] = true
}
}
}
for ns := range nsSet {
namespaces = append(namespaces, ns)
}
for p := range podSet {
pods = append(pods, p)
}
for c := range containerSet {
containers = append(containers, c)
}
return namespaces, pods, containers
}
func findContainerByName(containers []Container, containerName string) (*Container, error) {
for _, container := range containers {
if container.Name == containerName {
return &container, nil
}
}
return nil, fmt.Errorf("container %s not found", containerName)
}
func extractReadOnlyMounts(container *Container) []VolumeMount {
var mounts []VolumeMount
for _, mount := range container.VolumeMounts {
if mount.ReadOnly {
mounts = append(mounts, mount)
}
}
return mounts
}
func parsePodDetail(respBody []byte, containerName string) ([]VolumeMount, error) {
var podDetail PodDetail
if err := json.Unmarshal(respBody, &podDetail); err != nil {
return nil, fmt.Errorf("error parsing JSON response: %v", err)
}
container, err := findContainerByName(podDetail.Spec.Containers, containerName)
if err != nil {
errorMessage := fmt.Sprintf("Fail to find Pod name" +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
return nil, err
}
return extractReadOnlyMounts(container), nil
}
func (h *Handler) sendHTTPRequest(reqKey, podName string) (*http.Response, error) {
req, exists := h.RequestsMap[reqKey]
if !exists {
errorMessage := fmt.Sprintf("Fail to load requestMap" +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
return nil, fmt.Errorf("request configuration for '%s' not found", reqKey)
}
httpReq, err := http.NewRequest(req.Method, req.URL+podName, bytes.NewBuffer(req.Body))
if err != nil {
errorMessage := fmt.Sprintf("Fail to build new request" +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
return nil, err
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
return client.Do(httpReq)
}
func (h *Handler) getAlertingRules(request *restful.Request, response *restful.Response) {
req, exists := h.RequestsMap["getAlertingRules"]
if !exists {
errorMessage := fmt.Sprintf("Request configuration for 'getAlertingRules' not found." +
" Please ensure it is defined in the RequestsMap.")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusNotFound, "Resource not found")
return
}
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewBuffer(req.Body))
if err != nil {
errorMessage := fmt.Sprintf("LogError sending request to Loki:")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusNotFound, errorMessage)
return
}
for key, value := range req.Headers {
httpReq.Header.Set(key, value)
}
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
errorMessage := fmt.Sprintf("LogError sending request to Loki:")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusBadRequest, errorMessage)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
errorMessage := fmt.Sprintf("LogError reading response body:")
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusBadRequest, errorMessage)
return
}
var ruleFile RuleFile
err = yaml.Unmarshal(body, &ruleFile)
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: ruleFile,
}
err = response.WriteEntity(apiResponse)
if err != nil {
errorMessage := fmt.Sprintf("Failed to write entity: %v", err)
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusBadRequest, errorMessage)
return
}
}
func (h *Handler) displayconfigmap(request *restful.Request, response *restful.Response) {
result, err := h.getReadOnlyVolumesByNode()
if err != nil {
fmt.Println(result)
}
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: result,
}
err = response.WriteEntity(apiResponse)
if err != nil {
errorMessage := fmt.Sprintf("Failed to write entity: %v", err)
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusBadRequest, errorMessage)
}
}
func (h *Handler) findPodByNamePrefix(prefix string) (string, error) {
pods, err := h.PodClient.List(context.TODO(), metav1.ListOptions{})
if err != nil {
errorMessage := fmt.Sprintf("Fail to find Pod name" +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
return "", err
}
for _, pod := range pods.Items {
if strings.HasPrefix(pod.Name, prefix) {
return pod.Name, nil
}
}
return "", fmt.Errorf("no pod found with prefix %s", prefix)
}
func (h *Handler) getPodNode(podName string) (string, error) {
pod, err := h.PodClient.Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
errorMessage := fmt.Sprintf("Fail to build Pod client" +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
return "", fmt.Errorf("failed to get pod: %v", err)
}
return pod.Spec.NodeName, nil
}
func (h *Handler) getReadOnlyVolumesByNode() (map[string][]VolumeMount, error) {
podName, err := h.findPodByNamePrefix("loki-promtail")
if err != nil {
errorMessage := fmt.Sprintf("Fail to find Pod name" +
" Please ensure it is defined in the Json output.")
tools.LogError(errorMessage)
return nil, err
}
returnValues := make(map[string][]VolumeMount)
pod, err := h.PodClient.Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
fmt.Println("Failed to get pod:", err)
}
for _, container := range pod.Spec.Containers {
fmt.Printf("Container Name: %s\n", container.Name)
if container.Name != "promtail" {
continue
}
for _, mount := range container.VolumeMounts {
if mount.ReadOnly && mount.MountPath != "/var/run/secrets/kubernetes.io/serviceaccount" {
key := fmt.Sprintf("%s/%s", pod.Name, container.Name)
vol := VolumeMount{
MountPath: mount.MountPath,
Name: mount.Name,
ReadOnly: mount.ReadOnly,
}
returnValues[key] = append(returnValues[key], vol)
fmt.Printf("Mount Path: %s, Volume Name: %s, ReadOnly: %t\n",
mount.MountPath, mount.Name, mount.ReadOnly)
}
}
}
return returnValues, nil
}
func (h *Handler) getConfigmapDetails(request *restful.Request, response *restful.Response) {
cm, err := h.ConfigmapClient.Get(context.TODO(), "loki-promtail", metav1.GetOptions{})
if err != nil {
handleError(response, "Failed to get configmap", err)
return
}
promtailYAML, ok := cm.Data["promtail.yaml"]
if !ok {
handleError(response, "promtail.yaml not found in configmap", nil)
return
}
var promtailConfig PromtailConfig
if err = yaml.Unmarshal([]byte(promtailYAML), &promtailConfig); err != nil {
handleError(response, "Failed to unmarshal promtail configuration", err)
return
}
jobPaths := getJobPaths(promtailConfig)
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: jobPaths,
}
if err = response.WriteEntity(apiResponse); err != nil {
handleError(response, "Failed to write entity", err)
}
}
func getJobPaths(promtailConfig PromtailConfig) map[string]string {
jobPaths := make(map[string]string)
for _, scrapeConfig := range promtailConfig.ScrapeConfigs {
jobPaths[scrapeConfig.JobName] = determinePath(scrapeConfig)
}
return jobPaths
}
func determinePath(scrapeConfig ScrapeConfig) string {
path := ""
if scrapeConfig.JobName == "kubernetes-pods" {
path = "/var/log/pods/*/*/*.log"
} else {
for _, staticConfig := range scrapeConfig.StaticConfigs {
if p, ok := staticConfig.Labels["__path__"]; ok {
path = p
break
}
}
}
return path
}
func handleError(response *restful.Response, message string, err error) {
apiResponse := ApiResponse{
Code: 400,
Msg: "Unexpected Error occurred",
Data: nil,
}
errorMessage := message
if err != nil {
errorMessage = fmt.Sprintf("%s: %v", message, err)
}
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusBadRequest, errorMessage)
err = response.WriteEntity(apiResponse)
if err != nil {
errorMessage := "Error Write Entity: "
tools.LogError(errorMessage, err)
}
}
func (h *Handler) getMountPaths(response *restful.Response) ([]string, error) {
result, err := h.getReadOnlyVolumesByNode()
var mountPaths []string
if err != nil {
return mountPaths, err
}
for _, mounts := range result {
for _, mount := range mounts {
mountPaths = append(mountPaths, mount.MountPath)
}
}
return mountPaths, err
}
func validatePath(path string) bool {
if path == "" {
errorMessage := "Input Path should not be empty"
tools.LogError(errorMessage)
return false
}
maxLength := 300
if len(path) > maxLength {
errorMessage := "Input Path should less than 300 chars"
tools.LogError(errorMessage)
return false
}
if !strings.HasSuffix(path, ".log") {
errorMessage := "Input Path should end with .log"
tools.LogError(errorMessage)
return false
}
pattern := `^[\w\-.*\/]+$`
match, err := regexp.MatchString(pattern, path)
if err != nil {
fmt.Println("正则表达式错误:", err)
return false
}
if !match {
errorMessage := "Input path can only use - / . * for special characters"
tools.LogError(errorMessage)
}
return match
}
func isValidUpdateRequest(req PromtailUpdateRequest, mountPaths []string) bool {
isValidPrefix := false
for _, mountPath := range mountPaths {
if strings.HasPrefix(req.Path, mountPath) {
isValidPrefix = true
break
}
}
if !isValidPrefix {
errorMessage := "The path should follow with the agents collect source"
tools.LogError(errorMessage)
}
isValidValues := false
if validatePath(req.Path) && validateJobName(req.JobName) {
isValidValues = true
} else {
errorMessage := "The patch body format is invalid"
tools.LogError(errorMessage)
isValidValues = false
}
isValidJobName := true
requiredFields := []string{"jobName", "path"}
for _, field := range requiredFields {
switch field {
case "jobName":
if req.JobName == "" {
isValidJobName = false
errorMessage := "The input body job name should as same as jobName"
tools.LogError(errorMessage)
}
case "path":
if req.Path == "" {
isValidJobName = false
errorMessage := "The input body path name should as same as path"
tools.LogError(errorMessage)
}
default:
isValidJobName = false
}
}
isValidContent := isValidValues && isValidJobName
return isValidContent && isValidPrefix
}
func (h *Handler) isValidUpdateRequest(updateReq PromtailUpdateRequest, response *restful.Response) bool {
mountPaths, err := h.getMountPaths(response)
if err != nil {
handleError(response, "Failed to get mount paths", err)
return false
}
if !isValidUpdateRequest(updateReq, mountPaths) {
handleError(response, "Failed to validate update request", fmt.Errorf("the body format is invalid"))
return false
}
return true
}
func (h *Handler) createScrapeConfig(updateReq PromtailUpdateRequest) ScrapeConfig {
return ScrapeConfig{
JobName: updateReq.JobName,
StaticConfigs: []StaticConfig{{
Targets: []string{"localhost"},
Labels: map[string]string{
"__path__": updateReq.Path,
"job": updateReq.JobName,
},
}},
}
}
func validateJobName(jobName string) bool {
maxLength := 30
if len(jobName) < 1 || len(jobName) > maxLength {
errorMessage := "Input job name should not be empty or combined with more than 30 chars"
tools.LogError(errorMessage)
return false
}
pattern := `^[a-zA-Z0-9_-]+$`
match, err := regexp.MatchString(pattern, jobName)
if err != nil {
return false
}
if !match {
errorMessage := "Input job name should only contains a-z A-Z 0-9 - _"
tools.LogError(errorMessage)
}
return match
}
func (h *Handler) updatePromtailConfig(request *restful.Request, response *restful.Response) {
cm, err := h.ConfigmapClient.Get(context.TODO(), "loki-promtail", metav1.GetOptions{})
if err != nil {
handleError(response, "Failed to get configmap client", err)
}
promtailYAML, ok := cm.Data["promtail.yaml"]
if !ok {
handleError(response, "Failed to find promtail.yaml file", err)
}
var promtailConfig PromtailConfig
err = yaml.Unmarshal([]byte(promtailYAML), &promtailConfig)
if err != nil {
handleError(response, "Failed to unmarshal yaml file", err)
}
var updateReq PromtailUpdateRequest
if err := request.ReadEntity(&updateReq); err != nil {
response.WriteErrorString(http.StatusBadRequest, "Invalid request")
}
if !h.isValidUpdateRequest(updateReq, response) {
response.WriteErrorString(http.StatusBadRequest, "Invalid request format")
return
}
newConfig := h.createScrapeConfig(updateReq)
promtailConfig.ScrapeConfigs = append(promtailConfig.ScrapeConfigs, newConfig)
updatedYAML, err := yaml.Marshal(promtailConfig)
if err != nil {
response.WriteError(http.StatusInternalServerError, fmt.Errorf("failed to marshal updated promtail "+
"config: %v", err))
}
cm.Data["promtail.yaml"] = string(updatedYAML)
_, err = h.ConfigmapClient.Update(context.TODO(), cm, metav1.UpdateOptions{})
if err != nil {
response.WriteError(http.StatusInternalServerError, fmt.Errorf("failed to update ConfigMap: %v", err))
}
podName, err := h.findPodByNamePrefix("loki-promtail")
if err != nil {
handleError(response, "Failed to find pod name", err)
}
h.deletePod(podName)
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: nil,
}
err = response.WriteEntity(apiResponse)
}
func (h *Handler) deleteJob(request *restful.Request, response *restful.Response) {
jobName := request.PathParameter("job-name")
cm, err := h.ConfigmapClient.Get(context.TODO(), "loki-promtail", metav1.GetOptions{})
if err != nil {
response.WriteError(http.StatusInternalServerError, fmt.Errorf("failed to get configmap: %v", err))
return
}
var promtailConfig PromtailConfig
err = yaml.Unmarshal([]byte(cm.Data["promtail.yaml"]), &promtailConfig)
if err != nil {
response.WriteError(http.StatusInternalServerError, fmt.Errorf("failed to unmarshal promtail"+
" config: %v", err))
return
}
var updatedJobs []ScrapeConfig
for _, job := range promtailConfig.ScrapeConfigs {
if job.JobName != jobName {
updatedJobs = append(updatedJobs, job)
}
}
if len(updatedJobs) == len(promtailConfig.ScrapeConfigs) {
response.WriteErrorString(http.StatusBadRequest, "Job name not found")
return
}
promtailConfig.ScrapeConfigs = updatedJobs
updatedYAML, err := yaml.Marshal(promtailConfig)
if err != nil {
response.WriteError(http.StatusInternalServerError, fmt.Errorf("failed to marshal updated promtail "+
"config: %v", err))
return
}
cm.Data["promtail.yaml"] = string(updatedYAML)
_, err = h.ConfigmapClient.Update(context.TODO(), cm, metav1.UpdateOptions{})
if err != nil {
response.WriteError(http.StatusInternalServerError, fmt.Errorf("failed to update configmap: %v", err))
return
}
podName, err := h.findPodByNamePrefix("loki-promtail")
if err != nil {
return
}
err = h.deletePod(podName)
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: nil}
err = response.WriteEntity(apiResponse)
}
func (h *Handler) deletePod(podName string) error {
deletePolicy := metav1.DeletePropagationForeground
return h.PodClient.Delete(context.TODO(), podName, metav1.DeleteOptions{
PropagationPolicy: &deletePolicy,
})
}
func (h *Handler) getPodRunningStatus(request *restful.Request, response *restful.Response) {
podName, err := h.findPodByNamePrefix("loki-promtail")
pod, err := h.PodClient.Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
return
}
for _, cond := range pod.Status.Conditions {
if cond.Type == "Ready" && cond.Status == "True" {
apiResponse := ApiResponse{
Code: 200,
Msg: "The promtail now is available",
Data: nil,
}
err = response.WriteEntity(apiResponse)
if err != nil {
return
}
return
}
}
apiResponse := ApiResponse{
Code: 503,
Msg: "The promtail now is unavailable",
Data: nil,
}
err = response.WriteEntity(apiResponse)
return
}
func calculateTimeRange(pivotTime string, interval string, isFirstHalf bool) (string, string) {
base := 10
bitSize := 64
nanoSec, err := strconv.ParseInt(pivotTime, base, bitSize)
if err != nil {
tools.LogError(fmt.Sprintf("Error parsing timestamp: %v", err))
return "0", "0"
}
pivot := time.Unix(0, nanoSec)
duration, err := time.ParseDuration(interval)
if err != nil {
errorMessage := fmt.Sprintf("Invalid duration format: %s."+
" Please provide a valid duration string (e.g., '300ms', '1.5h' "+
"or '2h45m'). LogError: %v", duration, err)
tools.LogError(errorMessage)
return "0", "0"
}
if isFirstHalf {
endTime := pivot
startTime := endTime.Add(-duration)
return fmt.Sprintf("%d", startTime.Unix()), fmt.Sprintf("%d", endTime.Unix())
} else {
startTime := pivot
endTime := startTime.Add(duration)
return fmt.Sprintf("%d", startTime.Unix()), fmt.Sprintf("%d", endTime.Unix())
}
}
func (h *Handler) handleContextLogDetails(request *restful.Request, response *restful.Response) {
if !h.checkRequestConfig(response) {
return
}
params := h.buildQueryParams(request)
query := lokiQueryBuilder(params)
result1, err := h.executeLokiQuery(request, params, query, true)
if err != nil {
handleError(response, "Failed to send HTTP request", err)
return
}
result2, err := h.executeLokiQuery(request, params, query, false)
if err != nil {
handleError(response, "Failed to send HTTP request", err)
return
}
h.respondWithResults(response, result1, result2)
}
func (h *Handler) checkRequestConfig(response *restful.Response) bool {
_, exists := h.RequestsMap["filterRequest"]
if !exists {
errorMessage := "Request configuration for 'filterRequest' not found. Please ensure it is defined " +
"in the RequestsMap."
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusNotFound, errorMessage)
return false
}
return true
}
func (h *Handler) buildQueryParams(request *restful.Request) LokiQueryParams {
filename := server.GetQueryParamOrDefault(request, server.FileName, "")
loglevel := server.GetQueryParamOrDefault(request, server.Level, "")
keyword := server.GetQueryParamOrDefault(request, server.Keyword, "")
return LokiQueryParams{
Filename: filename,
LogLevel: loglevel,
Keyword: keyword,
}
}
func (h *Handler) executeLokiQuery(request *restful.Request, params LokiQueryParams, query string,
isFirstQuery bool) (interface{}, error) {
timestamp := server.GetQueryParamOrDefault(request, server.TimeStamp, "")
contextInterval := server.GetQueryParamOrDefault(request, server.TimePeriod, "")
duration := h.timeUnitsDict[contextInterval]
startTime, endTime := calculateTimeRange(timestamp, duration, isFirstQuery)
fmt.Println("start time: ", startTime, "end time: ", endTime)
queryParams := queryParamBuilder(startTime, endTime, server.GetQueryParamOrDefault(request, server.Sizes,
"10"), query)
req, _ := h.RequestsMap["filterRequest"]
fullURL := fmt.Sprintf("%s?%s", req.URL, queryParams.Encode())
fmt.Println(fullURL)
return sendHTTPRequest(req, fullURL)
}
func (h *Handler) respondWithResults(response *restful.Response, result1, result2 interface{}) {
combinedResult := CombinedResult{Results: []interface{}{result1, result2}}
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: combinedResult,
}
if err := response.WriteEntity(apiResponse); err != nil {
handleError(response, "Failed to write entity", err)
}
}
func (h *Handler) getlogSecrets(request *restful.Request, response *restful.Response) {
secret, err := h.SecretsClient.Get(context.TODO(), "loki", metav1.GetOptions{})
if err != nil {
errorMessage := "Fail to build k8s secret client"
tools.LogError(errorMessage)
return
}
data, ok := secret.Data["loki.yaml"]
if !ok {
errorMessage := "Fail to get secret information"
tools.LogError(errorMessage)
return
}
var config SecretConfig
err = yaml.Unmarshal(data, &config)
if err != nil {
errorMessage := "Error parsing JSON: "
tools.LogError(errorMessage, err)
return
}
apiResponse := ApiResponse{
Code: 200,
Msg: "Operation successful",
Data: config,
}
err = response.WriteEntity(apiResponse)
if err != nil {
errorMessage := fmt.Sprintf("Failed to write entity: %v", err)
tools.LogError(errorMessage)
response.WriteErrorString(http.StatusBadRequest, errorMessage)
}
}