-
Notifications
You must be signed in to change notification settings - Fork 0
/
logging.clj
216 lines (183 loc) · 7 KB
/
logging.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
(ns monkey.ci.logging
"Handles log configuration and how to process logs from a build script"
(:require [babashka.fs :as fs]
[clj-commons.byte-streams :as bs]
[clojure.java.io :as io]
[clojure.string :as cs]
[clojure.tools.logging :as log]
[manifold.deferred :as md]
[monkey.ci
[build :as b]
[config :as c]
[oci :as oci]
[runtime :as rt]
[utils :as u]]
[monkey.ci.storage.oci :as st]
[monkey.oci.os.core :as os]))
(defprotocol LogCapturer
"Used to allow processes to store log information. Depending on the implementation,
this can be local on disk, or some cloud object storage."
(log-output [this])
(handle-stream [this in]))
(defmulti make-logger (comp :type :logging))
(deftype InheritLogger []
LogCapturer
(log-output [_]
:inherit)
(handle-stream [_ _]
nil))
(defmethod make-logger :inherit [_]
(fn [& _]
(->InheritLogger)))
(defmethod make-logger :default [_]
(fn [& _]
(->InheritLogger)))
(deftype FileLogger [conf rt path]
LogCapturer
(log-output [_]
(let [f (apply io/file
(or (:dir conf) (io/file (:work-dir rt) "logs"))
(concat (drop-last (b/get-sid rt)) path))]
(.mkdirs (.getParentFile f))
f))
(handle-stream [_ _]
nil))
(defmethod make-logger :file [conf]
(partial ->FileLogger (:logging conf)))
(defn- ensure-cleanup
"Registers a shutdown hook to ensure the deferred is being completed, even if the
system shuts down. The shutdown hook is removed on completion. If we don't do
this, the multipart streams don't get committed when the vm shuts down in the
process."
[d]
(let [shutdown? (atom false)
t (Thread. (fn []
(reset! shutdown? true)
(log/debug "Waiting for upload to complete...")
(deref d)
(log/debug "Upload completed")))
remove-hook (fn [& _]
(when-not @shutdown?
(try
(.removeShutdownHook (Runtime/getRuntime) t)
(catch Exception _
(log/warn "Unable to remove shutdown hook, process is probably already shutting down.")))))]
(when (md/deferred? d)
(.addShutdownHook (Runtime/getRuntime) t)
(md/on-realized d remove-hook remove-hook))
d))
(defn sid->path [{:keys [prefix]} path sid]
(->> (concat [prefix] sid path)
(remove nil?)
(cs/join "/")))
(deftype OciBucketLogger [conf rt path]
LogCapturer
(log-output [_]
:stream)
(handle-stream [_ in]
(let [sid (get-in rt [:build :sid])
;; Since the configured path already includes the build id,
;; we only use repo id to build the path
on (sid->path conf path (u/sid->repo-sid sid))]
(-> (oci/stream-to-bucket (assoc conf :object-name on)
in)
(ensure-cleanup)))))
(defmethod make-logger :oci [conf]
(fn [rt path]
(-> conf
:logging
(->OciBucketLogger rt path))))
(defn handle-process-streams
"Given a process return value (as from `babashka.process/process`) and two
loggers, will invoke the `handle-stream` on each logger for out and error
output. Returns the process."
[{:keys [out err] :as proc} loggers]
(->> [out err]
(map handle-stream loggers)
(doall))
proc)
(defprotocol LogRetriever
"Interface for retrieving log files. This is more or less the opposite of the `LogCapturer`.
It allows to list logs and fetch a log according to path."
(list-logs [this build-sid]
"Lists available logs for the build id, with name and size")
(fetch-log [this build-sid path]
"Retrieves log for given build id and path. Returns a stream and its size."))
(deftype FileLogRetriever [dir]
LogRetriever
(list-logs [_ build-sid]
(let [build-dir (apply io/file dir build-sid)
->out (fn [p]
{:name (str (fs/relativize build-dir p))
:size (fs/size p)})]
;; Recursively list files in the build dir
(->> (loop [dirs [build-dir]
r []]
(if (empty? dirs)
r
(let [f (fs/list-dir (first dirs))
{ffiles false fdirs true} (group-by fs/directory? f)]
(recur (concat (rest dirs) fdirs)
(concat r ffiles)))))
(map ->out))))
(fetch-log [_ build-sid path]
(let [f (apply io/file dir (concat build-sid [path]))]
(when (.exists f)
(io/input-stream f)))))
(defmulti make-log-retriever (comp :type :logging))
(defmethod make-log-retriever :file [conf]
(->FileLogRetriever (get-in conf [:logging :dir])))
(deftype NoopLogRetriever []
LogRetriever
(list-logs [_ _]
[])
(fetch-log [_ _ _]
nil))
(defmethod make-log-retriever :default [_]
(->NoopLogRetriever))
(defn- sid->prefix [sid {:keys [prefix]}]
(cond->> (str (cs/join st/delim sid) st/delim)
(some? prefix) (str prefix "/")))
(deftype OciBucketLogRetriever [client conf]
LogRetriever
(list-logs [_ sid]
(let [prefix (sid->prefix sid conf)
->out (fn [r]
;; Strip the prefix to retain the relative path
(update r :name subs (count prefix)))]
@(md/chain
(os/list-objects client (-> conf
(select-keys [:ns :compartment-id :bucket-name])
(assoc :prefix prefix
:fields "name,size")))
(fn [{:keys [objects]}]
(->> objects
(map ->out))))))
(fetch-log [_ sid path]
;; TODO Also return object size, so we can tell the client
;; FIXME Return nil if file does not exist, instead of throwing an error
@(md/chain
(os/get-object client (-> conf
(select-keys [:ns :compartment-id :bucket-name])
(assoc :object-name (str (sid->prefix sid conf) path))))
bs/to-input-stream)))
(defmethod make-log-retriever :oci [conf]
(let [oci-conf (-> conf
:logging
(oci/->oci-config))
client (os/make-client oci-conf)]
(->OciBucketLogRetriever client oci-conf)))
;;; Configuration handling
(defmulti normalize-logging-config (comp :type :logging))
(defmethod normalize-logging-config :default [conf]
conf)
(defmethod normalize-logging-config :file [conf]
(update-in conf [:logging :dir] #(or (u/abs-path %) (u/combine (c/abs-work-dir conf) "logs"))))
(defmethod normalize-logging-config :oci [conf]
(-> (oci/normalize-config conf :logging)
(update :logging select-keys [:type :credentials :ns :compartment-id :bucket-name :region])))
(defmethod c/normalize-key :logging [k conf]
(c/normalize-typed k conf normalize-logging-config))
(defmethod rt/setup-runtime :logging [conf _]
{:maker (make-logger conf)
:retriever (make-log-retriever conf)})